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:
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"version": "1.9.41"
|
||||
"version": "1.9.42"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user