authorize('customers.manage'); } public function create(): void { $this->authorize('customers.manage'); $data = $this->validate(); MailTemplate::create([ 'name' => trim($data['name']), 'subject' => trim($data['subject']), 'body' => $data['body'], // At the end of the list, where a new one belongs until somebody // decides otherwise. 'sort' => (int) MailTemplate::query()->max('sort') + 10, 'active' => true, ]); $this->reset(['name', 'subject', 'body']); $this->dispatch('notify', message: __('templates.created')); } /** * Retire one, or bring it back. * * Not deleted: an answer that is no longer given is still the answer * somebody was given last year, and the register of sent mail refers to it. */ public function toggleActive(string $uuid): void { $this->authorize('customers.manage'); $template = MailTemplate::query()->where('uuid', $uuid)->first(); if ($template !== null) { $template->update(['active' => ! $template->active]); } } /** * Swap one with its neighbour. * * It used to add or subtract a fixed amount, which is not the same thing: * with the seeded values ten apart, ±15 usually landed correctly — and * sometimes landed on a value another template already had, or on the 0 * floor along with three others. Equal `sort` values fall back to the name, * so a click could reorder something else, or visibly nothing, which is * indistinguishable from a click that never registered. * * A swap always moves exactly one place, or does nothing at the ends, and * both of those are what the operator expects to see. */ public function move(string $uuid, int $direction): void { $this->authorize('customers.manage'); $template = MailTemplate::query()->where('uuid', $uuid)->first(); if ($template === null) { return; } // The one immediately above or below in the order the page shows — // read through the same ordering, so the swap agrees with the list. $neighbour = MailTemplate::query() ->when($direction < 0, fn ($q) => $q ->where(fn ($w) => $w->where('sort', '<', $template->sort) ->orWhere(fn ($t) => $t->where('sort', $template->sort)->where('name', '<', $template->name))) ->orderByDesc('sort')->orderByDesc('name')) ->when($direction > 0, fn ($q) => $q ->where(fn ($w) => $w->where('sort', '>', $template->sort) ->orWhere(fn ($t) => $t->where('sort', $template->sort)->where('name', '>', $template->name))) ->orderBy('sort')->orderBy('name')) ->first(); if ($neighbour === null) { return; // already at the end; nothing to swap with } // Distinct values, even where the two were equal before: leaving them // equal would put the order back in the hands of the name. $mine = $template->sort; $theirs = $neighbour->sort; if ($mine === $theirs) { // Moving UP means taking the lower value, so the one the mover // receives is below the neighbour's — not above it. The other way // round swaps them into the order they were already in, which looks // exactly like the bug this method was rewritten to fix. $theirs = $mine + ($direction < 0 ? -1 : 1); } $template->update(['sort' => $theirs]); $neighbour->update(['sort' => $mine]); } public function render() { $this->authorize('customers.manage'); return view('livewire.admin.mail-templates', [ 'templates' => MailTemplate::query()->orderBy('sort')->orderBy('name')->get(), // The list IS the documentation: a token added to the renderer // appears here without anybody remembering to write it down twice. 'placeholders' => MailTemplateRenderer::PLACEHOLDERS, ]); } }