feat(web): vocabulary/term/authority list+create hooks + MSW handlers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -200,6 +200,87 @@ export function useDeleteObject() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NewVocabularyRequest = components["schemas"]["NewVocabularyRequest"];
|
||||||
|
type LabelInput = components["schemas"]["LabelInput"];
|
||||||
|
|
||||||
|
export function useVocabularies() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["vocabularies"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data, error } = await api.GET("/api/admin/vocabularies");
|
||||||
|
|
||||||
|
if (error || !data) throw new Error("failed to load vocabularies");
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateVocabulary() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (body: NewVocabularyRequest) => {
|
||||||
|
const { data, error } = await api.POST("/api/admin/vocabularies", { body });
|
||||||
|
|
||||||
|
if (error || !data) throw new Error("create vocabulary failed");
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["vocabularies"] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAddTerm() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
vocabularyId,
|
||||||
|
external_uri,
|
||||||
|
labels,
|
||||||
|
}: {
|
||||||
|
vocabularyId: string;
|
||||||
|
external_uri: string | null;
|
||||||
|
labels: LabelInput[];
|
||||||
|
}) => {
|
||||||
|
const { response } = await api.POST("/api/admin/vocabularies/{id}/terms", {
|
||||||
|
params: { path: { id: vocabularyId } },
|
||||||
|
body: { external_uri, labels },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 201) throw new Error("add term failed");
|
||||||
|
},
|
||||||
|
onSuccess: (_result, { vocabularyId }) =>
|
||||||
|
qc.invalidateQueries({ queryKey: ["terms", vocabularyId] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateAuthority() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
kind,
|
||||||
|
external_uri,
|
||||||
|
labels,
|
||||||
|
}: {
|
||||||
|
kind: string;
|
||||||
|
external_uri: string | null;
|
||||||
|
labels: LabelInput[];
|
||||||
|
}) => {
|
||||||
|
const { response } = await api.POST("/api/admin/authorities", {
|
||||||
|
body: { kind, external_uri, labels },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 201) throw new Error("create authority failed");
|
||||||
|
},
|
||||||
|
onSuccess: (_result, { kind }) =>
|
||||||
|
qc.invalidateQueries({ queryKey: ["authorities", kind] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type Visibility = "draft" | "internal" | "public";
|
type Visibility = "draft" | "internal" | "public";
|
||||||
|
|
||||||
/** Error carrying the HTTP status so callers can branch 422-gate vs 409-illegal. */
|
/** Error carrying the HTTP status so callers can branch 422-gate vs 409-illegal. */
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { renderHook, waitFor } from "@testing-library/react";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { http, HttpResponse } from "msw";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { server } from "../test/server";
|
||||||
|
import { useVocabularies, useCreateVocabulary, useAddTerm, useCreateAuthority } from "./queries";
|
||||||
|
|
||||||
|
function wrapper({ children }: { children: ReactNode }) {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("vocab/authority hooks", () => {
|
||||||
|
test("useVocabularies lists vocabularies", async () => {
|
||||||
|
const { result } = renderHook(() => useVocabularies(), { wrapper });
|
||||||
|
await waitFor(() => expect(result.current.data?.length).toBe(2));
|
||||||
|
expect(result.current.data?.[0].key).toBe("material");
|
||||||
|
});
|
||||||
|
test("useCreateVocabulary POSTs the key", async () => {
|
||||||
|
let body: unknown;
|
||||||
|
server.use(http.post("/api/admin/vocabularies", async ({ request }) => {
|
||||||
|
body = await request.json();
|
||||||
|
return HttpResponse.json({ id: "v-x", key: "colour" }, { status: 201 });
|
||||||
|
}));
|
||||||
|
const { result } = renderHook(() => useCreateVocabulary(), { wrapper });
|
||||||
|
await result.current.mutateAsync({ key: "colour" });
|
||||||
|
expect((body as { key: string }).key).toBe("colour");
|
||||||
|
});
|
||||||
|
test("useAddTerm POSTs labels to the vocabulary", async () => {
|
||||||
|
let body: unknown;
|
||||||
|
server.use(http.post("/api/admin/vocabularies/:id/terms", async ({ request }) => {
|
||||||
|
body = await request.json();
|
||||||
|
return HttpResponse.json({ id: "t-x" }, { status: 201 });
|
||||||
|
}));
|
||||||
|
const { result } = renderHook(() => useAddTerm(), { wrapper });
|
||||||
|
await result.current.mutateAsync({ vocabularyId: "v-material", external_uri: null, labels: [{ lang: "en", label: "Red" }] });
|
||||||
|
expect((body as { labels: { label: string }[] }).labels[0].label).toBe("Red");
|
||||||
|
});
|
||||||
|
test("useCreateAuthority POSTs kind + labels", async () => {
|
||||||
|
let body: unknown;
|
||||||
|
server.use(http.post("/api/admin/authorities", async ({ request }) => {
|
||||||
|
body = await request.json();
|
||||||
|
return HttpResponse.json({ id: "a-x" }, { status: 201 });
|
||||||
|
}));
|
||||||
|
const { result } = renderHook(() => useCreateAuthority(), { wrapper });
|
||||||
|
await result.current.mutateAsync({ kind: "person", external_uri: null, labels: [{ lang: "en", label: "Ada" }] });
|
||||||
|
expect((body as { kind: string }).kind).toBe("person");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -62,3 +62,10 @@ export const materialTerms: TermView[] = [
|
|||||||
export const personAuthorities: AuthorityView[] = [
|
export const personAuthorities: AuthorityView[] = [
|
||||||
{ id: "a-ada", kind: "person", external_uri: null, labels: [{ lang: "en", label: "Ada Lovelace" }] },
|
{ id: "a-ada", kind: "person", external_uri: null, labels: [{ lang: "en", label: "Ada Lovelace" }] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export type VocabularyView = components["schemas"]["VocabularyView"];
|
||||||
|
|
||||||
|
export const vocabularies: VocabularyView[] = [
|
||||||
|
{ id: "v-material", key: "material" },
|
||||||
|
{ id: "v-technique", key: "technique" },
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { http, HttpResponse } from "msw";
|
import { http, HttpResponse } from "msw";
|
||||||
|
|
||||||
import { amphora, fibula, fieldDefinitions, materialTerms, objectsPage, personAuthorities } from "./fixtures";
|
import { amphora, fibula, fieldDefinitions, materialTerms, objectsPage, personAuthorities, vocabularies } from "./fixtures";
|
||||||
|
|
||||||
export const handlers = [
|
export const handlers = [
|
||||||
http.get("/api/admin/me", () =>
|
http.get("/api/admin/me", () =>
|
||||||
@@ -17,6 +17,8 @@ export const handlers = [
|
|||||||
|
|
||||||
http.get("/api/admin/field-definitions", () => HttpResponse.json(fieldDefinitions)),
|
http.get("/api/admin/field-definitions", () => HttpResponse.json(fieldDefinitions)),
|
||||||
|
|
||||||
|
http.get("/api/admin/vocabularies", () => HttpResponse.json(vocabularies)),
|
||||||
|
|
||||||
http.get("/api/admin/vocabularies/:id/terms", () => HttpResponse.json(materialTerms)),
|
http.get("/api/admin/vocabularies/:id/terms", () => HttpResponse.json(materialTerms)),
|
||||||
|
|
||||||
http.get("/api/admin/authorities", ({ request }) => {
|
http.get("/api/admin/authorities", ({ request }) => {
|
||||||
@@ -25,6 +27,18 @@ export const handlers = [
|
|||||||
return HttpResponse.json(kind === "person" ? personAuthorities : []);
|
return HttpResponse.json(kind === "person" ? personAuthorities : []);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
http.post("/api/admin/vocabularies", () =>
|
||||||
|
HttpResponse.json({ id: "v-new", key: "new" }, { status: 201 }),
|
||||||
|
),
|
||||||
|
|
||||||
|
http.post("/api/admin/vocabularies/:id/terms", () =>
|
||||||
|
HttpResponse.json({ id: "t-new" }, { status: 201 }),
|
||||||
|
),
|
||||||
|
|
||||||
|
http.post("/api/admin/authorities", () =>
|
||||||
|
HttpResponse.json({ id: "a-new" }, { status: 201 }),
|
||||||
|
),
|
||||||
|
|
||||||
http.post("/api/admin/objects", () =>
|
http.post("/api/admin/objects", () =>
|
||||||
HttpResponse.json({ id: "11111111-1111-1111-1111-111111111111" }, { status: 201 }),
|
HttpResponse.json({ id: "11111111-1111-1111-1111-111111111111" }, { status: 201 }),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user