diff --git a/web/src/authorities/authorities-page.tsx b/web/src/authorities/authorities-page.tsx index 19bd9f8..37c2b5f 100644 --- a/web/src/authorities/authorities-page.tsx +++ b/web/src/authorities/authorities-page.tsx @@ -6,9 +6,13 @@ import type { components } from "../api/schema"; import { useAuthorities, useCreateAuthority } from "../api/queries"; import { LabelEditor } from "../components/label-editor"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { PageTitle } from "@/components/ui/page-title"; import { ListSkeleton } from "@/components/ui/skeletons"; import { AuthorityRow } from "./authority-row"; +import { byLabel } from "../lib/sort"; +import { labelText } from "../lib/labels"; import { useDocumentTitle } from "../lib/use-document-title"; import { useBreadcrumb } from "../shell/use-breadcrumb"; @@ -29,6 +33,8 @@ export function AuthoritiesPage() { const [labels, setLabels] = useState([]); const [error, setError] = useState(false); + const [filter, setFilter] = useState(""); + const [uri, setUri] = useState(""); useDocumentTitle(t("nav.authorities")); useBreadcrumb([{ label: t("nav.authorities") }]); @@ -45,8 +51,13 @@ export function AuthoritiesPage() { setError(false); create.mutate( - { kind: kind as string, external_uri: null, labels }, - { onSuccess: () => setLabels([]) }, + { kind: kind as string, external_uri: uri.trim() || null, labels }, + { + onSuccess: () => { + setLabels([]); + setUri(""); + }, + }, ); }; @@ -69,20 +80,41 @@ export function AuthoritiesPage() { ))} +
+ setFilter(e.target.value)} + /> +
+ {isLoading ? ( ) : ( - + (() => { + const q = filter.trim().toLowerCase(); + const rows = [...(authorities ?? [])] + .filter((a) => !q || labelText(a.labels, lang).toLowerCase().includes(q)) + .sort(byLabel(lang)); + + return ( + + ); + })() )}
@@ -92,6 +124,17 @@ export function AuthoritiesPage() { +
+ + setUri(e.target.value)} + /> +
+ {error && (

{t("form.required")} diff --git a/web/src/authorities/authorities.test.tsx b/web/src/authorities/authorities.test.tsx index fc2ffb8..d718752 100644 --- a/web/src/authorities/authorities.test.tsx +++ b/web/src/authorities/authorities.test.tsx @@ -69,3 +69,69 @@ test("unknown kind redirects to person list", async () => { renderApp(tree(), { route: "/authorities/banana" }); expect(await screen.findByText("Ada Lovelace")).toBeInTheDocument(); }); + +test("authorities render sorted by label", async () => { + server.use( + http.get("/api/admin/authorities", () => + HttpResponse.json([ + { id: "a-zoe", kind: "person", external_uri: null, labels: [{ lang: "en", label: "Zoe" }] }, + { id: "a-adam", kind: "person", external_uri: null, labels: [{ lang: "en", label: "Adam" }] }, + ]), + ), + ); + renderApp(tree(), { route: "/authorities/person" }); + expect(await screen.findByText("Adam")).toBeInTheDocument(); + const items = screen.getAllByRole("listitem"); + const texts = items.map((item) => item.textContent ?? ""); + const adam = texts.findIndex((text) => text.includes("Adam")); + const zoe = texts.findIndex((text) => text.includes("Zoe")); + expect(adam).toBeLessThan(zoe); +}); + +test("filter narrows the authority list", async () => { + server.use( + http.get("/api/admin/authorities", () => + HttpResponse.json([ + { id: "a-ada", kind: "person", external_uri: null, labels: [{ lang: "en", label: "Ada Lovelace" }] }, + { id: "a-grace", kind: "person", external_uri: null, labels: [{ lang: "en", label: "Grace Hopper" }] }, + ]), + ), + ); + renderApp(tree(), { route: "/authorities/person" }); + expect(await screen.findByText("Ada Lovelace")).toBeInTheDocument(); + expect(screen.getByText("Grace Hopper")).toBeInTheDocument(); + await userEvent.type(screen.getByRole("textbox", { name: /filter/i }), "grace"); + expect(screen.getByText("Grace Hopper")).toBeInTheDocument(); + expect(screen.queryByText("Ada Lovelace")).not.toBeInTheDocument(); +}); + +test("create posts the entered external_uri", async () => { + let body: unknown; + server.use( + http.post("/api/admin/authorities", async ({ request }) => { + body = await request.json(); + return HttpResponse.json({ id: "a-c" }, { status: 201 }); + }), + ); + renderApp(tree(), { route: "/authorities/person" }); + expect(await screen.findByText("Ada Lovelace")).toBeInTheDocument(); + await userEvent.type(screen.getByLabelText(/^label$/i), "Carl von Linné"); + await userEvent.type(screen.getByLabelText(/external uri/i), "https://viaf.org/456"); + await userEvent.click(screen.getByRole("button", { name: /create/i })); + await waitFor(() => + expect((body as { external_uri: string })?.external_uri).toBe("https://viaf.org/456"), + ); +}); + +test("read row shows its external_uri as a link", async () => { + server.use( + http.get("/api/admin/authorities", () => + HttpResponse.json([ + { id: "a-ada", kind: "person", external_uri: "https://viaf.org/123", labels: [{ lang: "en", label: "Ada Lovelace" }] }, + ]), + ), + ); + renderApp(tree(), { route: "/authorities/person" }); + expect(await screen.findByText("Ada Lovelace")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: /viaf\.org/ })).toBeInTheDocument(); +}); diff --git a/web/src/authorities/authority-row.tsx b/web/src/authorities/authority-row.tsx index fe06c1b..a9a5f60 100644 --- a/web/src/authorities/authority-row.tsx +++ b/web/src/authorities/authority-row.tsx @@ -5,6 +5,7 @@ import type { components } from "../api/schema"; import { useUpdateAuthority, useDeleteAuthority } from "../api/queries"; import { LabelEditor } from "../components/label-editor"; import { DeleteConfirmDialog } from "../components/delete-confirm-dialog"; +import { ExternalUriLink } from "../components/external-uri-link"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -29,7 +30,13 @@ export function AuthorityRow({ authority, kind, lang }: { authority: AuthorityVi

- setUri(e.target.value)} /> + setUri(e.target.value)} + />