Skip to content

Commit

Permalink
frontend: Refactor workflows to use new layout (#3159)
Browse files Browse the repository at this point in the history
  • Loading branch information
septum authored Nov 6, 2024
1 parent f646ee2 commit bbd04de
Show file tree
Hide file tree
Showing 39 changed files with 236 additions and 108 deletions.
21 changes: 17 additions & 4 deletions frontend/packages/core/src/AppProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ const ClutchApp = ({

return (
<Router>
<Theme>
<Theme useWorkflowLayout={appConfiguration?.useWorkflowLayout}>
<div id="App">
<ApplicationContext.Provider value={appContextValue}>
<UserPreferencesProvider>
Expand Down Expand Up @@ -217,9 +217,22 @@ const ClutchApp = ({
: workflow.displayName;

const workflowLayoutProps: LayoutProps = {
...route.layoutProps,
heading: route.layoutProps?.heading || heading,
workflow,
title: heading,
subtitle: route.description,
variant:
route.layoutProps?.variant === null ||
route.layoutProps?.variant !== undefined
? route.layoutProps?.variant
: workflow.defaultLayoutProps?.variant,
breadcrumbsOnly:
route.layoutProps?.breadcrumbsOnly ??
workflow.defaultLayoutProps?.breadcrumbsOnly ??
false,
hideHeader:
route.layoutProps?.hideHeader ??
workflow.defaultLayoutProps?.hideHeader ??
false,
};

const workflowRouteComponent = (
Expand All @@ -240,7 +253,7 @@ const ClutchApp = ({
key={workflow.path}
path={`${route.path.replace(/^\/+/, "").replace(/\/+$/, "")}`}
element={
appConfiguration?.enableWorkflowLayout ? (
appConfiguration?.useWorkflowLayout ? (
<WorkflowLayout {...workflowLayoutProps}>
{workflowRouteComponent}
</WorkflowLayout>
Expand Down
11 changes: 9 additions & 2 deletions frontend/packages/core/src/AppProvider/themes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@ declare module "@mui/material/styles" {

const useTheme = () => useMuiTheme() as MuiTheme;

const Theme: React.FC = ({ children }) => {
interface ThemeProps {
useWorkflowLayout?: boolean;
}

const Theme: React.FC<ThemeProps> = ({ useWorkflowLayout = false, children }) => {
// Uncomment to use dark mode
/* // Detect system color mode
const prefersDarkMode =
window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; */
const prefersDarkMode = false;
return (
<ThemeProvider variant={prefersDarkMode ? THEME_VARIANTS.dark : THEME_VARIANTS.light}>
<ThemeProvider
variant={prefersDarkMode ? THEME_VARIANTS.dark : THEME_VARIANTS.light}
useWorkflowLayout={useWorkflowLayout}
>
{children}
</ThemeProvider>
);
Expand Down
21 changes: 17 additions & 4 deletions frontend/packages/core/src/AppProvider/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,17 @@ interface WorkflowShortlinkConfiguration {
shortLink?: boolean;
}

export interface Workflow extends BaseWorkflowConfiguration, WorkflowShortlinkConfiguration {
interface WorkflowLayoutConfiguration {
/**
* (Optional) property to pass the defined layout properties to all of its defined routes
*/
defaultLayoutProps?: Omit<LayoutProps, "workflow" | "title" | "subtitle">;
}

export interface Workflow
extends BaseWorkflowConfiguration,
WorkflowShortlinkConfiguration,
WorkflowLayoutConfiguration {
/**
* An optional property that is set via the config and allows for the display of an icon given a path,
* this will override the default avatar.
Expand All @@ -68,7 +78,8 @@ export interface Workflow extends BaseWorkflowConfiguration, WorkflowShortlinkCo

export interface WorkflowConfiguration
extends BaseWorkflowConfiguration,
WorkflowShortlinkConfiguration {
WorkflowShortlinkConfiguration,
WorkflowLayoutConfiguration {
shortLink?: boolean;
routes: {
[key: string]: Route;
Expand All @@ -91,8 +102,10 @@ export interface Route {
* If this is not set the route will always be registered.
*/
featureFlag?: string;

layoutProps?: Omit<LayoutProps, "workflow">;
/**
* (Optional) property to define layout properties for a single route
*/
layoutProps?: Omit<LayoutProps, "workflow" | "title" | "subtitle">;
}

export interface ConfiguredRoute extends Route {
Expand Down
23 changes: 20 additions & 3 deletions frontend/packages/core/src/Theme/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ declare module "@emotion/react" {
declare module "@mui/material/styles" {
interface Theme {
clutch: {
useWorkflowLayout: boolean;
spacing: {
none: number;
xs: number;
Expand All @@ -27,10 +28,14 @@ declare module "@mui/material/styles" {
lg: number;
xl: number;
};
layout: {
gutter: string;
};
};
}
interface ThemeOptions {
clutch: {
useWorkflowLayout: boolean;
spacing: {
none: number;
xs: number;
Expand All @@ -40,19 +45,23 @@ declare module "@mui/material/styles" {
lg: number;
xl: number;
};
layout: {
gutter: string;
};
};
}
}

// Create a Material UI theme is propagated to all children.
const createTheme = (variant: ThemeVariant): MuiTheme => {
const createTheme = (variant: ThemeVariant, useWorkflowLayout: boolean): MuiTheme => {
return createMuiTheme({
colors: clutchColors(variant),
palette: palette(variant),
// `8` is the default scaling factor in MUI, we are setting it again to make it explicit
// https://v5.mui.com/material-ui/customization/spacing/
spacing: 8,
clutch: {
useWorkflowLayout,
spacing: {
none: 0,
xs: 0.5,
Expand All @@ -62,6 +71,9 @@ const createTheme = (variant: ThemeVariant): MuiTheme => {
lg: 4,
xl: 5,
},
layout: {
gutter: useWorkflowLayout ? "0px" : "24px",
},
},
transitions: {
// https://material-ui.com/getting-started/faq/#how-can-i-disable-transitions-globally
Expand Down Expand Up @@ -114,12 +126,17 @@ const createTheme = (variant: ThemeVariant): MuiTheme => {

interface ThemeProps {
variant?: ThemeVariant;
useWorkflowLayout?: boolean;
children: React.ReactNode;
}

const ThemeProvider = ({ children, variant = THEME_VARIANTS.light }: ThemeProps) => (
const ThemeProvider = ({
children,
useWorkflowLayout = false,
variant = THEME_VARIANTS.light,
}: ThemeProps) => (
<StyledEngineProvider injectFirst>
<MuiThemeProvider theme={createTheme(variant)}>
<MuiThemeProvider theme={createTheme(variant, useWorkflowLayout)}>
<CssBaseline />
{children}
</MuiThemeProvider>
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/core/src/Types/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export interface AppConfiguration {
/** Supports a react node or a string representing a public assets path */
logo?: React.ReactNode | string;
banners?: AppBanners;
enableWorkflowLayout?: boolean;
useWorkflowLayout?: boolean;
}
75 changes: 50 additions & 25 deletions frontend/packages/core/src/WorkflowLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from "react";
import { matchPath } from "react-router-dom";
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";
Expand All @@ -10,12 +11,14 @@ import styled from "../styled";
import { Typography } from "../typography";
import { generateBreadcrumbsEntries } from "../utils";

export type LayoutVariant = "standard" | "wizard" | "custom";
export type LayoutVariant = "standard" | "wizard";

export type LayoutProps = {
workflow: Workflow;
variant?: LayoutVariant;
heading?: string | React.ReactElement;
variant?: LayoutVariant | null;
title?: string | ((params: Record<string, string>) => string);
subtitle?: string;
breadcrumbsOnly?: boolean;
hideHeader?: boolean;
};

Expand Down Expand Up @@ -43,8 +46,6 @@ const getContainerVariantStyles = (variant: LayoutVariant, theme: Theme) => {
padding: theme.spacing(theme.clutch.spacing.lg, theme.clutch.spacing.none),
margin: theme.spacing(theme.clutch.spacing.none, "auto"),
},
// No styles
custom: {},
};
return layoutVariantStylesMap[variant];
};
Expand All @@ -63,49 +64,73 @@ const PageHeader = styled("div")(({ $variant, theme }: StyledVariantComponentPro
width: "100%",
}));

const PageHeaderMainContainer = styled("div")({
const PageHeaderBreadcrumbsWrapper = styled("div")(({ theme }: { theme: Theme }) => ({
marginBottom: theme.spacing(theme.clutch.spacing.xs),
}));

const PageHeaderMainContainer = styled("div")(({ theme }: { theme: Theme }) => ({
display: "flex",
alignItems: "center",
height: "70px",
marginBottom: theme.spacing(theme.clutch.spacing.sm),
}));

const PageHeaderInformation = styled("div")({
display: "flex",
flexDirection: "column",
justifyContent: "space-evenly",
height: "100%",
});

const Heading = styled(Typography)({
const Title = styled(Typography)({
lineHeight: 1,
});

const Subtitle = styled(Typography)(({ theme }: { theme: Theme }) => ({
color: alpha(theme.colors.neutral[900], 0.45),
}));

const WorkflowLayout = ({
workflow,
variant = "standard",
heading = null,
variant = null,
title = null,
subtitle = null,
breadcrumbsOnly = false,
hideHeader = false,
children,
}: React.PropsWithChildren<LayoutProps>) => {
const params = useParams();
const location = useLocation();

if (variant === null) {
return <>{children}</>;
}

const workflowPaths = workflow.routes.map(({ path }) => `/${workflow.path}/${path}`);
const breadcrumbsEntries = generateBreadcrumbsEntries(
location,
(url: string) =>
`/${workflow.path}` !== url &&
!workflowPaths.includes(url) &&
!workflowPaths.find(path => !!matchPath({ path }, url))
url => !!workflowPaths.find(path => !!matchPath({ path }, url))
);

return (
<LayoutContainer $variant={variant}>
{!hideHeader && (
<PageHeader $variant={variant}>
<Breadcrumbs entries={breadcrumbsEntries} />
<PageHeaderMainContainer>
{heading && (
<>
{React.isValidElement(heading) ? (
heading
) : (
<Heading variant="h2">{heading}</Heading>
<PageHeaderBreadcrumbsWrapper>
<Breadcrumbs entries={breadcrumbsEntries} />
</PageHeaderBreadcrumbsWrapper>
{!breadcrumbsOnly && (title || subtitle) && (
<PageHeaderMainContainer>
<PageHeaderInformation>
{title && (
<Title variant="h2" textTransform="capitalize">
{typeof title === "function" ? title(params) : title}
</Title>
)}
</>
)}
</PageHeaderMainContainer>
{subtitle && <Subtitle variant="subtitle2">{subtitle}</Subtitle>}
</PageHeaderInformation>
</PageHeaderMainContainer>
)}
</PageHeader>
)}
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import type { Location } from "react-router-dom";
import type { BreadcrumbEntry } from "../Breadcrumbs";

const generateBreadcrumbsEntries = (location: Location, validateUrl: (url: string) => boolean) => {
const labels = location.pathname
const labels = decodeURIComponent(location.pathname)
.split("/")
.slice(1, location.pathname.endsWith("/") ? -1 : undefined);

const entries: Array<BreadcrumbEntry> = [{ label: "Home", url: "/" }].concat(
labels.map((label, index) => {
let url = `/${labels.slice(0, index + 1).join("/")}`;

if (validateUrl(url)) {
if (!validateUrl(url)) {
url = undefined;
}

Expand Down
12 changes: 7 additions & 5 deletions frontend/packages/wizard/src/wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useLocation,
useNavigate,
useSearchParams,
useTheme,
WizardContext,
} from "@clutch-sh/core";
import type { ManagerLayout } from "@clutch-sh/data-layout";
Expand Down Expand Up @@ -71,10 +72,10 @@ const Header = styled(Grid)<{ $orientation: MuiStepperProps["orientation"] }>(
);

const Container = styled(MuiContainer)<{ $width: ContainerProps["width"] }>(
{
paddingBlock: "24px 32px",
height: "calc(100% - 56px)",
},
({ theme }: { theme: Theme }) => ({
padding: theme.clutch.layout.gutter,
height: "100%",
}),
props => ({
width: props.$width === "full" ? "100%" : "800px",
})
Expand Down Expand Up @@ -126,6 +127,7 @@ const Wizard = ({
const locationState = useLocation().state as { origin?: string };
const navigate = useNavigate();
const [origin] = React.useState(locationState?.origin);
const theme = useTheme();

const updateStepData = (stepName: string, data: object) => {
setWizardStepData(prevState => {
Expand Down Expand Up @@ -253,7 +255,7 @@ const Wizard = ({
return (
<Container $width={width} maxWidth={false} className={className}>
<MaxHeightGrid container alignItems="stretch">
{heading && (
{!theme.clutch.useWorkflowLayout && heading && (
<Header item $orientation={orientation}>
{React.isValidElement(heading) ? (
heading
Expand Down
Loading

0 comments on commit bbd04de

Please sign in to comment.