diff --git a/frontend/packages/core/src/AppProvider/index.tsx b/frontend/packages/core/src/AppProvider/index.tsx index 657829cd54..28e29c8e27 100644 --- a/frontend/packages/core/src/AppProvider/index.tsx +++ b/frontend/packages/core/src/AppProvider/index.tsx @@ -218,8 +218,8 @@ const ClutchApp = ({ : workflow.displayName; const workflowLayoutProps: LayoutProps = { - workflow, - title: heading, + workflowsInPath: workflows.filter(w => w.path === workflow.path), + title: route.displayName || workflow.displayName, subtitle: route.description, variant: route.layoutProps?.variant === null || 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 37811b3cce..db7e4c3e29 100644 --- a/frontend/packages/core/src/WorkflowLayout/index.tsx +++ b/frontend/packages/core/src/WorkflowLayout/index.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { matchPath } from "react-router-dom"; import type { Interpolation } from "@emotion/styled"; import type { CSSObject, Theme } from "@mui/material"; import { alpha } from "@mui/material"; @@ -17,7 +16,7 @@ import { useWorkflowLayoutContext } from "./context"; export type LayoutVariant = "standard" | "wizard"; export type LayoutProps = { - workflow: Workflow; + workflowsInPath: Array; variant?: LayoutVariant | null; title?: string; subtitle?: string; @@ -101,7 +100,7 @@ const Subtitle = styled(Typography)(({ theme }: { theme: Theme }) => ({ })); const WorkflowLayout = ({ - workflow, + workflowsInPath, variant = null, title = null, subtitle = null, @@ -124,22 +123,18 @@ const WorkflowLayout = ({ } }, [context]); + 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 && ( - + {(headerTitle || headerSubtitle) && ( 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, }; }) ); diff --git a/frontend/packages/wizard/src/wizard.tsx b/frontend/packages/wizard/src/wizard.tsx index 17de8ec39e..282fc99ba2 100644 --- a/frontend/packages/wizard/src/wizard.tsx +++ b/frontend/packages/wizard/src/wizard.tsx @@ -73,7 +73,9 @@ const Header = styled(Grid)<{ $orientation: MuiStepperProps["orientation"] }>( const Container = styled(MuiContainer)<{ $width: ContainerProps["width"] }>( ({ theme }: { theme: Theme }) => ({ - padding: theme.clutch.layout.gutter, + padding: theme.clutch.useWorkflowLayout + ? theme.spacing("none", "md") + : theme.clutch.layout.gutter, height: "100%", }), props => ({