diff --git a/frontend/packages/core/src/AppProvider/index.tsx b/frontend/packages/core/src/AppProvider/index.tsx index 0ea18eb2fc..fa4626212e 100644 --- a/frontend/packages/core/src/AppProvider/index.tsx +++ b/frontend/packages/core/src/AppProvider/index.tsx @@ -217,7 +217,7 @@ const ClutchApp = ({ : workflow.displayName; const workflowLayoutProps: LayoutProps = { - workflow, + workflowsInPath: workflows.filter(w => w.path === workflow.path), title: heading, subtitle: route.description, variant: diff --git a/frontend/packages/core/src/AppProvider/workflow.tsx b/frontend/packages/core/src/AppProvider/workflow.tsx index 9ae6d29957..82a5d61287 100644 --- a/frontend/packages/core/src/AppProvider/workflow.tsx +++ b/frontend/packages/core/src/AppProvider/workflow.tsx @@ -55,7 +55,7 @@ interface WorkflowLayoutConfiguration { /** * (Optional) property to pass the defined layout properties to all of its defined routes */ - defaultLayoutProps?: Omit; + defaultLayoutProps?: Omit; } export interface Workflow @@ -105,7 +105,7 @@ export interface Route { /** * (Optional) property to define layout properties for a single route */ - layoutProps?: Omit; + layoutProps?: Omit; } export interface ConfiguredRoute extends Route { diff --git a/frontend/packages/core/src/WorkflowLayout/index.tsx b/frontend/packages/core/src/WorkflowLayout/index.tsx index 3fb0f8bd53..63c3c86052 100644 --- a/frontend/packages/core/src/WorkflowLayout/index.tsx +++ b/frontend/packages/core/src/WorkflowLayout/index.tsx @@ -1,12 +1,11 @@ import React from "react"; -import { matchPath, useParams } from "react-router-dom"; import type { Interpolation } from "@emotion/styled"; import type { CSSObject, Theme } from "@mui/material"; import { alpha } from "@mui/material"; import type { Workflow } from "../AppProvider/workflow"; import Breadcrumbs from "../Breadcrumbs"; -import { useLocation } from "../navigation"; +import { useLocation, useParams } from "../navigation"; import styled from "../styled"; import { Typography } from "../typography"; import { generateBreadcrumbsEntries } from "../utils"; @@ -14,7 +13,7 @@ import { generateBreadcrumbsEntries } from "../utils"; export type LayoutVariant = "standard" | "wizard"; export type LayoutProps = { - workflow: Workflow; + workflowsInPath: Array; variant?: LayoutVariant | null; title?: string | ((params: Record) => string); subtitle?: string; @@ -88,7 +87,7 @@ const Subtitle = styled(Typography)(({ theme }: { theme: Theme }) => ({ })); const WorkflowLayout = ({ - workflow, + workflowsInPath, variant = null, title = null, subtitle = null, @@ -99,22 +98,18 @@ const WorkflowLayout = ({ const params = useParams(); const location = useLocation(); + const entries = generateBreadcrumbsEntries(workflowsInPath, location); + if (variant === null) { return <>{children}; } - const workflowPaths = workflow.routes.map(({ path }) => `/${workflow.path}/${path}`); - const breadcrumbsEntries = generateBreadcrumbsEntries( - location, - url => !!workflowPaths.find(path => !!matchPath({ path }, url)) - ); - return ( {!hideHeader && ( - + {!breadcrumbsOnly && (title || subtitle) && ( diff --git a/frontend/packages/core/src/utils/generateBreadcrumbsEntries.tsx b/frontend/packages/core/src/utils/generateBreadcrumbsEntries.tsx index 099e1e213b..e2d0887b01 100644 --- a/frontend/packages/core/src/utils/generateBreadcrumbsEntries.tsx +++ b/frontend/packages/core/src/utils/generateBreadcrumbsEntries.tsx @@ -1,23 +1,56 @@ -import type { Location } from "react-router-dom"; +import { Location, matchPath } from "react-router-dom"; +import type { Workflow } from "../AppProvider/workflow"; import type { BreadcrumbEntry } from "../Breadcrumbs"; -const generateBreadcrumbsEntries = (location: Location, validateUrl: (url: string) => boolean) => { - const labels = decodeURIComponent(location.pathname) +const HOME_ENTRY = { label: "Home", url: "/" }; + +const generateBreadcrumbsEntries = (workflowsInPath: Array, location: Location) => { + // The first workflow in the will contain + // the same path and displayName as the others + const firstWorkflow = workflowsInPath[0]; + + if (!firstWorkflow) { + return [HOME_ENTRY]; + } + + // Get a single level list of the routes available + const allRoutes = workflowsInPath.flatMap(w => w.routes); + + // Add to every item in the routes list the workflow path prefix + const fullPaths = allRoutes.map(({ path }) => `/${firstWorkflow.path}/${path}`); + + // Generate a list of path segments from the location + const pathSegments = decodeURIComponent(location.pathname) .split("/") - .slice(1, location.pathname.endsWith("/") ? -1 : undefined); + .slice(1, location.pathname.endsWith("/") ? -1 : undefined); // in case of a trailing `/` + + const entries: Array = [HOME_ENTRY].concat( + pathSegments.map((segment, index) => { + const nextIndex = index + 1; + const url = `/${pathSegments.slice(0, nextIndex).join("/")}`; - const entries: Array = [{ label: "Home", url: "/" }].concat( - labels.map((label, index) => { - let url = `/${labels.slice(0, index + 1).join("/")}`; + const path = fullPaths.find(p => !!matchPath(p, url)); - if (!validateUrl(url)) { - url = undefined; - } + // If there is a matched path, it's used to find the route that contains its displayName + const route = path + ? allRoutes.find(r => + r.path.startsWith("/") + ? r.path + : // Done in case of an empty path or missing a leading `/` + `/${r.path}` === `/${path.split("/").slice(2).join("/")}` + ) + : null; return { - label, - url, + // For the label: + // - Prioritize the display name + // - Handle the case of a single route with an unusual long name + // - Default to the path segment + label: + route?.displayName || (allRoutes.length === 1 && firstWorkflow.displayName) || segment, + // Set a null url if there is no path or for the last segment + url: !!path && pathSegments.length !== nextIndex ? url : null, }; }) );