Files
mimic/frontend/src/hooks/useTemplates.ts
Knacky 90fc5bab6c feat(frontend): sprint 5 — templates CRUD pages + nav + picker modal + dropdown
- types.ts: SimulationTemplate, SimulationTemplateCreateInput, SimulationTemplatePatchInput,
  extend SimulationCreateInput with template_id
- api/templates.ts: listTemplates, getTemplate, createTemplate, updateTemplate, deleteTemplate
- hooks/useTemplates.ts: useTemplates, useTemplate, useCreateTemplate, useUpdateTemplate,
  useDeleteTemplate (TanStack Query, invalidates ["templates"])
- TemplatesListPage: /admin/templates — table (name, MITRE count, created by, updated),
  New/Edit/Delete actions, loading/error/empty states
- TemplateFormPage: /admin/templates/new + /admin/templates/:id/edit — controlled form
  with inline MITRE field (picker + matrix modal), ConfirmDialog for delete
- TemplatePickerModal: reusable modal listing templates with empty state (AC-27.6)
- SimulationList: replace "New simulation" link with split-button dropdown
  (Blank → /simulations/new | From template… → TemplatePickerModal + POST template_id)
- Layout: "Templates" nav link (admin | redteam, before "Users")
- App.tsx: /admin/templates routes gated roles=["admin","redteam"]
- 26 new Vitest tests (118 total, 92 original preserved)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 06:36:10 +02:00

63 lines
1.7 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
createTemplate,
deleteTemplate,
getTemplate,
listTemplates,
updateTemplate,
} from '@/api/templates';
import type { SimulationTemplateCreateInput, SimulationTemplatePatchInput } from '@/api/types';
function templatesKey() {
return ['templates'] as const;
}
function templateKey(id: number) {
return ['templates', id] as const;
}
export function useTemplates() {
return useQuery({
queryKey: templatesKey(),
queryFn: listTemplates,
});
}
export function useTemplate(id: number | undefined) {
return useQuery({
queryKey: id ? templateKey(id) : ['templates', 'none'],
queryFn: () => getTemplate(id as number),
enabled: typeof id === 'number' && !Number.isNaN(id),
});
}
export function useCreateTemplate() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: SimulationTemplateCreateInput) => createTemplate(input),
onSuccess: () => qc.invalidateQueries({ queryKey: templatesKey() }),
});
}
export function useUpdateTemplate(id: number) {
const qc = useQueryClient();
return useMutation({
mutationFn: (patch: SimulationTemplatePatchInput) => updateTemplate(id, patch),
onSuccess: () => {
qc.invalidateQueries({ queryKey: templateKey(id) });
qc.invalidateQueries({ queryKey: templatesKey() });
},
});
}
export function useDeleteTemplate() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteTemplate(id),
onSuccess: (_data, id) => {
qc.invalidateQueries({ queryKey: templateKey(id) });
qc.invalidateQueries({ queryKey: templatesKey() });
},
});
}