Skip to content

Commit

Permalink
[DashboardLayout] Support route patterns in navigation (#3991)
Browse files Browse the repository at this point in the history
Signed-off-by: Pedro Ferreira <[email protected]>
Co-authored-by: Jan Potoms <[email protected]>
  • Loading branch information
apedroferreira and Janpot authored Sep 2, 2024
1 parent c5a7ced commit eb60981
Show file tree
Hide file tree
Showing 14 changed files with 460 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { extendTheme } from '@mui/material/styles';
import { createTheme } from '@mui/material/styles';
import DashboardIcon from '@mui/icons-material/Dashboard';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
Expand All @@ -15,7 +15,11 @@ const NAVIGATION = [
},
];

const demoTheme = extendTheme({
const demoTheme = createTheme({
cssVariables: {
colorSchemeSelector: 'data-toolpad-color-scheme',
},
colorSchemes: { light: true, dark: true },
breakpoints: {
values: {
xs: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { extendTheme } from '@mui/material/styles';
import { createTheme } from '@mui/material/styles';
import DashboardIcon from '@mui/icons-material/Dashboard';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
Expand All @@ -15,7 +15,11 @@ const NAVIGATION: Navigation = [
},
];

const demoTheme = extendTheme({
const demoTheme = createTheme({
cssVariables: {
colorSchemeSelector: 'data-toolpad-color-scheme',
},
colorSchemes: { light: true, dark: true },
breakpoints: {
values: {
xs: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { createTheme } from '@mui/material/styles';
import DashboardIcon from '@mui/icons-material/Dashboard';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';

const demoTheme = createTheme({
cssVariables: {
colorSchemeSelector: 'data-toolpad-color-scheme',
},
colorSchemes: { light: true, dark: true },
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 600,
lg: 1200,
xl: 1536,
},
},
});

function DemoPageContent({ pathname, navigate }) {
return (
<Box
sx={{
py: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
}}
>
<Typography>
<p>Dashboard content for {pathname}</p>
</Typography>
{pathname.startsWith('/orders') ? (
<Stack direction="row" spacing={1} sx={{ pt: 1 }}>
<Button
onClick={() => {
navigate('/orders/1');
}}
>
Order 1
</Button>
<Button
onClick={() => {
navigate('/orders/2');
}}
>
Order 2
</Button>
<Button
onClick={() => {
navigate('/orders/3');
}}
>
Order 3
</Button>
</Stack>
) : null}
</Box>
);
}

DemoPageContent.propTypes = {
navigate: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
};

function DashboardLayoutPattern(props) {
const { window } = props;

const [pathname, setPathname] = React.useState('/orders');
const navigate = React.useCallback((path) => setPathname(String(path)), []);

const router = React.useMemo(() => {
return {
pathname,
searchParams: new URLSearchParams(),
navigate,
};
}, [pathname, navigate]);

// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;

return (
// preview-start
<AppProvider
navigation={[
{
segment: 'dashboard',
title: 'Dashboard',
icon: <DashboardIcon />,
},
{
segment: 'orders',
title: 'Orders',
icon: <ShoppingCartIcon />,
pattern: '/orders{/:orderId}*',
},
]}
router={router}
theme={demoTheme}
window={demoWindow}
>
<DashboardLayout>
<DemoPageContent pathname={pathname} navigate={navigate} />
</DashboardLayout>
</AppProvider>
// preview-end
);
}

DashboardLayoutPattern.propTypes = {
/**
* Injected by the documentation to work in an iframe.
* Remove this when copying and pasting into your project.
*/
window: PropTypes.func,
};

export default DashboardLayoutPattern;
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { createTheme } from '@mui/material/styles';
import DashboardIcon from '@mui/icons-material/Dashboard';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
import type { Router } from '@toolpad/core';

const demoTheme = createTheme({
cssVariables: {
colorSchemeSelector: 'data-toolpad-color-scheme',
},
colorSchemes: { light: true, dark: true },
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 600,
lg: 1200,
xl: 1536,
},
},
});

function DemoPageContent({
pathname,
navigate,
}: {
pathname: string;
navigate: (path: string | URL) => void;
}) {
return (
<Box
sx={{
py: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
}}
>
<Typography>
<p>Dashboard content for {pathname}</p>
</Typography>
{pathname.startsWith('/orders') ? (
<Stack direction="row" spacing={1} sx={{ pt: 1 }}>
<Button
onClick={() => {
navigate('/orders/1');
}}
>
Order 1
</Button>
<Button
onClick={() => {
navigate('/orders/2');
}}
>
Order 2
</Button>
<Button
onClick={() => {
navigate('/orders/3');
}}
>
Order 3
</Button>
</Stack>
) : null}
</Box>
);
}

interface DemoProps {
/**
* Injected by the documentation to work in an iframe.
* Remove this when copying and pasting into your project.
*/
window?: () => Window;
}

export default function DashboardLayoutPattern(props: DemoProps) {
const { window } = props;

const [pathname, setPathname] = React.useState('/orders');
const navigate = React.useCallback(
(path: string | URL) => setPathname(String(path)),
[],
);

const router = React.useMemo<Router>(() => {
return {
pathname,
searchParams: new URLSearchParams(),
navigate,
};
}, [pathname, navigate]);

// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;

return (
// preview-start
<AppProvider
navigation={[
{
segment: 'dashboard',
title: 'Dashboard',
icon: <DashboardIcon />,
},
{
segment: 'orders',
title: 'Orders',
icon: <ShoppingCartIcon />,
pattern: '/orders{/:orderId}*',
},
]}
router={router}
theme={demoTheme}
window={demoWindow}
>
<DashboardLayout>
<DemoPageContent pathname={pathname} navigate={navigate} />
</DashboardLayout>
</AppProvider>
// preview-end
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<AppProvider
navigation={[
{
segment: 'dashboard',
title: 'Dashboard',
icon: <DashboardIcon />,
},
{
segment: 'orders',
title: 'Orders',
icon: <ShoppingCartIcon />,
pattern: '/orders{/:orderId}*',
},
]}
router={router}
theme={demoTheme}
window={demoWindow}
>
<DashboardLayout>
<DemoPageContent pathname={pathname} navigate={navigate} />
</DashboardLayout>
</AppProvider>
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,23 @@ Nested navigation structures can be placed in the sidebar as items with the form

### Navigation Actions

Navigation links have an optional `action` prop that can be used to render any content on the right-side of the respective list item, such as badges with numbers, or buttons to toggle a popover menu.
Navigation links have an optional `action` prop to render any content on the right-side of the respective list item, such as badges with numbers, or buttons to toggle a popover menu.

{{"demo": "DashboardLayoutNavigationActions.js", "height": 400, "iframe": true}}

### Navigation Pattern Matching

Navigation links have an optional `pattern` prop to define a pattern to be matched for the item to be marked as selected.
This feature is built on top of the [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) library. Some examples:

- Constant path: `/orders`
- Dynamic segment: `/orders/:segment`
- Zero or more segments: `/orders{/:segment}*`
- One or more segments: `/orders{/:segment}+`
- Optional segment: `/orders{/:segment}+?`

{{"demo": "DashboardLayoutPattern.js", "height": 400, "iframe": true}}

## Account

The `DashboardLayout` comes integrated with the [`<Account />`](/toolpad/core/react-account/) component. It renders as an account management menu when a user is signed in – a `session` object is present – and a button when not.
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/toolpad/core/api/app-provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"navigation": {
"type": {
"name": "arrayOf",
"description": "Array&lt;{ action?: node, children?: Array&lt;object<br>&#124;&nbsp;{ kind: 'header', title: string }<br>&#124;&nbsp;{ kind: 'divider' }&gt;, icon?: node, kind?: 'page', segment?: string, title?: string }<br>&#124;&nbsp;{ kind: 'header', title: string }<br>&#124;&nbsp;{ kind: 'divider' }&gt;"
"description": "Array&lt;{ action?: node, children?: Array&lt;object<br>&#124;&nbsp;{ kind: 'header', title: string }<br>&#124;&nbsp;{ kind: 'divider' }&gt;, icon?: node, kind?: 'page', pattern?: string, segment?: string, title?: string }<br>&#124;&nbsp;{ kind: 'header', title: string }<br>&#124;&nbsp;{ kind: 'divider' }&gt;"
},
"default": "[]"
},
Expand Down
1 change: 1 addition & 0 deletions packages/toolpad-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@toolpad/utils": "workspace:*",
"client-only": "^0.0.1",
"invariant": "2.2.4",
"path-to-regexp": "6.2.2",
"prop-types": "15.8.1"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions packages/toolpad-core/src/AppProvider/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface NavigationPageItem {
segment?: string;
title?: string;
icon?: React.ReactNode;
pattern?: string;
action?: React.ReactNode;
children?: Navigation;
}
Expand Down Expand Up @@ -221,6 +222,7 @@ AppProvider.propTypes /* remove-proptypes */ = {
),
icon: PropTypes.node,
kind: PropTypes.oneOf(['page']),
pattern: PropTypes.string,
segment: PropTypes.string,
title: PropTypes.string,
}),
Expand Down
Loading

0 comments on commit eb60981

Please sign in to comment.