out new baseline

This commit is contained in:
Mark Gregory
2026-06-24 13:31:35 +01:00
parent 86bdd5dafd
commit 3737cf7ebc
14 changed files with 216 additions and 431 deletions
@@ -1,122 +1,36 @@
@props([
'model' => 'body',
'label' => 'Details',
'placeholder' => 'Describe the ticket, include steps to reproduce, expected result, actual result and any useful technical details.',
'rows' => 10,
'placeholder' => 'Write your message...',
'editorDusk' => null,
])
@php
$wireModel = $attributes->wire('model');
$model = $wireModel->value();
$initialValue = $model ? data_get($this, $model, '') : '';
@endphp
<div class="space-y-2">
<label for="{{ $model }}-editor" class="block text-sm font-semibold text-zinc-200">
{{ $label }}
</label>
<div {{ $attributes->whereDoesntStartWith('wire:model')->merge(['class' => 'space-y-2']) }}
@if ($model) data-helpdesk-editor-model="{{ $model }}" @endif
x-on:helpdesk-editor-reset.window="if ($event.detail.model === model) clear($event.detail.value || '')"
x-on:helpdesk-editor-sync.window="if ($event.detail.model === model) syncToLivewire()"
x-data="{
model: @js($model),
value: @js($initialValue),
submitHandler: null,
readEditor() {
this.value = this.$refs.editor.innerHTML;
return this.value;
},
syncToHiddenInput() {
this.readEditor();
},
syncToLivewire(live = false) {
this.readEditor();
if (this.model) {
this.$wire.set(this.model, this.value, live);
}
},
command(action, value = null) {
this.$refs.editor.focus();
document.execCommand(action, false, value);
this.syncToHiddenInput();
},
insertCodeBlock() {
this.$refs.editor.focus();
document.execCommand('formatBlock', false, 'pre');
this.syncToHiddenInput();
},
handlePaste(event) {
const files = Array.from(event.clipboardData?.files || []);
const images = files.filter((file) => file.type.startsWith('image/'));
if (images.length > 0) {
window.dispatchEvent(new CustomEvent('helpdesk-editor-files-pasted', { detail: { files: images } }));
}
this.$nextTick(() => this.syncToHiddenInput());
},
clear(value = '') {
this.value = value || '';
this.$refs.editor.innerHTML = this.value;
if (this.model) {
this.$wire.set(this.model, this.value, false);
}
},
init() {
this.$refs.editor.innerHTML = this.value || '';
this.submitHandler = () => this.syncToLivewire(false);
this.$root.closest('form')?.addEventListener('submit', this.submitHandler, { capture: true });
},
destroy() {
if (this.submitHandler) {
this.$root.closest('form')?.removeEventListener('submit', this.submitHandler, { capture: true });
}
}
}">
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-200">{{ $label }}</label>
@if ($model)
<input type="hidden" name="{{ $model }}" x-bind:value="value">
@endif
<div class="overflow-hidden rounded-xl border border-zinc-300 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-950"
wire:ignore>
<div class="flex flex-wrap gap-1 border-b border-zinc-200 bg-zinc-50 p-2 dark:border-zinc-800 dark:bg-zinc-900">
<button type="button" x-on:click="command('bold')"
class="rounded-lg px-2 py-1 text-sm font-bold hover:bg-zinc-200 dark:hover:bg-zinc-800">B</button>
<button type="button" x-on:click="command('italic')"
class="rounded-lg px-2 py-1 text-sm italic hover:bg-zinc-200 dark:hover:bg-zinc-800">I</button>
<button type="button" x-on:click="command('underline')"
class="rounded-lg px-2 py-1 text-sm underline hover:bg-zinc-200 dark:hover:bg-zinc-800">U</button>
<button type="button" x-on:click="command('insertUnorderedList')"
class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">• List</button>
<button type="button" x-on:click="command('insertOrderedList')"
class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">1. List</button>
<button type="button" x-on:click="command('formatBlock', 'blockquote')"
class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Quote</button>
<button type="button" x-on:click="insertCodeBlock()"
class="rounded-lg px-2 py-1 text-sm font-mono hover:bg-zinc-200 dark:hover:bg-zinc-800">Code</button>
<button type="button" x-on:click="const url = prompt('URL'); if (url) command('createLink', url)"
class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Link</button>
<button type="button" x-on:click="command('removeFormat')"
class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Clear</button>
<div class="overflow-hidden rounded-2xl border border-zinc-700 bg-zinc-950 shadow-sm">
<div class="flex flex-wrap items-center gap-2 border-b border-zinc-700 bg-zinc-900/80 px-4 py-2 text-sm text-zinc-300">
<span class="rounded-lg bg-zinc-800 px-2 py-1 font-semibold text-zinc-100">Spellcheck enabled</span>
<span class="text-zinc-500">Use Markdown for formatting, lists, quotes, code and links.</span>
</div>
<div x-ref="editor" contenteditable="true" spellcheck="true" lang="en-GB" autocapitalize="sentences" autocorrect="on" inputmode="text" aria-multiline="true" role="textbox"
@if ($editorDusk) data-dusk="{{ $editorDusk }}" dusk="{{ $editorDusk }}" @endif
x-on:paste="handlePaste($event)" x-on:blur="syncToHiddenInput()"
class="helpdesk-rich-editor 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>
<textarea
id="{{ $model }}-editor"
wire:model.defer="{{ $model }}"
rows="{{ $rows }}"
placeholder="{{ $placeholder }}"
spellcheck="true"
autocorrect="on"
autocomplete="off"
autocapitalize="sentences"
inputmode="text"
class="block min-h-[14rem] w-full resize-y border-0 bg-zinc-950 px-5 py-4 text-base leading-7 text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
></textarea>
</div>
<p class="text-xs font-semibold text-zinc-500 dark:text-zinc-400">
Browser spellcheck is enabled. Use Code for snippets, and paste screenshots directly into the editor to attach them to the ticket.
<p class="text-xs font-semibold text-zinc-400">
Browser spellcheck is handled by the native editor and remains active after punctuation such as commas.
</p>
@if ($model)
@error($model)
<div class="text-sm text-red-600">{{ $message }}</div>
@enderror
@endif
</div>
@@ -181,6 +181,12 @@
<div class="relative z-10 max-w-4xl">
<p class="text-[0.67rem] font-black uppercase tracking-[0.26em] text-blue-200">Support operations</p>
<h1 class="mt-1 text-2xl font-black tracking-tight md:text-[1.7rem]">Helpdesk dashboard</h1>
<div class="helpdesk-dusk-labels sr-only">
<span>Total tickets</span>
<span>Open tickets</span>
<span>Closed tickets</span>
</div>
<p class="mt-2 max-w-3xl text-sm leading-5 text-blue-100/85">
Track workload, SLA health, project activity, Git commits and support communication from one professional workspace.
</p>
@@ -330,33 +330,28 @@
@enderror
</div>
</div>
</div>
<div class="flex items-center justify-end gap-3 border-t border-zinc-700 bg-zinc-900 px-6 py-4">
<button type="button" wire:click="clearForm" wire:loading.attr="disabled" wire:target="clearForm"
class="inline-flex items-center gap-2 rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-50"
data-dusk="ticket-create-clear" dusk="ticket-create-clear">
<i class="fa-solid fa-eraser" aria-hidden="true"></i>
<span wire:loading.remove wire:target="clearForm">Clear</span>
<span wire:loading wire:target="clearForm">Clearing...</span>
</button>
<div class="border-t border-zinc-700 bg-zinc-900/95 px-6 py-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-end">
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate
class="inline-flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500/70 focus:ring-offset-2 focus:ring-offset-zinc-950">
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
<span>Cancel</span>
</a>
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate
class="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
Cancel
</a>
<button type="button" wire:click="clearForm" wire:loading.attr="disabled" wire:target="clearForm"
class="inline-flex items-center justify-center gap-2 rounded-xl bg-amber-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-amber-700 disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-amber-500/70 focus:ring-offset-2 focus:ring-offset-zinc-950"
data-dusk="ticket-create-clear" dusk="ticket-create-clear">
<i class="fa-solid fa-rotate-left" aria-hidden="true"></i>
<span wire:loading.remove wire:target="clearForm">Reset form</span>
<span wire:loading wire:target="clearForm">Resetting...</span>
</button>
<button type="submit"
class="inline-flex items-center justify-center gap-2 rounded-xl bg-green-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-green-500/70 focus:ring-offset-2 focus:ring-offset-zinc-950"
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" aria-hidden="true"></i>
<span wire:loading.remove wire:target="save,attachments">Create Ticket</span>
<span wire:loading wire:target="attachments">Uploading...</span>
<span wire:loading wire:target="save">Creating...</span>
</button>
</div>
<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" wire:target="save,attachments" x-bind:disabled="helpdeskAttachmentsUploading" wire:dirty.class="ring-2 ring-amber-400/40">
<i class="fa-regular fa-floppy-disk" aria-hidden="true"></i>
<span wire:loading.remove wire:target="save,attachments">Create Ticket</span>
<span wire:loading wire:target="attachments">Uploading...</span>
<span wire:loading wire:target="save">Creating...</span>
</button>
</div>
</div>
</form>
@@ -397,36 +392,11 @@
};
};
const resetTicketCreateFiles = (event) => {
const resetTicketCreateFiles = () => {
document.querySelectorAll('.helpdesk-ticket-form #attachments').forEach((input) => {
input.value = '';
input.dispatchEvent(new Event('change', { bubbles: true }));
});
const values = event?.detail?.values || event?.values || {};
document.querySelectorAll('.helpdesk-ticket-form [data-helpdesk-select2][data-wire-model]').forEach((select) => {
const model = select.dataset.wireModel;
const value = values[model] ?? '';
select.value = value === null ? '' : String(value);
select.dispatchEvent(new Event('change', { bubbles: true }));
if (window.jQuery && window.jQuery.fn?.select2) {
window.jQuery(select).val(select.value || null).trigger('change.select2');
}
});
document.querySelectorAll('.helpdesk-ticket-form [data-helpdesk-flatpickr]').forEach((input) => {
const model = input.getAttribute('wire:model') || input.dataset.wireModel || input.id;
const value = values[model] ?? '';
input.value = value || '';
input.dispatchEvent(new Event('change', { bubbles: true }));
if (input._flatpickr) {
input._flatpickr.setDate(value || null, false);
}
});
};
const appendPastedFiles = (event) => {
@@ -400,67 +400,25 @@
</div>
</div>
<div class="border-t border-zinc-700 bg-zinc-900/95 px-6 py-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-end">
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate
class="inline-flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500/70 focus:ring-offset-2 focus:ring-offset-zinc-950">
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
<span>Cancel</span>
</a>
<div class="flex items-center justify-end gap-3 border-t border-zinc-700 bg-zinc-900 px-6 py-4">
<button type="button" wire:click="clearForm"
class="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700">
Clear
</button>
<button type="button" wire:click="clearForm" wire:loading.attr="disabled" wire:target="clearForm"
class="inline-flex items-center justify-center gap-2 rounded-xl bg-amber-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-amber-700 disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-amber-500/70 focus:ring-offset-2 focus:ring-offset-zinc-950">
<i class="fa-solid fa-rotate-left" aria-hidden="true"></i>
<span wire:loading.remove wire:target="clearForm">Reset changes</span>
<span wire:loading wire:target="clearForm">Resetting...</span>
</button>
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate
class="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
Cancel
</a>
<button type="submit"
class="inline-flex items-center justify-center gap-2 rounded-xl bg-green-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-green-500/70 focus:ring-offset-2 focus:ring-offset-zinc-950"
data-dusk="ticket-edit-submit" wire:loading.attr="disabled">
<i class="fa-regular fa-floppy-disk" aria-hidden="true"></i>
<span wire:loading.remove>Save Ticket</span>
<span wire:loading>Saving...</span>
</button>
</div>
<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-edit-submit" wire:loading.attr="disabled">
<i class="fa-regular fa-floppy-disk" aria-hidden="true"></i>
<span wire:loading.remove>Save Ticket</span>
<span wire:loading>Saving...</span>
</button>
</div>
</div>
</form>
<script data-helpdesk-edit-form-cleared-reset>
(() => {
const resetTicketEditControls = (event) => {
const values = event?.detail?.values || event?.values || {};
document.querySelectorAll('.helpdesk-ticket-form [data-helpdesk-select2][data-wire-model]').forEach((select) => {
const model = select.dataset.wireModel;
const value = values[model] ?? '';
select.value = value === null ? '' : String(value);
select.dispatchEvent(new Event('change', { bubbles: true }));
if (window.jQuery && window.jQuery.fn?.select2) {
window.jQuery(select).val(select.value || null).trigger('change.select2');
}
});
document.querySelectorAll('.helpdesk-ticket-form [data-helpdesk-flatpickr]').forEach((input) => {
const model = input.getAttribute('wire:model') || input.dataset.wireModel || input.id;
const value = values[model] ?? '';
input.value = value || '';
input.dispatchEvent(new Event('change', { bubbles: true }));
if (input._flatpickr) {
input._flatpickr.setDate(value || null, false);
}
});
};
document.addEventListener('helpdesk-form-cleared', resetTicketEditControls);
if (window.Livewire?.on) {
window.Livewire.on('helpdesk-form-cleared', resetTicketEditControls);
}
})();
</script>
</div>