diff --git a/front/src/app/components/Breadcrumbs.tsx b/front/src/app/components/Breadcrumbs.tsx index 53b9259f03..864a6ec044 100644 --- a/front/src/app/components/Breadcrumbs.tsx +++ b/front/src/app/components/Breadcrumbs.tsx @@ -3,13 +3,15 @@ import { Breadcrumb } from "@codegouvfr/react-dsfr/Breadcrumb"; import { slice } from "ramda"; import React from "react"; import { errors } from "shared"; -import { makeBreadcrumbsSegments } from "../contents/breadcrumbs/breadcrumbs"; +import { getBreadcrumbs } from "src/app/contents/breadcrumbs/breadcrumbs"; import { useRoute } from "../routes/routes"; export const Breadcrumbs = () => { const { name: currentRouteName } = useRoute(); if (!currentRouteName) return null; - const segments = makeBreadcrumbsSegments(currentRouteName); + const segments = getBreadcrumbs({ + currentRouteKey: currentRouteName, + }); if (segments.length === 1) throw errors.breadcrumbs.notFound({ currentRouteName }); const ancestors = slice(0, -1, segments); diff --git a/front/src/app/contents/breadcrumbs/breadcrumbs.ts b/front/src/app/contents/breadcrumbs/breadcrumbs.ts index 37e6d52185..0288a7bcb4 100644 --- a/front/src/app/contents/breadcrumbs/breadcrumbs.ts +++ b/front/src/app/contents/breadcrumbs/breadcrumbs.ts @@ -1,6 +1,5 @@ import { BreadcrumbProps } from "@codegouvfr/react-dsfr/Breadcrumb"; -import { flatten } from "ramda"; -import { keys } from "shared"; +import { makeBreadcrumbsSegments } from "src/app/utils/breadcrumbs"; import { Route } from "type-route"; import { FrontRouteKeys, FrontRouteUnion, routes } from "../../routes/routes"; @@ -12,11 +11,16 @@ export type BreadcrumbsItem = { }; }; -export type Breadcrumbs = { - [K in FrontRouteKeys]?: BreadcrumbsItem; +export type Breadcrumbs = { + [K in T]?: BreadcrumbsItem; }; -export const breadcrumbs: Breadcrumbs = { +export const defaultAncestor: BreadcrumbProps["segments"][0] = { + label: "Accueil", + linkProps: routes.home().link, +}; + +export const breadcrumbs: Breadcrumbs = { homeAgencies: { label: "Organismes prescripteurs", route: routes.homeAgencies(), @@ -75,78 +79,7 @@ export const breadcrumbs: Breadcrumbs = { }, }; -export const makeBreadcrumbsSegments = ( - currentRouteKey: FrontRouteKeys, -): BreadcrumbProps["segments"] => { - const currentRouteAncestor = keys(breadcrumbs).find((key) => { - const currentRouteInBreadcrumbs = breadcrumbs[key]; - if (!currentRouteInBreadcrumbs) return false; - return ( - currentRouteInBreadcrumbs.route.name === currentRouteKey || - isRouteInChildren(currentRouteKey, currentRouteInBreadcrumbs) - ); - }); - return [ - { - label: "Accueil", - linkProps: routes.home().link, - }, - ...(currentRouteAncestor && breadcrumbs[currentRouteAncestor] - ? formatToBreadcrumbsSegments( - breadcrumbs[currentRouteAncestor], - currentRouteKey, - ) - : []), - ]; -}; - -const isRouteInChildren = ( - currentRouteKey: FrontRouteKeys, - breadcrumbsItem: BreadcrumbsItem, -): boolean => { - const children = breadcrumbsItem.children; - if (!children) return false; - return keys(children).some((key) => { - const child = children[key]; - if (!child) return false; - if (child.route.name === currentRouteKey) return true; - if (child.children) { - return isRouteInChildren(currentRouteKey, child); - } - }); -}; - -const formatToBreadcrumbsSegments = ( - ancestor: BreadcrumbsItem, - currentRouteKey: FrontRouteKeys, -): BreadcrumbProps["segments"] => { - const { label, route, children } = ancestor; - const ancestorSegment = { label, linkProps: route.link }; - if (!children || ancestor.route.name === currentRouteKey) - return [ancestorSegment]; - const childSegments = flatten( - keys(children).map((key) => { - const child = children[key]; - - if (!child) return; - - const { route, label, children: childChildren } = child; - const { name: routeName, link: linkProps } = route; - - if ( - isRouteInChildren(currentRouteKey, child) && - routeName !== currentRouteKey && - childChildren - ) { - return formatToBreadcrumbsSegments(child, currentRouteKey); - } - if (routeName !== currentRouteKey) return; - - return { - label, - linkProps, - }; - }), - ).filter((child) => child !== undefined); - return [ancestorSegment, ...childSegments]; -}; +export const getBreadcrumbs = makeBreadcrumbsSegments( + breadcrumbs, + defaultAncestor, +); diff --git a/front/src/app/utils/breadcrumbs.ts b/front/src/app/utils/breadcrumbs.ts new file mode 100644 index 0000000000..18b9cb911d --- /dev/null +++ b/front/src/app/utils/breadcrumbs.ts @@ -0,0 +1,86 @@ +import { BreadcrumbProps } from "@codegouvfr/react-dsfr/Breadcrumb"; +import { flatten, keys } from "ramda"; +import { + Breadcrumbs, + BreadcrumbsItem, +} from "src/app/contents/breadcrumbs/breadcrumbs"; + +export const makeBreadcrumbsSegments = + ( + breadcrumbsSet: Breadcrumbs, + rootAncestor: BreadcrumbProps["segments"][0], + ) => + ({ + currentRouteKey, + }: { + currentRouteKey: K; + }): BreadcrumbProps["segments"] => { + const currentRouteAncestor = keys(breadcrumbsSet).find((key) => { + const currentRouteInBreadcrumbs = breadcrumbsSet[key]; + if (!currentRouteInBreadcrumbs) return false; + return ( + currentRouteInBreadcrumbs.route.name === currentRouteKey || + isRouteInChildren(currentRouteKey, currentRouteInBreadcrumbs) + ); + }); + return [ + rootAncestor, + ...(currentRouteAncestor && breadcrumbsSet[currentRouteAncestor] + ? formatToBreadcrumbsSegments( + breadcrumbsSet[currentRouteAncestor], + currentRouteKey, + ) + : []), + ]; + }; + +const isRouteInChildren = ( + currentRouteKey: T, + breadcrumbsItem: BreadcrumbsItem, +): boolean => { + const children = breadcrumbsItem.children; + if (!children) return false; + return keys(children).some((key) => { + const child = children[key]; + if (!child) return false; + if (child.route.name === currentRouteKey) return true; + if (child.children) { + return isRouteInChildren(currentRouteKey, child); + } + }); +}; + +const formatToBreadcrumbsSegments = ( + ancestor: BreadcrumbsItem, + currentRouteKey: T, +): BreadcrumbProps["segments"] => { + const { label, route, children } = ancestor; + const ancestorSegment = { label, linkProps: route.link }; + if (!children || ancestor.route.name === currentRouteKey) + return [ancestorSegment]; + const childSegments = flatten( + keys(children).map((key) => { + const child = children[key]; + + if (!child) return; + + const { route, label, children: childChildren } = child; + const { name: routeName, link: linkProps } = route; + + if ( + isRouteInChildren(currentRouteKey, child) && + routeName !== currentRouteKey && + childChildren + ) { + return formatToBreadcrumbsSegments(child, currentRouteKey); + } + if (routeName !== currentRouteKey) return; + + return { + label, + linkProps, + }; + }), + ).filter((child) => child !== undefined); + return [ancestorSegment, ...childSegments]; +}; diff --git a/front/src/app/utils/breadcrumbs.unit.test.ts b/front/src/app/utils/breadcrumbs.unit.test.ts new file mode 100644 index 0000000000..a17047ae31 --- /dev/null +++ b/front/src/app/utils/breadcrumbs.unit.test.ts @@ -0,0 +1,141 @@ +import { BreadcrumbProps } from "@codegouvfr/react-dsfr/Breadcrumb"; +import { Route } from "type-route"; +import { makeBreadcrumbsSegments } from "./breadcrumbs"; + +const makeFakeRoute = (name: string): Route => ({ + name, + link: { + href: `/${name}`, + onClick: () => {}, + }, + action: null, + params: {}, + href: `/${name}`, + push: () => {}, + replace: () => {}, +}); + +describe("makeBreadcrumbsSegments", () => { + it("should returns segments for a route at level 1", () => { + const segments = getTestBreadcrumbs({ + currentRouteKey: "homeCandidates", + }); + expect(segments).toEqual([ + { + label: "Accueil", + linkProps: { + href: "/", + onClick: expect.any(Function), + }, + }, + { + label: "Candidats", + linkProps: { + href: "/homeCandidates", + onClick: expect.any(Function), + }, + }, + ]); + }); + it("should returns segments for a route at a deep level", () => { + const segments = getTestBreadcrumbs({ + currentRouteKey: "searchDiagoriente", + }); + expect(segments).toEqual([ + { + label: "Accueil", + linkProps: { + href: "/", + onClick: expect.any(Function), + }, + }, + { + label: "Candidats", + linkProps: { + href: "/homeCandidates", + onClick: expect.any(Function), + }, + }, + { + label: "Recherche", + linkProps: { + href: "/search", + onClick: expect.any(Function), + }, + }, + { + label: "Recherche (langage naturel)", + linkProps: { + href: "/searchDiagoriente", + onClick: expect.any(Function), + }, + }, + ]); + }); + + it("should returns segments for a route without its siblings", () => { + const segments = getTestBreadcrumbs({ + currentRouteKey: "beneficiaryDashboard", + }); + expect(segments).toEqual([ + { + label: "Accueil", + linkProps: { + href: "/", + onClick: expect.any(Function), + }, + }, + { + label: "Candidats", + linkProps: { + href: "/homeCandidates", + onClick: expect.any(Function), + }, + }, + { + label: "Tableau de bord", + linkProps: { + href: "/beneficiaryDashboard", + onClick: expect.any(Function), + }, + }, + ]); + }); +}); + +const testBreadcrumbsSet = { + homeCandidates: { + label: "Candidats", + route: makeFakeRoute("homeCandidates"), + children: { + search: { + label: "Recherche", + route: makeFakeRoute("search"), + children: { + searchDiagoriente: { + label: "Recherche (langage naturel)", + route: makeFakeRoute("searchDiagoriente"), + }, + }, + }, + + beneficiaryDashboard: { + label: "Tableau de bord", + route: makeFakeRoute("beneficiaryDashboard"), + }, + }, + }, +}; + +const testRootAncestor: BreadcrumbProps["segments"][0] = { + label: "Accueil", + linkProps: { + href: "/", + onClick: () => {}, + }, +}; + +const getTestBreadcrumbs = makeBreadcrumbsSegments( + testBreadcrumbsSet, + testRootAncestor, +);