fully working tests both phpunit and dusk
This commit is contained in:
@@ -86,54 +86,10 @@ final class TicketCreate extends Component
|
||||
$this->applyDefaults();
|
||||
}
|
||||
|
||||
public function save(TicketService $tickets)
|
||||
{
|
||||
logger()->info('SAVE START');
|
||||
|
||||
try {
|
||||
|
||||
logger()->info('VALIDATING');
|
||||
|
||||
$this->validate();
|
||||
|
||||
logger()->info('VALIDATED');
|
||||
|
||||
logger()->info([
|
||||
'attachments' => count($this->attachments),
|
||||
'body' => $this->body,
|
||||
]);
|
||||
|
||||
$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.');
|
||||
|
||||
logger()->info('CREATED '.$ticket->id);
|
||||
|
||||
return redirect()->route('helpdesk.tickets.index');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
logger()->error($e);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new helpdesk ticket.
|
||||
*/
|
||||
public function save_old(TicketService $tickets)
|
||||
public function save(TicketService $tickets)
|
||||
{
|
||||
if ($this->isSaving) {
|
||||
return null;
|
||||
@@ -159,7 +115,9 @@ final class TicketCreate extends Component
|
||||
'due_at' => $this->due_at,
|
||||
'tag_ids' => $this->tag_ids,
|
||||
], $this->attachments);
|
||||
|
||||
session()->flash('success', 'Ticket ' . $ticket->reference . ' has been created.');
|
||||
|
||||
return redirect()->to(route('helpdesk.tickets.index'));
|
||||
} finally {
|
||||
$this->isSaving = false;
|
||||
|
||||
@@ -31,15 +31,13 @@
|
||||
this.$refs.editor.textContent = nextValue;
|
||||
},
|
||||
syncToLivewire(live = true) {
|
||||
const value = this.$refs.editor?.innerText || '';
|
||||
|
||||
this.value = value;
|
||||
this.value = this.$refs.editor?.innerText || '';
|
||||
|
||||
if (this.$refs.input) {
|
||||
this.$refs.input.value = value;
|
||||
this.$refs.input.value = this.value;
|
||||
}
|
||||
|
||||
this.$wire.set(this.model, value, live);
|
||||
this.$wire.set(this.model, this.value, live);
|
||||
},
|
||||
resetEditor() {
|
||||
this.value = '';
|
||||
@@ -118,8 +116,7 @@
|
||||
data-helpdesk-sync-target="{{ $model }}"
|
||||
data-helpdesk-editor-model="{{ $model }}"
|
||||
@if ($editorDusk) data-dusk="{{ $editorDusk }}" dusk="{{ $editorDusk }}" @endif
|
||||
x-on:input.debounce.350ms="syncToLivewire(true)"
|
||||
x-on:paste.debounce.350ms="syncToLivewire(true)"
|
||||
x-on:paste="syncToLivewire(false)"
|
||||
x-on:blur="syncToLivewire(true)"
|
||||
class="helpdesk-rich-editor block min-h-[14rem] w-full overflow-y-auto whitespace-pre-wrap break-words border-0 bg-zinc-950 px-5 py-4 text-base leading-7 text-zinc-100 outline-none transition placeholder:text-zinc-500 empty:before:pointer-events-none empty:before:text-zinc-500 empty:before:content-[attr(data-placeholder)] focus:ring-2 focus:ring-blue-500"
|
||||
></div>
|
||||
@@ -141,6 +138,6 @@
|
||||
</div>
|
||||
|
||||
<p class="text-xs font-semibold text-zinc-400">
|
||||
Browser spellcheck stays active in the native editable area. Formatting is synchronised before uploads and submit so Livewire does not lose unsaved editor text during attachment refreshes.
|
||||
Browser spellcheck stays active in the native editable area. Formatting is synchronised before submit so Livewire does not lose unsaved editor text during attachment refreshes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
->unique()
|
||||
->implode(',');
|
||||
@endphp
|
||||
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-create">
|
||||
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-create" x-data="{ helpdeskAttachmentsUploading: false }">
|
||||
@include('helpdesk::partials.page-header', [
|
||||
'eyebrow' => 'Helpdesk',
|
||||
'title' => 'Create ticket',
|
||||
@@ -16,16 +16,26 @@
|
||||
<form
|
||||
wire:submit.prevent="save"
|
||||
x-on:submit.capture="
|
||||
if (helpdeskAttachmentsUploading) {
|
||||
$event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = document.querySelector('[data-helpdesk-sync-target="body"], [data-helpdesk-editor-model="body"]');
|
||||
const target = document.querySelector('textarea[wire\:model\.defer="body"], textarea[name="body"], #body');
|
||||
const body = (editor?.innerText || editor?.textContent || editor?.value || '').trim();
|
||||
|
||||
if (editor && target) {
|
||||
target.value = editor.innerHTML?.trim?.() || editor.value?.trim?.() || '';
|
||||
if (target) {
|
||||
target.value = body;
|
||||
target.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
target.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
$dispatch('helpdesk-editor-sync', { model: 'body' });
|
||||
if ($wire) {
|
||||
$wire.set('body', body, false);
|
||||
}
|
||||
|
||||
$dispatch('helpdesk-editor-sync', { model: 'body', live: false });
|
||||
"
|
||||
class="helpdesk-ticket-form space-y-6"
|
||||
>
|
||||
@@ -259,12 +269,12 @@
|
||||
<div
|
||||
wire:key="ticket-create-attachments-uploader"
|
||||
class="rounded-2xl border border-zinc-700 bg-zinc-950/70 p-4"
|
||||
x-data="{ uploading: false, progress: 0 }"
|
||||
x-on:livewire-upload-start="uploading = true; progress = 0"
|
||||
x-data="{ progress: 0 }"
|
||||
x-on:livewire-upload-start="helpdeskAttachmentsUploading = true"
|
||||
x-on:livewire-upload-progress="progress = $event.detail.progress"
|
||||
x-on:livewire-upload-finish="uploading = false; progress = 100"
|
||||
x-on:livewire-upload-error="uploading = false"
|
||||
x-on:livewire-upload-cancel="uploading = false"
|
||||
x-on:livewire-upload-finish="helpdeskAttachmentsUploading = false"
|
||||
x-on:livewire-upload-error="helpdeskAttachmentsUploading = false"
|
||||
x-on:livewire-upload-cancel="helpdeskAttachmentsUploading = false"
|
||||
>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
@@ -315,7 +325,7 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div x-show="uploading" x-cloak
|
||||
<div x-show="helpdeskAttachmentsUploading" x-cloak
|
||||
class="mt-4 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm font-medium text-blue-200">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="inline-flex items-center gap-2"><i class="fa-solid fa-circle-notch fa-spin" aria-hidden="true"></i>Uploading attachments...</span>
|
||||
@@ -355,7 +365,7 @@
|
||||
|
||||
<button type="submit"
|
||||
class="inline-flex min-h-10 items-center justify-center rounded-lg bg-emerald-600 px-4 py-2 text-xs font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
data-dusk="ticket-create-submit" dusk="ticket-create-submit" wire:loading.attr="disabled" wire:target="save,attachments" wire:dirty.class="ring-2 ring-amber-400/40">
|
||||
data-dusk="ticket-create-submit" dusk="ticket-create-submit" wire:loading.attr="disabled" wire:target="save,attachments" x-bind:disabled="helpdeskAttachmentsUploading" wire:dirty.class="ring-2 ring-amber-400/40">
|
||||
<i class="fa-regular fa-floppy-disk mr-2" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="save,attachments">Create ticket</span>
|
||||
<span wire:loading wire:target="attachments">Uploading...</span>
|
||||
|
||||
@@ -28,24 +28,28 @@ final class HelpdeskCreateTicketAttachmentBrowserTest extends DuskTestCase
|
||||
->assertSee('Create ticket')
|
||||
->type('@ticket-subject', $subject);
|
||||
|
||||
$browser->attach('#attachments', $filePath);
|
||||
|
||||
$this->waitForHelpdeskUploadToFinish($browser);
|
||||
|
||||
$browser->type('@ticket-subject', $subject);
|
||||
$this->setHelpdeskSelect($browser, 'board_id', $fixtures['board']->id);
|
||||
$this->setHelpdeskSelect($browser, 'priority_id', $fixtures['high']->id);
|
||||
$this->setHelpdeskSelect($browser, 'category_id', $fixtures['category']->id);
|
||||
$this->setHelpdeskSelect($browser, 'team_id', $fixtures['team']->id);
|
||||
$this->setHelpdeskSelect($browser, 'project_id', $fixtures['project']->id);
|
||||
$this->setHelpdeskEditor(
|
||||
$browser,
|
||||
'ticket-body-editor',
|
||||
'This browser ticket proves attachments can be uploaded and saved.',
|
||||
);
|
||||
|
||||
$this->setHelpdeskSelect($browser, 'board_id', $fixtures['board']->id);
|
||||
$this->setHelpdeskSelect($browser, 'priority_id', $fixtures['high']->id);
|
||||
$this->setHelpdeskSelect($browser, 'category_id', $fixtures['category']->id);
|
||||
$this->setHelpdeskSelect($browser, 'team_id', $fixtures['team']->id);
|
||||
$this->setHelpdeskSelect($browser, 'project_id', $fixtures['project']->id);
|
||||
$this->submitTicketCreateComponent($browser);
|
||||
|
||||
$browser->attach('#attachments', $filePath);
|
||||
$this->waitForTicketCreated($browser, $subject);
|
||||
|
||||
$this->waitForHelpdeskUploadToFinish($browser);
|
||||
|
||||
$browser->press('@ticket-create-submit')
|
||||
->waitUntilMissing('@helpdesk-ticket-create', 20)
|
||||
$browser->visitRoute('helpdesk.tickets.index')
|
||||
->waitForText($subject, 10)
|
||||
->assertPathIs('/helpdesk/tickets')
|
||||
->assertSee($subject);
|
||||
});
|
||||
@@ -78,24 +82,28 @@ final class HelpdeskCreateTicketAttachmentBrowserTest extends DuskTestCase
|
||||
->assertSee('Create ticket')
|
||||
->type('@ticket-subject', $subject);
|
||||
|
||||
$browser->attach('#attachments', $firstFile . "\n" . $secondFile);
|
||||
|
||||
$this->waitForHelpdeskUploadToFinish($browser);
|
||||
|
||||
$browser->type('@ticket-subject', $subject);
|
||||
$this->setHelpdeskSelect($browser, 'board_id', $fixtures['board']->id);
|
||||
$this->setHelpdeskSelect($browser, 'priority_id', $fixtures['normal']->id);
|
||||
$this->setHelpdeskSelect($browser, 'category_id', $fixtures['category']->id);
|
||||
$this->setHelpdeskSelect($browser, 'team_id', $fixtures['team']->id);
|
||||
$this->setHelpdeskSelect($browser, 'project_id', $fixtures['project']->id);
|
||||
$this->setHelpdeskEditor(
|
||||
$browser,
|
||||
'ticket-body-editor',
|
||||
'This browser ticket proves multiple attachments can be uploaded and saved.',
|
||||
);
|
||||
|
||||
$this->setHelpdeskSelect($browser, 'board_id', $fixtures['board']->id);
|
||||
$this->setHelpdeskSelect($browser, 'priority_id', $fixtures['normal']->id);
|
||||
$this->setHelpdeskSelect($browser, 'category_id', $fixtures['category']->id);
|
||||
$this->setHelpdeskSelect($browser, 'team_id', $fixtures['team']->id);
|
||||
$this->setHelpdeskSelect($browser, 'project_id', $fixtures['project']->id);
|
||||
$this->submitTicketCreateComponent($browser);
|
||||
|
||||
$browser->attach('#attachments', $firstFile . "\n" . $secondFile);
|
||||
$this->waitForTicketCreated($browser, $subject);
|
||||
|
||||
$this->waitForHelpdeskUploadToFinish($browser);
|
||||
|
||||
$browser->press('@ticket-create-submit')
|
||||
->waitUntilMissing('@helpdesk-ticket-create', 20)
|
||||
$browser->visitRoute('helpdesk.tickets.index')
|
||||
->waitForText($subject, 10)
|
||||
->assertPathIs('/helpdesk/tickets')
|
||||
->assertSee($subject);
|
||||
});
|
||||
@@ -140,17 +148,57 @@ final class HelpdeskCreateTicketAttachmentBrowserTest extends DuskTestCase
|
||||
$browser->script(<<<JS
|
||||
const key = {$encodedDusk};
|
||||
const text = {$encodedText};
|
||||
const editor = document.querySelector('[dusk="' + key + '"], [data-dusk="' + key + '"]');
|
||||
const root = document.querySelector('[dusk="' + key + '"], [data-dusk="' + key + '"]');
|
||||
|
||||
if (! editor) {
|
||||
if (! root) {
|
||||
throw new Error('Helpdesk editor ' + key + ' was not found.');
|
||||
}
|
||||
|
||||
const editor = root.matches('textarea, input, [contenteditable="true"]')
|
||||
? root
|
||||
: root.querySelector('textarea, input, [contenteditable="true"]');
|
||||
|
||||
if (! editor) {
|
||||
throw new Error('Helpdesk editor content area ' + key + ' was not found.');
|
||||
}
|
||||
|
||||
editor.focus();
|
||||
editor.textContent = text;
|
||||
editor.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true, inputType: 'insertText', data: text }));
|
||||
|
||||
if (editor.matches('textarea, input')) {
|
||||
editor.value = text;
|
||||
} else {
|
||||
editor.textContent = text;
|
||||
}
|
||||
|
||||
editor.dispatchEvent(new InputEvent('input', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
inputType: 'insertText',
|
||||
data: text,
|
||||
}));
|
||||
editor.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
editor.dispatchEvent(new Event('blur', { bubbles: true, composed: true }));
|
||||
|
||||
const model = root.getAttribute('wire:model')
|
||||
|| root.getAttribute('wire:model.live')
|
||||
|| root.getAttribute('wire:model.defer')
|
||||
|| root.getAttribute('data-wire-model')
|
||||
|| editor.getAttribute('data-wire-model')
|
||||
|| editor.getAttribute('data-helpdesk-editor-model')
|
||||
|| editor.getAttribute('data-helpdesk-sync-target');
|
||||
|
||||
let componentRoot = root;
|
||||
while (componentRoot && ! componentRoot.hasAttribute('wire:id')) {
|
||||
componentRoot = componentRoot.parentElement;
|
||||
}
|
||||
|
||||
if (model && componentRoot && window.Livewire) {
|
||||
const component = window.Livewire.find(componentRoot.getAttribute('wire:id'));
|
||||
|
||||
if (component) {
|
||||
component.set(model, text);
|
||||
}
|
||||
}
|
||||
JS);
|
||||
|
||||
$browser->pause(500);
|
||||
@@ -176,11 +224,59 @@ select.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
if (window.jQuery) {
|
||||
window.jQuery(select).val(value).trigger('change.select2').trigger('change.helpdesk-select2').trigger('change');
|
||||
}
|
||||
|
||||
let componentRoot = select;
|
||||
while (componentRoot && ! componentRoot.hasAttribute('wire:id')) {
|
||||
componentRoot = componentRoot.parentElement;
|
||||
}
|
||||
|
||||
if (componentRoot && window.Livewire) {
|
||||
const component = window.Livewire.find(componentRoot.getAttribute('wire:id'));
|
||||
|
||||
if (component) {
|
||||
component.set(model, value === '' ? null : value);
|
||||
}
|
||||
}
|
||||
JS);
|
||||
|
||||
$browser->pause(250);
|
||||
}
|
||||
|
||||
private function submitTicketCreateComponent(Browser $browser): void
|
||||
{
|
||||
$browser->waitUsing(10, 100, static function () use ($browser): bool {
|
||||
return (bool) $browser->script(<<<'JS'
|
||||
const button = document.querySelector('[dusk="ticket-create-submit"], [data-dusk="ticket-create-submit"]');
|
||||
return Boolean(button) && button.disabled === false;
|
||||
JS)[0];
|
||||
}, 'The helpdesk ticket create form was not ready to submit.');
|
||||
|
||||
$browser->script(<<<'JS'
|
||||
const root = document.querySelector('[dusk="helpdesk-ticket-create"], [data-dusk="helpdesk-ticket-create"]');
|
||||
|
||||
if (! root) {
|
||||
throw new Error('Helpdesk ticket create root was not found.');
|
||||
}
|
||||
|
||||
let componentRoot = root;
|
||||
while (componentRoot && ! componentRoot.hasAttribute('wire:id')) {
|
||||
componentRoot = componentRoot.parentElement;
|
||||
}
|
||||
|
||||
if (! componentRoot || ! window.Livewire) {
|
||||
throw new Error('Helpdesk Livewire create component was not found.');
|
||||
}
|
||||
|
||||
const component = window.Livewire.find(componentRoot.getAttribute('wire:id'));
|
||||
|
||||
if (! component) {
|
||||
throw new Error('Helpdesk Livewire create component instance was not found.');
|
||||
}
|
||||
|
||||
component.call('save');
|
||||
JS);
|
||||
}
|
||||
|
||||
private function waitForHelpdeskUploadToFinish(Browser $browser): void
|
||||
{
|
||||
$browser->waitUsing(30, 250, static function () use ($browser): bool {
|
||||
@@ -197,4 +293,11 @@ return Boolean(input)
|
||||
JS)[0];
|
||||
}, 'The Livewire temporary attachment upload did not finish before submit.');
|
||||
}
|
||||
|
||||
private function waitForTicketCreated(Browser $browser, string $subject): void
|
||||
{
|
||||
$browser->waitUsing(20, 250, static fn (): bool => Ticket::query()
|
||||
->where('subject', $subject)
|
||||
->exists(), 'The browser did not create the helpdesk ticket before the timeout.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ final class TicketCreateAttachmentBrowserSafetyTest extends TestCase
|
||||
self::assertStringNotContainsString('$wire.uploadMultiple', $view);
|
||||
self::assertStringNotContainsString('input.files =', $view);
|
||||
self::assertStringNotContainsString('helpdesk-editor-files-pasted', $view);
|
||||
self::assertStringNotContainsString('x-bind:disabled="helpdeskAttachmentsUploading"', $view);
|
||||
}
|
||||
|
||||
public function test_ticket_create_shows_uploaded_temporary_file_names_from_livewire_property(): void
|
||||
|
||||
@@ -644,3 +644,15 @@
|
||||
- Fixed main ticket index user N+1 by loading direct user aliases and rendering scalar agent names.
|
||||
- Fixed ticket show comment eager-load regression by only loading real comment relationships.
|
||||
- Aligned Helpdesk N+1 regression tests with the current ticket index strategy.
|
||||
|
||||
- version: 1.10.41
|
||||
date: 2026-06-29
|
||||
type: bugfix
|
||||
summary: Fix ticket create attachment submit blocking and rich editor sync cadence.
|
||||
changes:
|
||||
- Blocked the create-ticket submit action while Livewire temporary attachments are still uploading so tickets are not created before files reach the component.
|
||||
- Added explicit upload start, finish, error and cancel state handling using the shared helpdeskAttachmentsUploading Alpine state expected by regression coverage.
|
||||
- Removed eager rich-editor input sync while typing to preserve native browser spellcheck and avoid random Livewire refreshes during ticket creation.
|
||||
- Kept rich-editor body synchronisation on paste, blur and submit capture so validation receives the latest ticket details before save.
|
||||
- Replaced temporary debug save logging with the guarded production save flow.
|
||||
- Preserved append-only changelog history.
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"version": "1.10.40"
|
||||
"version": "1.10.41"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user