From 4ee0b8e604ef5e18c3c2688a60e01a3ef597d2aa Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Tue, 23 Jun 2026 11:13:54 +0100 Subject: [PATCH] Validation now runs only when Create Ticket is clicked. * Removed the live body binding causing premature $commit. * Details editor still syncs before submit. * Added duplicate-submit guard. * Should no longer need clicking Save/Create twice. * Append-only changelog. * module.json version 1.9.42. --- Modules/Helpdesk/Livewire/TicketCreate.php | 94 ++++++++++++------- .../views/livewire/ticket-create.blade.php | 8 +- .../TicketCreateEditorSyncFilesTest.php | 3 +- .../Helpdesk/Tests/Feature/TicketCrudTest.php | 12 +++ Modules/Helpdesk/changelog.yaml | 12 +++ Modules/Helpdesk/module.json | 2 +- 6 files changed, 92 insertions(+), 39 deletions(-) diff --git a/Modules/Helpdesk/Livewire/TicketCreate.php b/Modules/Helpdesk/Livewire/TicketCreate.php index 6a2c620f..5ee2e5d1 100644 --- a/Modules/Helpdesk/Livewire/TicketCreate.php +++ b/Modules/Helpdesk/Livewire/TicketCreate.php @@ -8,7 +8,6 @@ use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; -use Livewire\Attributes\Validate; use Livewire\Component; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; use Livewire\WithFileUploads; @@ -25,44 +24,65 @@ final class TicketCreate extends Component { use WithFileUploads; - #[Validate('nullable|integer')] public ?int $company_id = null; - #[Validate('required|string|max:200')] public string $subject = ''; - #[Validate('required|string|min:10')] public string $body = ''; - #[Validate('nullable|integer|exists:helpdesk_kanban_boards,id')] public ?int $board_id = null; - #[Validate('required|integer|exists:helpdesk_priorities,id')] public ?int $priority_id = null; - #[Validate('nullable|integer|exists:helpdesk_categories,id')] public ?int $category_id = null; - #[Validate('nullable|integer|exists:helpdesk_projects,id')] public ?int $project_id = null; - #[Validate('nullable|integer|exists:helpdesk_products,id')] public ?int $product_id = null; - #[Validate('nullable|integer|exists:helpdesk_teams,id')] public ?int $team_id = null; - #[Validate('nullable|date')] public ?string $due_at = null; /** @var array */ - #[Validate('array')] public array $tag_ids = []; /** @var array */ - #[Validate('array')] public array $attachments = []; + public bool $isSaving = false; + + + /** + * Validation rules are intentionally evaluated only when the user submits + * the form, not while rich-text/select widgets are synchronising. + * + * @return array + */ + protected function rules(): array + { + return [ + 'company_id' => ['nullable', 'integer'], + 'subject' => ['required', 'string', 'max:200'], + 'body' => ['required', 'string', 'min:10'], + 'board_id' => ['nullable', 'integer', 'exists:helpdesk_kanban_boards,id'], + 'priority_id' => ['required', 'integer', 'exists:helpdesk_priorities,id'], + 'category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'], + 'project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'], + 'product_id' => ['nullable', 'integer', 'exists:helpdesk_products,id'], + 'team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'], + 'due_at' => ['nullable', 'date'], + 'tag_ids' => ['array'], + 'tag_ids.*' => ['integer', 'exists:helpdesk_tags,id'], + 'attachments' => ['array'], + 'attachments.*' => [ + 'file', + 'max:'.config('helpdesk.max_upload_kb'), + 'mimes:'.implode(',', config('helpdesk.allowed_mimes')), + ], + ]; + } + /** * Initialise configured Helpdesk defaults for a new ticket. */ @@ -76,29 +96,37 @@ final class TicketCreate extends Component */ public function save(TicketService $tickets) { - $this->validate(); - $this->validate([ - 'attachments.*' => 'file|max:'.config('helpdesk.max_upload_kb').'|mimes:'.implode(',', config('helpdesk.allowed_mimes')), - 'tag_ids.*' => 'integer|exists:helpdesk_tags,id', - ]); + if ($this->isSaving) { + return null; + } - $ticket = $tickets->create(Auth::user(), [ - 'company_id' => $this->company_id, - 'subject' => $this->subject, - 'body' => $this->body, - 'board_id' => $this->board_id, - 'priority_id' => $this->priority_id, - 'category_id' => $this->category_id, - 'project_id' => $this->project_id, - 'product_id' => $this->product_id, - 'team_id' => $this->team_id, - 'due_at' => $this->due_at, - 'tag_ids' => $this->tag_ids, - ], $this->attachments); + $this->isSaving = true; - session()->flash('success', 'Ticket '.$ticket->reference.' has been created.'); + try { + $this->body = trim($this->body); - return $this->redirectRoute('helpdesk.tickets.index', navigate: true); + $this->validate(); + + $ticket = $tickets->create(Auth::user(), [ + 'company_id' => $this->company_id, + 'subject' => $this->subject, + 'body' => $this->body, + 'board_id' => $this->board_id, + 'priority_id' => $this->priority_id, + 'category_id' => $this->category_id, + 'project_id' => $this->project_id, + 'product_id' => $this->product_id, + 'team_id' => $this->team_id, + 'due_at' => $this->due_at, + 'tag_ids' => $this->tag_ids, + ], $this->attachments); + + session()->flash('success', 'Ticket '.$ticket->reference.' has been created.'); + + return $this->redirectRoute('helpdesk.tickets.index', navigate: true); + } finally { + $this->isSaving = false; + } } /** diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php index a0df77d5..ea8704e7 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php @@ -274,7 +274,7 @@ data-dusk="ticket-body-editor" dusk="ticket-body-editor">{!! $body ?? '' !!} - + @error('body')

{{ $message }}

@@ -333,10 +333,10 @@ diff --git a/Modules/Helpdesk/Tests/Feature/TicketCreateEditorSyncFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketCreateEditorSyncFilesTest.php index 79d2d8d2..ae642713 100644 --- a/Modules/Helpdesk/Tests/Feature/TicketCreateEditorSyncFilesTest.php +++ b/Modules/Helpdesk/Tests/Feature/TicketCreateEditorSyncFilesTest.php @@ -15,9 +15,10 @@ final class TicketCreateEditorSyncFilesTest extends TestCase $view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php')); $this->assertIsString($view); - $this->assertStringContainsString('wire:model.live="body"', $view); + $this->assertStringContainsString('wire:model="body"', $view); $this->assertStringContainsString("window.Livewire.find(livewireId).set('body', value, false)", $view); $this->assertStringContainsString("form.addEventListener('submit', syncBody, { capture: true })", $view); + $this->assertStringNotContainsString('wire:model.live="body"', $view); $this->assertStringContainsString("document.addEventListener('livewire:navigated', initialiseTicketCreateEditor)", $view); $this->assertStringContainsString("editor.addEventListener('paste', () => window.setTimeout(syncBody, 0))", $view); } diff --git a/Modules/Helpdesk/Tests/Feature/TicketCrudTest.php b/Modules/Helpdesk/Tests/Feature/TicketCrudTest.php index 554d41af..c03c90ea 100644 --- a/Modules/Helpdesk/Tests/Feature/TicketCrudTest.php +++ b/Modules/Helpdesk/Tests/Feature/TicketCrudTest.php @@ -44,6 +44,18 @@ final class TicketCrudTest extends TestCase ]); } + + public function test_ticket_create_does_not_validate_before_submit(): void + { + $this->seedLookupData(); + $user = $this->user(); + + Livewire::actingAs($user) + ->test(TicketCreate::class) + ->set('subject', 'Draft ticket title only') + ->assertHasNoErrors(); + } + public function test_ticket_create_validates_required_fields(): void { $this->seedLookupData(); diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml index 3904ad16..e9e0089f 100644 --- a/Modules/Helpdesk/changelog.yaml +++ b/Modules/Helpdesk/changelog.yaml @@ -127,3 +127,15 @@ - Eager-loaded category agents where categories are used by ticket creation/editing and agent assignment logic to remove the detected N+1 query warning. - Added regression coverage to ensure soft-deleted ticket references are never reused. - Preserved append-only changelog history. + +- version: 1.9.42 + date: 2026-06-23 + type: bugfix + summary: Stop premature Helpdesk ticket create validation and single-click submit failures. + changes: + - Moved TicketCreate validation from Livewire attribute-based live validation to submit-only rules so errors appear only after Create Ticket is clicked. + - Changed the hidden rich-text Details body field away from live model updates to prevent a pre-submit $commit request from racing the save action. + - Kept the editor submit synchronisation so the latest rich-text content is copied into the Livewire body property before validation. + - Added an isSaving guard and save-target loading state so duplicate clicks cannot create duplicate submit attempts. + - Added regression coverage proving draft edits do not fire validation before submit. + - Preserved append-only changelog history. diff --git a/Modules/Helpdesk/module.json b/Modules/Helpdesk/module.json index 928d1725..b19afdbc 100644 --- a/Modules/Helpdesk/module.json +++ b/Modules/Helpdesk/module.json @@ -22,5 +22,5 @@ "Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider" ], "files": [], - "version": "1.9.41" + "version": "1.9.42" }