diff --git a/web/src/objects/objects-page.tsx b/web/src/objects/objects-page.tsx index d3f490c..5894ec6 100644 --- a/web/src/objects/objects-page.tsx +++ b/web/src/objects/objects-page.tsx @@ -6,6 +6,7 @@ import { X } from "lucide-react"; import { ObjectsTable } from "./objects-table"; import { useMediaQuery } from "../lib/use-media-query"; import { useDocumentTitle } from "../lib/use-document-title"; +import { useBreadcrumb } from "../shell/use-breadcrumb"; import { PageTitle } from "@/components/ui/page-title"; const ObjectDetailDrawer = lazy(() => @@ -26,6 +27,7 @@ export function ObjectsPage() { const isWide = useMediaQuery("(min-width: 1024px)"); useDocumentTitle(t("nav.objects")); + useBreadcrumb([{ label: t("nav.objects") }]); const closeDetail = () => navigate(`/objects?${searchParams}`); diff --git a/web/src/shell/app-shell.tsx b/web/src/shell/app-shell.tsx index d7ff942..9c06929 100644 --- a/web/src/shell/app-shell.tsx +++ b/web/src/shell/app-shell.tsx @@ -6,6 +6,8 @@ import { Button } from "@/components/ui/button"; import { LangSwitch } from "./lang-switch"; import { ThemeSwitch } from "./theme-switch"; import { Sidebar } from "./sidebar"; +import { BreadcrumbProvider } from "./breadcrumb-provider"; +import { Breadcrumb } from "./breadcrumb"; export function AppShell() { const { t } = useTranslation(); @@ -20,19 +22,21 @@ export function AppShell() { return (
-
-
-
- - - -
-
- -
-
+ +
+
+ + + + +
+
+ +
+
+
); } diff --git a/web/src/shell/breadcrumb-context.ts b/web/src/shell/breadcrumb-context.ts new file mode 100644 index 0000000..d1efe51 --- /dev/null +++ b/web/src/shell/breadcrumb-context.ts @@ -0,0 +1,17 @@ +import { createContext, useContext } from "react"; + +export type BreadcrumbItem = { label: string; to?: string }; + +type BreadcrumbContextValue = { + trail: BreadcrumbItem[]; + setTrail: (trail: BreadcrumbItem[]) => void; +}; + +export const BreadcrumbContext = createContext({ + trail: [], + setTrail: () => {}, +}); + +export function useBreadcrumbTrail(): BreadcrumbItem[] { + return useContext(BreadcrumbContext).trail; +} diff --git a/web/src/shell/breadcrumb-provider.tsx b/web/src/shell/breadcrumb-provider.tsx new file mode 100644 index 0000000..9bbca29 --- /dev/null +++ b/web/src/shell/breadcrumb-provider.tsx @@ -0,0 +1,12 @@ +import { useState, type ReactNode } from "react"; + +import { BreadcrumbContext, type BreadcrumbItem } from "./breadcrumb-context"; + +export function BreadcrumbProvider({ children }: { children: ReactNode }) { + const [trail, setTrail] = useState([]); + return ( + + {children} + + ); +} diff --git a/web/src/shell/breadcrumb.test.tsx b/web/src/shell/breadcrumb.test.tsx new file mode 100644 index 0000000..2934a63 --- /dev/null +++ b/web/src/shell/breadcrumb.test.tsx @@ -0,0 +1,26 @@ +import { expect, test } from "vitest"; +import { screen } from "@testing-library/react"; +import { renderApp } from "../test/render"; +import { BreadcrumbProvider } from "./breadcrumb-provider"; +import { Breadcrumb } from "./breadcrumb"; +import { useBreadcrumb } from "./use-breadcrumb"; + +function Setter() { + useBreadcrumb([ + { label: "Objects", to: "/objects" }, + { label: "LM-0042" }, + ]); + return null; +} + +test("renders the trail with a link on non-leaf crumbs", async () => { + renderApp( + + + + , + ); + const link = await screen.findByRole("link", { name: "Objects" }); + expect(link).toHaveAttribute("href", "/objects"); + expect(screen.getByText("LM-0042")).toBeInTheDocument(); +}); diff --git a/web/src/shell/breadcrumb.tsx b/web/src/shell/breadcrumb.tsx new file mode 100644 index 0000000..68ed412 --- /dev/null +++ b/web/src/shell/breadcrumb.tsx @@ -0,0 +1,30 @@ +import { Fragment } from "react"; +import { Link } from "react-router-dom"; + +import { useBreadcrumbTrail } from "./breadcrumb-context"; + +export function Breadcrumb() { + const trail = useBreadcrumbTrail(); + if (trail.length === 0) return
; + return ( + + ); +} diff --git a/web/src/shell/use-breadcrumb.ts b/web/src/shell/use-breadcrumb.ts new file mode 100644 index 0000000..042d3ed --- /dev/null +++ b/web/src/shell/use-breadcrumb.ts @@ -0,0 +1,12 @@ +import { useContext, useEffect } from "react"; + +import { BreadcrumbContext, type BreadcrumbItem } from "./breadcrumb-context"; + +export function useBreadcrumb(trail: BreadcrumbItem[]): void { + const { setTrail } = useContext(BreadcrumbContext); + const key = trail.map((i) => `${i.label} ${i.to ?? ""}`).join(""); + useEffect(() => { + setTrail(trail); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [key, setTrail]); +}