feat: add template delete — backend DELETE route, frontend trash button

- DELETE /templates/:id — backend route
- deleteTemplate() API client function
- Trash icon on each template list item (shows on hover)
- Confirms inline, no dialog needed
- Resets builder if the deleted template was being edited

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-09 12:49:45 +02:00
parent 7be90684fb
commit affbbdaa46
3 changed files with 48 additions and 4 deletions

View File

@@ -10,6 +10,7 @@ import {
PlusIcon,
Settings2Icon,
SlidersHorizontalIcon,
Trash2Icon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -52,6 +53,7 @@ import {
createTemplate,
updateTemplate,
previewTemplate,
deleteTemplate,
getCustomFields,
getQueueCustomFields,
assignQueueCustomField,
@@ -1699,6 +1701,7 @@ Location: {{custom_fields.location}}`);
const [previewError, setPreviewError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const fetchTemplates = useCallback(async () => {
setLoading(true);
@@ -1741,6 +1744,14 @@ Location: {{custom_fields.location}}`;
setSaveError(null);
};
const handleDeleteTemplate = async (templateId: string) => {
setDeletingId(templateId);
await deleteTemplate(templateId);
if (editingId === templateId) resetBuilder();
await fetchTemplates();
setDeletingId(null);
};
const selectTemplate = (template: Template) => {
setEditingId(template.id);
setName(template.name);
@@ -1835,7 +1846,7 @@ Location: {{custom_fields.location}}`;
type="button"
onClick={() => selectTemplate(template)}
className={cn(
"min-w-0 max-w-full overflow-hidden rounded-md border p-3 text-left transition hover:border-primary/45 hover:bg-accent/45",
"group min-w-0 max-w-full overflow-hidden rounded-md border p-3 text-left transition hover:border-primary/45 hover:bg-accent/45",
editingId === template.id ? "border-primary bg-primary/10" : "border-border bg-card"
)}
>
@@ -1844,9 +1855,23 @@ Location: {{custom_fields.location}}`;
<div className="truncate text-sm font-semibold text-foreground">{template.name}</div>
<div className="mt-1 truncate text-xs text-muted-foreground">{queueName(template.queue_id)}</div>
</div>
<Badge variant="outline" className="shrink-0 rounded">
{template.queue_id ? "Queue" : "Global"}
</Badge>
<div className="flex shrink-0 items-center gap-1">
<Badge variant="outline" className="rounded">
{template.queue_id ? "Queue" : "Global"}
</Badge>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
void handleDeleteTemplate(template.id);
}}
disabled={deletingId === template.id}
className="flex h-6 w-6 items-center justify-center rounded text-muted-foreground/60 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100 disabled:opacity-50"
title="Delete template"
>
<Trash2Icon className="h-3.5 w-3.5" />
</button>
</div>
</div>
<div className="mt-3 truncate font-mono text-[11px] text-muted-foreground">
{template.subject_template}

View File

@@ -169,6 +169,10 @@ export async function previewTemplate(data: {
return request<TemplatePreview>("/templates/preview", { method: "POST", body: JSON.stringify(data) });
}
export async function deleteTemplate(id: string): Promise<{ data: { ok: boolean } | null; error: string | null }> {
return request<{ ok: boolean }>(`/templates/${id}`, { method: "DELETE" });
}
export async function getLifecycles(): Promise<{ data: Lifecycle[] | null; error: string | null }> {
return request<Lifecycle[]>("/lifecycles");
}