merge: deep-linkable field selection (#72)
CI / web (push) Successful in 6m14s

This commit is contained in:
2026-06-10 13:44:08 +02:00
3 changed files with 51 additions and 9 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ const router = createBrowserRouter(
<Route path="/authorities" element={<Navigate to="/authorities/person" replace />} />
<Route path="/authorities/:kind" element={<AuthoritiesPage />} />
<Route
path="/fields"
path="/fields/:key?"
element={
<Suspense fallback={<ListSkeleton />}>
<FieldsPage />
+15 -7
View File
@@ -1,18 +1,23 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import type { components } from "../api/schema";
import { useFieldDefinitions } from "../api/queries";
import { FieldList } from "./field-list";
import { FieldForm } from "./field-form";
import { useDocumentTitle } from "../lib/use-document-title";
import { useBreadcrumb } from "../shell/use-breadcrumb";
import { PageTitle } from "@/components/ui/page-title";
type FieldDefinitionView = components["schemas"]["FieldDefinitionView"];
export function FieldsPage() {
const { t } = useTranslation();
const [selected, setSelected] = useState<FieldDefinitionView | null>(null);
const navigate = useNavigate();
const { key } = useParams();
const { data } = useFieldDefinitions();
// Selection lives in the URL (/fields/:key) so it survives reload and can be
// shared, matching /vocabularies/:id. An unknown or absent key falls back to
// the create form. Same cached query as FieldList, so no extra fetch.
const selected = (key && data?.find((def) => def.key === key)) || null;
useDocumentTitle(t("fields.title"));
useBreadcrumb([{ label: t("nav.fields") }]);
@@ -22,13 +27,16 @@ export function FieldsPage() {
<PageTitle className="px-4 pt-4 pb-2">{t("fields.title")}</PageTitle>
<div className="grid flex-1 grid-cols-1 overflow-auto lg:grid-cols-[20rem_1fr] lg:overflow-hidden">
<div className="overflow-hidden border-b lg:border-r lg:border-b-0">
<FieldList selectedKey={selected?.key ?? null} onSelect={setSelected} />
<FieldList
selectedKey={selected?.key ?? null}
onSelect={(def) => navigate(`/fields/${encodeURIComponent(def.key)}`)}
/>
</div>
<div className="overflow-hidden">
<FieldForm
key={selected?.key ?? "create"}
editing={selected}
onDone={() => setSelected(null)}
onDone={() => navigate("/fields")}
/>
</div>
</div>
+35 -1
View File
@@ -11,7 +11,7 @@ import { FieldsPage } from "./fields-page";
function tree() {
return (
<Routes>
<Route path="/fields" element={<FieldsPage />} />
<Route path="/fields/:key?" element={<FieldsPage />} />
</Routes>
);
}
@@ -87,6 +87,40 @@ test("filter narrows the visible fields", async () => {
expect(await screen.findByText(/no matches/i)).toBeInTheDocument();
});
test("deep link /fields/:key opens the edit form for that field", async () => {
renderApp(tree(), { route: "/fields/inscription" });
// edit mode: the key input is locked and prefilled from the URL. The form
// remounts when the defs query resolves, so re-query inside waitFor.
await waitFor(() => expect(screen.getByLabelText(/^key$/i)).toHaveValue("inscription"));
expect(screen.getByLabelText(/^key$/i)).toBeDisabled();
expect(screen.getByRole("button", { name: /cancel/i })).toBeInTheDocument();
});
test("selecting a field switches to its edit form; cancel returns to create", async () => {
renderApp(tree(), { route: "/fields" });
await userEvent.click(await screen.findByRole("button", { name: /inscription/i }));
await waitFor(() => expect(screen.getByLabelText(/^key$/i)).toHaveValue("inscription"));
expect(screen.getByLabelText(/^key$/i)).toBeDisabled();
await userEvent.click(screen.getByRole("button", { name: /cancel/i }));
await waitFor(() => expect(screen.getByLabelText(/^key$/i)).toHaveValue(""));
expect(screen.getByLabelText(/^key$/i)).toBeEnabled();
});
test("an unknown key falls back to the create form", async () => {
renderApp(tree(), { route: "/fields/zzz-does-not-exist" });
await screen.findByText("Inscription");
const key = screen.getByLabelText(/^key$/i);
expect(key).toHaveValue("");
expect(key).toBeEnabled();
});
test("creates a text field — posts the body and clears the key input", async () => {
let body: { key: string; data_type: string } | undefined;