debugging ticket creation, lost spell checker
This commit is contained in:
@@ -14,29 +14,49 @@
|
|||||||
<div {{ $attributes->whereDoesntStartWith('wire:model')->merge(['class' => 'space-y-2']) }}
|
<div {{ $attributes->whereDoesntStartWith('wire:model')->merge(['class' => 'space-y-2']) }}
|
||||||
@if ($model) data-helpdesk-editor-model="{{ $model }}" @endif
|
@if ($model) data-helpdesk-editor-model="{{ $model }}" @endif
|
||||||
x-on:helpdesk-editor-reset.window="if ($event.detail.model === model) clear()"
|
x-on:helpdesk-editor-reset.window="if ($event.detail.model === model) clear()"
|
||||||
x-on:helpdesk-editor-sync.window="if ($event.detail.model === model) sync()" x-data="{
|
x-on:helpdesk-editor-sync.window="if ($event.detail.model === model) sync(false)" x-data="{
|
||||||
model: @js($model),
|
model: @js($model),
|
||||||
value: @js($initialValue),
|
value: @js($initialValue),
|
||||||
sync() {
|
submitHandler: null,
|
||||||
|
sync(live = false) {
|
||||||
this.value = this.$refs.editor.innerHTML;
|
this.value = this.$refs.editor.innerHTML;
|
||||||
|
|
||||||
if (this.model) {
|
if (this.model) {
|
||||||
this.$wire.set(this.model, this.value);
|
this.$wire.set(this.model, this.value, live);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
command(action, value = null) {
|
command(action, value = null) {
|
||||||
this.$refs.editor.focus();
|
this.$refs.editor.focus();
|
||||||
document.execCommand(action, false, value);
|
document.execCommand(action, false, value);
|
||||||
this.sync();
|
this.sync(false);
|
||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
this.value = '';
|
this.value = '';
|
||||||
this.$refs.editor.innerHTML = '';
|
this.$refs.editor.innerHTML = '';
|
||||||
|
|
||||||
if (this.model) {
|
if (this.model) {
|
||||||
this.$wire.set(this.model, '');
|
this.$wire.set(this.model, '', false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.$refs.editor.innerHTML = this.value || '';
|
this.$refs.editor.innerHTML = this.value || '';
|
||||||
|
|
||||||
|
this.submitHandler = () => this.sync(false);
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const form = this.$root.closest('form');
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', this.submitHandler, { capture: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
const form = this.$root.closest('form');
|
||||||
|
|
||||||
|
if (form && this.submitHandler) {
|
||||||
|
form.removeEventListener('submit', this.submitHandler, { capture: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}">
|
}">
|
||||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-200">{{ $label }}</label>
|
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-200">{{ $label }}</label>
|
||||||
@@ -66,9 +86,9 @@
|
|||||||
class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Clear</button>
|
class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-ref="editor" contenteditable="true" spellcheck="true" lang="en-GB" autocapitalize="sentences" autocorrect="on" autocomplete="on" inputmode="text"
|
<div x-ref="editor" contenteditable="true" spellcheck="true" lang="en-GB" autocapitalize="off" autocorrect="off" autocomplete="off" inputmode="text"
|
||||||
@if ($editorDusk) data-dusk="{{ $editorDusk }}" dusk="{{ $editorDusk }}" @endif
|
@if ($editorDusk) data-dusk="{{ $editorDusk }}" dusk="{{ $editorDusk }}" @endif
|
||||||
x-on:input.debounce.150ms="sync" x-on:keyup="sync" x-on:paste="$nextTick(() => sync())" x-on:blur="sync"
|
x-on:input.debounce.250ms="sync(false)" x-on:paste="$nextTick(() => sync(false))" x-on:blur="sync(false)"
|
||||||
class="prose prose-sm min-h-[{{ (int) $rows * 28 }}px] max-w-none bg-white p-4 text-sm text-zinc-900 outline-none dark:prose-invert dark:bg-zinc-950 dark:text-zinc-100"
|
class="prose prose-sm min-h-[{{ (int) $rows * 28 }}px] max-w-none bg-white p-4 text-sm text-zinc-900 outline-none dark:prose-invert dark:bg-zinc-950 dark:text-zinc-100"
|
||||||
data-placeholder="{{ $placeholder }}"></div>
|
data-placeholder="{{ $placeholder }}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,17 +15,31 @@ final class TicketCreateEditorSyncFilesTest extends TestCase
|
|||||||
public function test_ticket_create_editor_pushes_details_body_to_livewire_before_validation(): void
|
public function test_ticket_create_editor_pushes_details_body_to_livewire_before_validation(): void
|
||||||
{
|
{
|
||||||
$view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php'));
|
$view = file_get_contents(base_path('Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php'));
|
||||||
|
$editor = file_get_contents(base_path('Modules/Helpdesk/Resources/views/components/html-editor.blade.php'));
|
||||||
|
|
||||||
$this->assertIsString($view);
|
$this->assertIsString($view);
|
||||||
|
$this->assertIsString($editor);
|
||||||
$this->assertStringContainsString('wire:model="body"', $view);
|
$this->assertStringContainsString('wire:model="body"', $view);
|
||||||
$this->assertStringNotContainsString('wire:model.live="body"', $view);
|
$this->assertStringNotContainsString('wire:model.live="body"', $view);
|
||||||
$this->assertStringContainsString('submit', $view);
|
$this->assertStringContainsString('form.addEventListener(\'submit\'', $editor);
|
||||||
$this->assertTrue(
|
$this->assertStringContainsString('this.$wire.set(this.model, this.value, false)', $editor);
|
||||||
str_contains($view, 'Livewire')
|
}
|
||||||
|| str_contains($view, '$wire')
|
|
||||||
|| str_contains($view, '@this')
|
/**
|
||||||
|| str_contains($view, 'wire:model="body"'),
|
* The rich text editor must not fire a Livewire request for every keystroke,
|
||||||
'Ticket create editor must keep the body field synchronised with Livewire before validation.'
|
* otherwise the submit button can remain disabled and typed content can race
|
||||||
);
|
* with stale Livewire responses.
|
||||||
|
*/
|
||||||
|
public function test_ticket_create_editor_does_not_live_sync_on_each_keypress(): void
|
||||||
|
{
|
||||||
|
$editor = file_get_contents(base_path('Modules/Helpdesk/Resources/views/components/html-editor.blade.php'));
|
||||||
|
|
||||||
|
$this->assertIsString($editor);
|
||||||
|
$this->assertStringNotContainsString('x-on:keyup="sync"', $editor);
|
||||||
|
$this->assertStringNotContainsString('this.$wire.set(this.model, this.value);', $editor);
|
||||||
|
$this->assertStringContainsString('x-on:input.debounce.250ms="sync(false)"', $editor);
|
||||||
|
$this->assertStringContainsString('autocorrect="off"', $editor);
|
||||||
|
$this->assertStringContainsString('autocomplete="off"', $editor);
|
||||||
|
$this->assertStringContainsString('spellcheck="true"', $editor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,3 +228,15 @@
|
|||||||
- Mirrored manual time-log entries into helpdesk_ticket_time_logs when the timer history table is present so the ticket detail modal stays current.
|
- Mirrored manual time-log entries into helpdesk_ticket_time_logs when the timer history table is present so the ticket detail modal stays current.
|
||||||
- Updated TicketTimeLogTest to use authorised Helpdesk fixture users and lookup data instead of ad-hoc users that fail the ticket update policy.
|
- Updated TicketTimeLogTest to use authorised Helpdesk fixture users and lookup data instead of ad-hoc users that fail the ticket update policy.
|
||||||
- Kept the pricing-free dashboard test baseline and preserved append-only changelog history.
|
- Kept the pricing-free dashboard test baseline and preserved append-only changelog history.
|
||||||
|
|
||||||
|
- version: 1.9.51
|
||||||
|
date: 2026-06-24
|
||||||
|
type: bugfix
|
||||||
|
summary: Stabilise Helpdesk ticket create rich editor and single-click submit.
|
||||||
|
changes:
|
||||||
|
- Changed the reusable Helpdesk rich editor to defer Livewire body synchronisation instead of sending a server request for every keypress.
|
||||||
|
- Added a submit-capture synchronisation hook so the latest Details content is included before TicketCreate validation runs.
|
||||||
|
- Removed the duplicate keyup synchronisation path that could leave Create Ticket disabled while typing.
|
||||||
|
- Disabled browser autocorrect and autocomplete on the contenteditable editor while preserving native spellcheck support.
|
||||||
|
- Updated regression coverage for deferred editor sync, submit sync and keypress race prevention.
|
||||||
|
- Preserved append-only changelog history.
|
||||||
|
|||||||
Reference in New Issue
Block a user