feat(web): authoring query/mutation hooks + MSW handlers + shadcn select/checkbox/alert-dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 00:22:59 +02:00
parent f3bab3336c
commit b23a48c310
9 changed files with 647 additions and 13 deletions
+67
View File
@@ -0,0 +1,67 @@
import { describe, expect, test } from "vitest";
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
import {
useAuthorities,
useCreateObject,
useDeleteObject,
useSetFields,
useTerms,
useUpdateObject,
} from "./queries";
function wrapper({ children }: { children: ReactNode }) {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
}
describe("authoring hooks", () => {
test("useTerms loads a vocabulary's terms", async () => {
const { result } = renderHook(() => useTerms("v-material"), { wrapper });
await waitFor(() => expect(result.current.data).toBeDefined());
expect(result.current.data?.[0].id).toBe("t-bronze");
});
test("useAuthorities loads by kind", async () => {
const { result } = renderHook(() => useAuthorities("person"), { wrapper });
await waitFor(() => expect(result.current.data?.length).toBe(1));
expect(result.current.data?.[0].id).toBe("a-ada");
});
test("useCreateObject returns the new id", async () => {
const { result } = renderHook(() => useCreateObject(), { wrapper });
const created = await result.current.mutateAsync({
object_number: "A-1",
object_name: "x",
number_of_objects: 1,
visibility: "draft",
});
expect(created.id).toBe("11111111-1111-1111-1111-111111111111");
});
test("useSetFields / useUpdateObject / useDeleteObject resolve", async () => {
const setFields = renderHook(() => useSetFields(), { wrapper });
await setFields.result.current.mutateAsync({ id: "o1", fields: { inscription: "hi" } });
const update = renderHook(() => useUpdateObject(), { wrapper });
await update.result.current.mutateAsync({
id: "o1",
body: { object_number: "A-1", object_name: "x", number_of_objects: 1 },
});
const del = renderHook(() => useDeleteObject(), { wrapper });
await del.result.current.mutateAsync("o1");
expect(true).toBe(true);
});
});
+104
View File
@@ -95,3 +95,107 @@ export function useLogout() {
onSuccess: () => qc.setQueryData(["me"], null),
});
}
type ObjectCreateRequest = components["schemas"]["ObjectCreateRequest"];
type ObjectUpdateRequest = components["schemas"]["ObjectUpdateRequest"];
export function useTerms(vocabularyId: string | null | undefined) {
return useQuery({
queryKey: ["terms", vocabularyId],
enabled: !!vocabularyId,
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/vocabularies/{id}/terms", {
params: { path: { id: vocabularyId! } },
});
if (error || !data) throw new Error("failed to load terms");
return data;
},
staleTime: 5 * 60 * 1000,
});
}
export function useAuthorities(kind: string | null | undefined) {
return useQuery({
queryKey: ["authorities", kind],
enabled: !!kind,
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/authorities", {
params: { query: { kind: kind! } },
});
if (error || !data) throw new Error("failed to load authorities");
return data;
},
staleTime: 5 * 60 * 1000,
});
}
export function useCreateObject() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: ObjectCreateRequest) => {
const { data, error } = await api.POST("/api/admin/objects", { body });
if (error || !data) throw new Error("create failed");
return data;
},
onSuccess: () => qc.invalidateQueries({ queryKey: ["objects"] }),
});
}
export function useUpdateObject() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id, body }: { id: string; body: ObjectUpdateRequest }) => {
const { response } = await api.PUT("/api/admin/objects/{id}", {
params: { path: { id } },
body,
});
if (response.status !== 204) throw new Error("update failed");
},
onSuccess: (_d, { id }) => {
void qc.invalidateQueries({ queryKey: ["objects"] });
void qc.invalidateQueries({ queryKey: ["object", id] });
},
});
}
export function useSetFields() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id, fields }: { id: string; fields: Record<string, unknown> }) => {
const { response } = await api.PUT("/api/admin/objects/{id}/fields", {
params: { path: { id } },
body: fields as Record<string, never>,
});
if (response.status !== 204) throw new Error("set fields failed");
},
onSuccess: (_d, { id }) => {
void qc.invalidateQueries({ queryKey: ["object", id] });
},
});
}
export function useDeleteObject() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { response } = await api.DELETE("/api/admin/objects/{id}", {
params: { path: { id } },
});
if (response.status !== 204) throw new Error("delete failed");
},
onSuccess: () => qc.invalidateQueries({ queryKey: ["objects"] }),
});
}