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>
This commit is contained in:
62
frontend/src/hooks/useTemplates.ts
Normal file
62
frontend/src/hooks/useTemplates.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
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() });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user