diff --git a/web/src/app.tsx b/web/src/app.tsx index 5f227b1..388e12d 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -72,7 +72,7 @@ const router = createBrowserRouter( } /> } /> }> diff --git a/web/src/fields/fields-page.tsx b/web/src/fields/fields-page.tsx index 3da3f6c..1f62de7 100644 --- a/web/src/fields/fields-page.tsx +++ b/web/src/fields/fields-page.tsx @@ -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(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() { {t("fields.title")}
- + navigate(`/fields/${encodeURIComponent(def.key)}`)} + />
setSelected(null)} + onDone={() => navigate("/fields")} />
diff --git a/web/src/fields/fields.test.tsx b/web/src/fields/fields.test.tsx index 1b399e4..6ee065d 100644 --- a/web/src/fields/fields.test.tsx +++ b/web/src/fields/fields.test.tsx @@ -11,7 +11,7 @@ import { FieldsPage } from "./fields-page"; function tree() { return ( - } /> + } /> ); } @@ -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;