diff --git a/web/src/authorities/authorities-page.tsx b/web/src/authorities/authorities-page.tsx index ec90e7f..ad821ac 100644 --- a/web/src/authorities/authorities-page.tsx +++ b/web/src/authorities/authorities-page.tsx @@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button"; import { PageTitle } from "@/components/ui/page-title"; import { AuthorityRow } from "./authority-row"; import { useDocumentTitle } from "../lib/use-document-title"; +import { useBreadcrumb } from "../shell/use-breadcrumb"; type LabelInput = components["schemas"]["LabelInput"]; @@ -29,6 +30,7 @@ export function AuthoritiesPage() { const [error, setError] = useState(false); useDocumentTitle(t("nav.authorities")); + useBreadcrumb([{ label: t("nav.authorities") }]); if (!isValidKind) return ; diff --git a/web/src/fields/fields-page.tsx b/web/src/fields/fields-page.tsx index 3027317..47adb86 100644 --- a/web/src/fields/fields-page.tsx +++ b/web/src/fields/fields-page.tsx @@ -5,6 +5,7 @@ import type { components } from "../api/schema"; 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"]; @@ -14,6 +15,7 @@ export function FieldsPage() { const [selected, setSelected] = useState(null); useDocumentTitle(t("fields.title")); + useBreadcrumb([{ label: t("nav.fields") }]); return (
diff --git a/web/src/objects/object-detail.tsx b/web/src/objects/object-detail.tsx index fc6e2ac..326d48f 100644 --- a/web/src/objects/object-detail.tsx +++ b/web/src/objects/object-detail.tsx @@ -6,6 +6,7 @@ import type { components } from "../api/schema"; import { useObject, useFieldDefinitions } from "../api/queries"; import { formatDate } from "../lib/format-date"; import { useDocumentTitle } from "../lib/use-document-title"; +import { useBreadcrumb } from "../shell/use-breadcrumb"; import { DeleteObjectDialog } from "./delete-object-dialog"; import { FlexibleFieldValue } from "./flexible-field-value"; import { PublishControl } from "./publish-control"; @@ -52,6 +53,7 @@ function ObjectDetailLoaded({ object }: { object: AdminObjectView }) { const { data: definitions } = useFieldDefinitions(); useDocumentTitle(object.object_number); + useBreadcrumb([{ label: t("nav.objects"), to: "/objects" }, { label: object.object_number }]); // Prefer the active locale's label, then English, then the raw key. const lang = i18n.language.startsWith("sv") ? "sv" : "en"; diff --git a/web/src/objects/object-edit-form.tsx b/web/src/objects/object-edit-form.tsx index 3bb9188..39f1006 100644 --- a/web/src/objects/object-edit-form.tsx +++ b/web/src/objects/object-edit-form.tsx @@ -2,16 +2,31 @@ import { useState } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; +import type { components } from "../api/schema"; import { useObject, useUpdateObject, useSetFields, FieldRejection } from "../api/queries"; +import { useBreadcrumb } from "../shell/use-breadcrumb"; import { ObjectForm, type ObjectCore, type ObjectFormValues } from "./object-form"; +type AdminObjectView = components["schemas"]["AdminObjectView"]; + export function ObjectEditForm() { const { t } = useTranslation(); const { id } = useParams(); + + const { data: object, isLoading } = useObject(id!); + + if (isLoading) return
; + + if (!object) return

{t("objects.notFound")}

; + + return ; +} + +function ObjectEditFormLoaded({ object, id }: { object: AdminObjectView; id: string }) { + const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); - const { data: object, isLoading } = useObject(id!); const update = useUpdateObject(); const setFields = useSetFields(); @@ -27,9 +42,11 @@ export function ObjectEditForm() { locationState?.fieldErrorKey ?? null, ); - if (isLoading) return
; - - if (!object) return

{t("objects.notFound")}

; + useBreadcrumb([ + { label: t("nav.objects"), to: "/objects" }, + { label: object.object_number, to: `/objects/${id}` }, + { label: t("actions.edit") }, + ]); const core: ObjectCore = { object_number: object.object_number, @@ -49,8 +66,8 @@ export function ObjectEditForm() { setFieldErrorKey(null); try { - await update.mutateAsync({ id: id!, body: values.core }); - await setFields.mutateAsync({ id: id!, fields: values.fields }); + await update.mutateAsync({ id, body: values.core }); + await setFields.mutateAsync({ id, fields: values.fields }); } catch (e) { if (e instanceof FieldRejection) { setFieldErrorKey(e.field); diff --git a/web/src/objects/object-new-page.tsx b/web/src/objects/object-new-page.tsx index 4887877..d541b76 100644 --- a/web/src/objects/object-new-page.tsx +++ b/web/src/objects/object-new-page.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { ObjectForm, type ObjectFormValues } from "./object-form"; import { useCreateObject, useSetFields, FieldRejection } from "../api/queries"; import { useDocumentTitle } from "../lib/use-document-title"; +import { useBreadcrumb } from "../shell/use-breadcrumb"; import { PageTitle } from "@/components/ui/page-title"; export function ObjectNewPage() { @@ -15,6 +16,7 @@ export function ObjectNewPage() { const [error, setError] = useState(null); useDocumentTitle(t("objects.new")); + useBreadcrumb([{ label: t("nav.objects"), to: "/objects" }, { label: t("objects.new") }]); const onSubmit = async (values: ObjectFormValues) => { setError(null); diff --git a/web/src/search/search-page.tsx b/web/src/search/search-page.tsx index a14706a..4084b13 100644 --- a/web/src/search/search-page.tsx +++ b/web/src/search/search-page.tsx @@ -3,12 +3,14 @@ import { useTranslation } from "react-i18next"; import { SearchPanel } from "./search-panel"; import { useDocumentTitle } from "../lib/use-document-title"; +import { useBreadcrumb } from "../shell/use-breadcrumb"; import { PageTitle } from "@/components/ui/page-title"; export function SearchPage() { const { t } = useTranslation(); useDocumentTitle(t("nav.search")); + useBreadcrumb([{ label: t("nav.search") }]); return (
diff --git a/web/src/shell/breadcrumb.test.tsx b/web/src/shell/breadcrumb.test.tsx index 2934a63..67d090d 100644 --- a/web/src/shell/breadcrumb.test.tsx +++ b/web/src/shell/breadcrumb.test.tsx @@ -1,6 +1,9 @@ import { expect, test } from "vitest"; -import { screen } from "@testing-library/react"; +import { screen, within } from "@testing-library/react"; +import { Routes, Route } from "react-router-dom"; import { renderApp } from "../test/render"; +import { AppShell } from "./app-shell"; +import { ObjectNewPage } from "../objects/object-new-page"; import { BreadcrumbProvider } from "./breadcrumb-provider"; import { Breadcrumb } from "./breadcrumb"; import { useBreadcrumb } from "./use-breadcrumb"; @@ -24,3 +27,21 @@ test("renders the trail with a link on non-leaf crumbs", async () => { expect(link).toHaveAttribute("href", "/objects"); expect(screen.getByText("LM-0042")).toBeInTheDocument(); }); + +test("a nested route sets the header breadcrumb inside AppShell", async () => { + renderApp( + + }> + } /> + + , + { route: "/objects/new" }, + ); + + const nav = await screen.findByRole("navigation", { name: "Breadcrumb" }); + const within_nav = within(nav); + + const objectsLink = within_nav.getByRole("link", { name: "Objects" }); + expect(objectsLink).toHaveAttribute("href", "/objects"); + expect(within_nav.getByText("New object")).toBeInTheDocument(); +}); diff --git a/web/src/vocab/vocabularies-page.tsx b/web/src/vocab/vocabularies-page.tsx index 64e06d5..a82c8d1 100644 --- a/web/src/vocab/vocabularies-page.tsx +++ b/web/src/vocab/vocabularies-page.tsx @@ -3,12 +3,14 @@ import { useTranslation } from "react-i18next"; import { VocabularyList } from "./vocabulary-list"; import { useDocumentTitle } from "../lib/use-document-title"; +import { useBreadcrumb } from "../shell/use-breadcrumb"; import { PageTitle } from "@/components/ui/page-title"; export function VocabulariesPage() { const { t } = useTranslation(); useDocumentTitle(t("nav.vocabularies")); + useBreadcrumb([{ label: t("nav.vocabularies") }]); return (
diff --git a/web/src/vocab/vocabulary-terms.tsx b/web/src/vocab/vocabulary-terms.tsx index 4a786a2..dde518f 100644 --- a/web/src/vocab/vocabulary-terms.tsx +++ b/web/src/vocab/vocabulary-terms.tsx @@ -3,7 +3,8 @@ import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import type { components } from "../api/schema"; -import { useTerms, useAddTerm } from "../api/queries"; +import { useTerms, useAddTerm, useVocabularies } from "../api/queries"; +import { useBreadcrumb } from "../shell/use-breadcrumb"; import { LabelEditor } from "../components/label-editor"; import { TermRow } from "./term-row"; import { Button } from "@/components/ui/button"; @@ -29,6 +30,15 @@ export function VocabularyTerms() { const [error, setError] = useState(false); + const { data: vocabularies } = useVocabularies(); + const vocabKey = vocabularies?.find((v) => v.id === id)?.key; + + useBreadcrumb( + vocabKey + ? [{ label: t("nav.vocabularies"), to: "/vocabularies" }, { label: vocabKey }] + : [{ label: t("nav.vocabularies"), to: "/vocabularies" }], + ); + if (!id) return null; const onAdd = (event: FormEvent) => {