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:
@@ -151,6 +151,21 @@ export function createTemplatesRouter(db: Db): Hono {
|
|||||||
return c.json(updated);
|
return c.json(updated);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', async (c) => {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
|
||||||
|
const existing = await db.query.templates.findFirst({
|
||||||
|
where: eq(templates.id, id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
throw new HTTPException(404, { message: 'Template not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.delete(templates).where(eq(templates.id, id));
|
||||||
|
return c.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
router.post('/preview', async (c) => {
|
router.post('/preview', async (c) => {
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const subjectTemplate = String(body.subject_template ?? '');
|
const subjectTemplate = String(body.subject_template ?? '');
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
Settings2Icon,
|
Settings2Icon,
|
||||||
SlidersHorizontalIcon,
|
SlidersHorizontalIcon,
|
||||||
|
Trash2Icon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -52,6 +53,7 @@ import {
|
|||||||
createTemplate,
|
createTemplate,
|
||||||
updateTemplate,
|
updateTemplate,
|
||||||
previewTemplate,
|
previewTemplate,
|
||||||
|
deleteTemplate,
|
||||||
getCustomFields,
|
getCustomFields,
|
||||||
getQueueCustomFields,
|
getQueueCustomFields,
|
||||||
assignQueueCustomField,
|
assignQueueCustomField,
|
||||||
@@ -1699,6 +1701,7 @@ Location: {{custom_fields.location}}`);
|
|||||||
const [previewError, setPreviewError] = useState<string | null>(null);
|
const [previewError, setPreviewError] = useState<string | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchTemplates = useCallback(async () => {
|
const fetchTemplates = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -1741,6 +1744,14 @@ Location: {{custom_fields.location}}`;
|
|||||||
setSaveError(null);
|
setSaveError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteTemplate = async (templateId: string) => {
|
||||||
|
setDeletingId(templateId);
|
||||||
|
await deleteTemplate(templateId);
|
||||||
|
if (editingId === templateId) resetBuilder();
|
||||||
|
await fetchTemplates();
|
||||||
|
setDeletingId(null);
|
||||||
|
};
|
||||||
|
|
||||||
const selectTemplate = (template: Template) => {
|
const selectTemplate = (template: Template) => {
|
||||||
setEditingId(template.id);
|
setEditingId(template.id);
|
||||||
setName(template.name);
|
setName(template.name);
|
||||||
@@ -1835,7 +1846,7 @@ Location: {{custom_fields.location}}`;
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => selectTemplate(template)}
|
onClick={() => selectTemplate(template)}
|
||||||
className={cn(
|
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"
|
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="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 className="mt-1 truncate text-xs text-muted-foreground">{queueName(template.queue_id)}</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className="shrink-0 rounded">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
|
<Badge variant="outline" className="rounded">
|
||||||
{template.queue_id ? "Queue" : "Global"}
|
{template.queue_id ? "Queue" : "Global"}
|
||||||
</Badge>
|
</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>
|
||||||
<div className="mt-3 truncate font-mono text-[11px] text-muted-foreground">
|
<div className="mt-3 truncate font-mono text-[11px] text-muted-foreground">
|
||||||
{template.subject_template}
|
{template.subject_template}
|
||||||
|
|||||||
@@ -169,6 +169,10 @@ export async function previewTemplate(data: {
|
|||||||
return request<TemplatePreview>("/templates/preview", { method: "POST", body: JSON.stringify(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 }> {
|
export async function getLifecycles(): Promise<{ data: Lifecycle[] | null; error: string | null }> {
|
||||||
return request<Lifecycle[]>("/lifecycles");
|
return request<Lifecycle[]>("/lifecycles");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user