From 173f05c1bdc9f29870e7a91ff44854b231398169 Mon Sep 17 00:00:00 2001 From: nexxo Date: Wed, 29 Jul 2026 22:50:16 +0200 Subject: [PATCH] Fix a cancel button that did nothing, and fit the answers on one screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cancel button on the template modal was written with Alpine's $dispatch, which fires a browser DOM event — and wire-elements/modal listens for a LIVEWIRE event of that name. So it did nothing at all, while saving worked, which is the combination that makes somebody click twice and then close the tab. Every other modal in the repo already used Livewire.dispatch; this one was the odd one out, and nothing would have caught it, so there is now a scan that fails on the Alpine form. Narrowed to Alpine handlers deliberately: wire:click="$dispatch('closeModal')" IS Livewire's own dispatcher and works. The list was one column of thirteen entries, each carrying a name, a subject, two clamped lines of body and two full-width buttons — four screens to find the one you want, with the body preview taking most of the height while being the least useful part of it. The whole text is one click away in the modal. Now: two columns of one-line entries — name and subject, which is what tells two similar answers apart — with the actions as icons that do not compete with the name. The create form is folded away behind the button that creates one, because a form that is always open is a screen of form between the operator and the thing they came for. And the placeholder list runs as a grid across the page instead of a tall sidebar that squeezed the entries into half the window. Co-Authored-By: Claude Opus 5 --- VERSION | 2 +- .../admin/edit-mail-template.blade.php | 6 +- .../livewire/admin/mail-templates.blade.php | 231 ++++++++++-------- tests/Feature/EditInModalTest.php | 32 +++ 4 files changed, 167 insertions(+), 104 deletions(-) diff --git a/VERSION b/VERSION index b2be32b..8c6b1d1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.38 +1.3.39 diff --git a/resources/views/livewire/admin/edit-mail-template.blade.php b/resources/views/livewire/admin/edit-mail-template.blade.php index c7cf6b5..2300876 100644 --- a/resources/views/livewire/admin/edit-mail-template.blade.php +++ b/resources/views/livewire/admin/edit-mail-template.blade.php @@ -37,7 +37,11 @@ {{ __('templates.save') }} - {{ __('templates.cancel') }} + {{-- Livewire.dispatch, not Alpine's $dispatch. Alpine's fires a + browser DOM event, and the modal listens for a LIVEWIRE event of + that name — so the button did nothing at all. Every other modal + in this repo does it this way; this one was the odd one out. --}} + {{ __('templates.cancel') }} diff --git a/resources/views/livewire/admin/mail-templates.blade.php b/resources/views/livewire/admin/mail-templates.blade.php index fd29e85..6f59c92 100644 --- a/resources/views/livewire/admin/mail-templates.blade.php +++ b/resources/views/livewire/admin/mail-templates.blade.php @@ -1,110 +1,137 @@ -
-
-

{{ __('templates.title') }}

-

{{ __('templates.subtitle') }}

+{{-- + Thirteen answers on one screen. + + The first version was one column: name, subject, two clamped lines of body + and two full-width buttons per entry. Thirteen of those is four screens of + scrolling to find the one you want, and the body preview was the least + useful part of it — the whole text is one click away in the modal anyway. + + So: two columns of one-line entries, actions as icons that do not compete + with the name, the create form folded away behind a button, and the + placeholder list as a compact grid rather than a tall sidebar that pushed the + entries into half the window. +--}} +
+
+
+

{{ __('templates.title') }}

+

{{ __('templates.subtitle') }}

+
+ + {{ __('templates.new') }} +
-
-
- {{-- The list. Order is the operator's own, because the order they - think in is neither alphabetical nor the order these happened to - be written in. --}} -
-

{{ __('templates.list') }}

- - @if ($templates->isEmpty()) -

{{ __('templates.empty') }}

- @else -
    - @foreach ($templates as $template) -
  • -
    - $template->active, 'text-muted line-through' => ! $template->active])> - {{ $template->name }} - - @if (! $template->active) - {{ __('templates.retired') }} - @endif - - {{-- Two clicks, one value each, no height - change: R20's exception. --}} - - - -
    - -

    {{ $template->subject }}

    -

    {{ $template->body }}

    - -
    - {{-- R20: editing opens a modal. A five-line - textarea in a list row is the height jump - the rule exists to stop. --}} - - {{ __('templates.edit') }} - - - {{ $template->active ? __('templates.retire') : __('templates.restore') }} - -
    -
  • - @endforeach -
- @endif -
- - {{-- Creating. A form that IS the page rather than a modal — R20 - exempts exactly this, because nothing is being edited in place. --}} -
-

{{ __('templates.new') }}

- -
- - -
- -
- - - @error('body')

{{ $message }}

@enderror -
- - - {{ __('templates.create') }} - -
+ {{-- Folded away by default. A form that is always open is a screen of form + between the operator and the thing they came for — and creating a + template is the rarer of the two things done here. It stays a form that + IS the page (R20 exempts exactly that); only its visibility changed. --}} +
+
+ +
- {{-- The placeholders, rendered from the renderer's own list so a token - added there appears here without anybody writing it down twice. --}} -
+
+ + + @error('body')

{{ $message }}

@enderror +
+ +
+ + {{ __('templates.create') }} + + {{ __('templates.cancel') }} +
+ + + {{-- The placeholders, as a grid across the page rather than a column beside + it. Rendered from the renderer's own list, so a token added there + appears here without being written down twice. --}} +
+

{{ __('templates.placeholders') }}

-

{{ __('templates.placeholders_note') }}

- -
- @foreach ($placeholders as $token => $explanation) - {{-- Assembled in PHP, not inline: Blade's echo tag ends at - the first }} it finds, so a literal one inside the - expression cuts the tag in half and compiles to - nonsense. Caught by the test that renders this page. --}} - @php $literal = '{{'.$token.'}}'; @endphp -
-
{{ $literal }}
-
{{ __($explanation) }}
-
- @endforeach -
- -

{{ __('templates.unknown_note') }}

+

{{ __('templates.placeholders_note') }}

+ +
+ @foreach ($placeholders as $token => $explanation) + {{-- Assembled in PHP, not inline: Blade's echo tag ends at the + first }} it finds, so a literal one inside the expression + cuts the tag in half and compiles to nonsense. --}} + @php $literal = '{{'.$token.'}}'; @endphp +
+
{{ $literal }}
+
{{ __($explanation) }}
+
+ @endforeach +
+ +

{{ __('templates.unknown_note') }}

+ + {{-- The entries. Two columns, one line each: what an operator scans for is + the name, and the subject is the only other thing that tells two similar + ones apart. The text itself is in the modal. --}} + @if ($templates->isEmpty()) +
+

{{ __('templates.empty') }}

+
+ @else +
+ @foreach ($templates as $template) +
$template->active, + // A retired one stays visible and stays quiet. + 'border-line bg-surface-2' => ! $template->active, + ])> +
+
+ $template->active, + 'text-muted line-through' => ! $template->active, + ])>{{ $template->name }} + @if (! $template->active) + {{ __('templates.retired') }} + @endif +
+

{{ $template->subject }}

+
+ + {{-- Icons, and one word for the state change. Two full-width + buttons per entry was most of the height. --}} +
+ + + + {{-- R20: editing opens a modal. A five-line textarea in a + list row is the height jump the rule exists to stop. --}} + +
+
+ @endforeach +
+ @endif
diff --git a/tests/Feature/EditInModalTest.php b/tests/Feature/EditInModalTest.php index 2b7187a..708db70 100644 --- a/tests/Feature/EditInModalTest.php +++ b/tests/Feature/EditInModalTest.php @@ -76,3 +76,35 @@ it('reaches every edit modal through openModal', function () { expect($users)->toContain("component: 'edit-seat'"); }); + +it('closes every modal through the event the modal actually listens for', function () { + // Alpine's $dispatch fires a browser DOM event; wire-elements/modal listens + // for a LIVEWIRE event of that name. A cancel button written the Alpine way + // therefore does nothing at all — reported on the mail-template modal, where + // saving worked and cancelling did not. Every other modal in the repo was + // already correct, which is exactly why nobody noticed the odd one out. + $offenders = collect(File::allFiles(resource_path('views'))) + ->filter(fn ($file) => str_ends_with($file->getFilename(), '.blade.php')) + ->filter(function ($file) { + // Blade comments stripped first: this file's own explanation of the + // wrong form must not read as an offence. + $body = preg_replace('/\{\{--.*?--\}\}/s', '', $file->getContents()); + + // ALPINE's $dispatch only. wire:click="$dispatch('closeModal')" is + // Livewire's own dispatcher and works — the broken form is the one + // bound through an Alpine handler, where $dispatch means the DOM. + return preg_match( + '/(?:x-on:click|@click)\s*=\s*"[^"]*\$dispatch\(\s*[\'"]closeModal/', + (string) $body, + ) === 1; + }) + ->map(fn ($file) => $file->getRelativePathname()) + ->values() + ->all(); + + expect($offenders)->toBe([]); + + // And the working form is the one in use. + expect(File::get(resource_path('views/livewire/admin/edit-mail-template.blade.php'))) + ->toContain("Livewire.dispatch('closeModal')"); +});