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.
This commit is contained in:
Mark Gregory
2026-06-23 11:13:54 +01:00
parent 7c4a94b5ea
commit 4ee0b8e604
6 changed files with 92 additions and 39 deletions
+61 -33
View File
@@ -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<int, int> */
#[Validate('array')]
public array $tag_ids = [];
/** @var array<int, TemporaryUploadedFile> */
#[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<string, mixed>
*/
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;
}
}
/**
@@ -274,7 +274,7 @@
data-dusk="ticket-body-editor" dusk="ticket-body-editor">{!! $body ?? '' !!}</div>
</div>
<textarea id="body" wire:model.live="body" class="sr-only" tabindex="-1" aria-hidden="true"></textarea>
<textarea id="body" wire:model="body" class="sr-only" tabindex="-1" aria-hidden="true"></textarea>
@error('body')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@@ -333,10 +333,10 @@
<button type="submit"
class="inline-flex items-center gap-2 rounded-xl bg-green-600 px-5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
data-dusk="ticket-create-submit" dusk="ticket-create-submit" wire:loading.attr="disabled">
data-dusk="ticket-create-submit" dusk="ticket-create-submit" wire:loading.attr="disabled" wire:target="save" wire:dirty.class="ring-2 ring-amber-400/40">
<i class="fa-regular fa-floppy-disk" aria-hidden="true"></i>
<span wire:loading.remove>Create Ticket</span>
<span wire:loading>Creating...</span>
<span wire:loading.remove wire:target="save">Create Ticket</span>
<span wire:loading wire:target="save">Creating...</span>
</button>
</div>
</div>
@@ -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);
}
@@ -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();
+12
View File
@@ -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.
+1 -1
View File
@@ -22,5 +22,5 @@
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
],
"files": [],
"version": "1.9.41"
"version": "1.9.42"
}