From 6c799924b99043560c5c50d63dac71b036840965 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:44:11 +0200 Subject: [PATCH 01/14] Make PageContainer customizable --- .../page-container/CustomPageContainer.js | 62 +++++++++++ .../page-container/CustomPageContainer.tsx | 66 ++++++++++++ .../CustomPageContainer.tsx.preview | 1 + .../TitleBreadcrumbsPageContainer.js | 8 +- .../TitleBreadcrumbsPageContainer.tsx | 8 +- .../page-container/page-container.md | 7 +- .../src/PageContainer/PageContainer.test.tsx | 33 ++++++ .../src/PageContainer/PageContainer.tsx | 97 +++-------------- packages/toolpad-core/src/index.ts | 2 + packages/toolpad-core/src/internals/demo.tsx | 50 +++++++++ .../toolpad-core/src/shared/navigation.tsx | 102 +++++++++++++++++- .../toolpad-core/src/useActivePage/index.ts | 1 + .../src/useActivePage/useActivePage.ts | 54 ++++++++++ 13 files changed, 393 insertions(+), 98 deletions(-) create mode 100644 docs/data/toolpad/core/components/page-container/CustomPageContainer.js create mode 100644 docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx create mode 100644 docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx.preview create mode 100644 packages/toolpad-core/src/useActivePage/index.ts create mode 100644 packages/toolpad-core/src/useActivePage/useActivePage.ts diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js new file mode 100644 index 00000000000..6b25087c47c --- /dev/null +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { PageContainer } from '@toolpad/core/PageContainer'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { DemoBrowser, Link, useDemoRouter } from '@toolpad/core/internals'; +import { useActivePage } from '@toolpad/core/useActivePage'; +import { useTheme } from '@mui/material/styles'; +import Box from '@mui/material/Box'; + +const NAVIGATION = [ + { + segment: 'inbox', + title: 'Orders', + pattern: '/inbox/:id', + }, +]; + +function Content({ router }) { + const id = Number(router.pathname.replace('/inbox/', '')); + + const title = `Item ${id}`; + + const activePage = useActivePage(); + const breadCrumbs = [ + ...(activePage?.breadCrumbs ?? []), + { title, path: `/inbox/${id}` }, + ]; + + return ( + // preview-start + + {/* preview-end */} + + previous + next + + + ); +} + +export default function CustomPageContainer() { + const router = useDemoRouter('/inbox/123'); + + const theme = useTheme(); + + let content = ( + + Item 123 + + ); + + if (router.pathname.startsWith('/inbox/')) { + content = ; + } + + return ( + + + {content} + + + ); +} diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx new file mode 100644 index 00000000000..ab40fc0d3a4 --- /dev/null +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { PageContainer } from '@toolpad/core/PageContainer'; +import { AppProvider, Router } from '@toolpad/core/AppProvider'; +import { DemoBrowser, Link, useDemoRouter } from '@toolpad/core/internals'; +import { useActivePage } from '@toolpad/core/useActivePage'; +import { useTheme } from '@mui/material/styles'; +import Box from '@mui/material/Box'; + +const NAVIGATION = [ + { + segment: 'inbox', + title: 'Orders', + pattern: '/inbox/:id', + }, +]; + +interface ContentProps { + router: Router; +} + +function Content({ router }: ContentProps) { + const id = Number(router.pathname.replace('/inbox/', '')); + + const title = `Item ${id}`; + + const activePage = useActivePage(); + const breadCrumbs = [ + ...(activePage?.breadCrumbs ?? []), + { title, path: `/inbox/${id}` }, + ]; + + return ( + // preview-start + + {/* preview-end */} + + previous + next + + + ); +} + +export default function CustomPageContainer() { + const router = useDemoRouter('/inbox/123'); + + const theme = useTheme(); + + let content = ( + + Item 123 + + ); + + if (router.pathname.startsWith('/inbox/')) { + content = ; + } + + return ( + + + {content} + + + ); +} diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx.preview b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx.preview new file mode 100644 index 00000000000..06545ed2211 --- /dev/null +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.js b/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.js index a90486b361e..f0604f19386 100644 --- a/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.js +++ b/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.js @@ -6,6 +6,7 @@ import { useTheme } from '@mui/material/styles'; import Paper from '@mui/material/Paper'; const NAVIGATION = [ + { segment: '', title: 'ACME' }, { segment: 'inbox', title: 'Home', @@ -24,12 +25,7 @@ export default function TitleBreadcrumbsPageContainer() { const theme = useTheme(); return ( - + diff --git a/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.tsx b/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.tsx index a90486b361e..f0604f19386 100644 --- a/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.tsx +++ b/docs/data/toolpad/core/components/page-container/TitleBreadcrumbsPageContainer.tsx @@ -6,6 +6,7 @@ import { useTheme } from '@mui/material/styles'; import Paper from '@mui/material/Paper'; const NAVIGATION = [ + { segment: '', title: 'ACME' }, { segment: 'inbox', title: 'Home', @@ -24,12 +25,7 @@ export default function TitleBreadcrumbsPageContainer() { const theme = useTheme(); return ( - + diff --git a/docs/data/toolpad/core/components/page-container/page-container.md b/docs/data/toolpad/core/components/page-container/page-container.md index a39e4161402..657c5b30f70 100644 --- a/docs/data/toolpad/core/components/page-container/page-container.md +++ b/docs/data/toolpad/core/components/page-container/page-container.md @@ -22,7 +22,6 @@ For example, under the following navigation structure: ```tsx { test('renders nested', async () => { const navigation = [ + { segment: '', title: 'ACME' }, { segment: 'home', title: 'Home', @@ -79,4 +80,36 @@ describe('PageContainer', () => { expect(within(breadCrumbs).getByText('Home')).toBeTruthy(); expect(within(breadCrumbs).getByText('Orders')).toBeTruthy(); }); + + test('renders dynamic correctly', async () => { + const user = await userEvent.setup(); + const router = { + pathname: '/orders/123', + searchParams: new URLSearchParams(), + navigate: vi.fn(), + }; + render( + + + , + ); + + const breadCrumbs = screen.getByRole('navigation', { name: 'breadcrumb' }); + + const homeLink = within(breadCrumbs).getByRole('link', { name: 'Home' }); + await user.click(homeLink); + + expect(router.navigate).toHaveBeenCalledWith('/', expect.objectContaining({})); + router.navigate.mockClear(); + + expect(within(breadCrumbs).getByText('Orders')).toBeTruthy(); + + expect(screen.getByText('Orders', { ignore: 'nav *' })); + }); }); diff --git a/packages/toolpad-core/src/PageContainer/PageContainer.tsx b/packages/toolpad-core/src/PageContainer/PageContainer.tsx index ba5f5052932..fd3b3f020a7 100644 --- a/packages/toolpad-core/src/PageContainer/PageContainer.tsx +++ b/packages/toolpad-core/src/PageContainer/PageContainer.tsx @@ -10,77 +10,16 @@ import useSlotProps from '@mui/utils/useSlotProps'; import { styled } from '@mui/material'; import { Link as ToolpadLink } from '../shared/Link'; import { PageContainerToolbar, PageContainerToolbarProps } from './PageContainerToolbar'; -import { NavigationContext, RouterContext } from '../shared/context'; -import { getItemTitle, isPageItem } from '../shared/navigation'; -import { NavigationItem, NavigationPageItem, Navigation } from '../AppProvider'; -import { useApplicationTitle } from '../shared/branding'; +import { getItemTitle } from '../shared/navigation'; +import { useActivePage } from '../useActivePage'; const PageContentHeader = styled('div')(({ theme }) => ({ display: 'flex', flexDirection: 'row', - jusifyCOntent: 'space-between', + jusifyContent: 'space-between', gap: theme.spacing(2), })); -const isRootPage = (item: NavigationItem) => isPageItem(item) && !item.segment; - -interface BreadCrumbItem extends NavigationPageItem { - path: string; -} - -function createPageLookup( - navigation: Navigation, - segments: BreadCrumbItem[] = [], - base = '', -): Map { - const result = new Map(); - - const resolveSegment = (segment?: string) => `${base}${segment ? `/${segment}` : ''}` || '/'; - - const root = navigation.find((item) => isRootPage(item)) as NavigationPageItem | undefined; - const rootCrumb = root ? { path: resolveSegment(''), ...root } : undefined; - - for (const item of navigation) { - if (!isPageItem(item)) { - continue; - } - - const isNonProdEnv = process.env.NODE_ENV !== 'production'; - - const path = resolveSegment(item.segment); - if (isNonProdEnv && result.has(path)) { - console.warn(`Duplicate path in navigation: ${path}`); - } - - const itemCrumb: BreadCrumbItem = { path, ...item }; - - const navigationSegments: BreadCrumbItem[] = [ - ...segments, - ...(rootCrumb && !isRootPage(item) ? [rootCrumb] : []), - itemCrumb, - ]; - - result.set(path, navigationSegments); - - if (item.children) { - const childrenLookup = createPageLookup(item.children, navigationSegments, path); - for (const [childPath, childItems] of childrenLookup) { - if (isNonProdEnv && result.has(childPath)) { - console.warn(`Duplicate path in navigation: ${childPath}`); - } - result.set(childPath, childItems); - } - } - } - - return result; -} - -function matchPath(navigation: Navigation, path: string): BreadCrumbItem[] | null { - const lookup = createPageLookup(navigation); - return lookup.get(path) ?? null; -} - export interface PageContainerSlotProps { toolbar: PageContainerToolbarProps; } @@ -93,12 +32,19 @@ export interface PageContainerSlots { toolbar: React.ElementType; } +export interface BreadCrumb { + title: string; + path: string; +} + export interface PageContainerProps extends ContainerProps { children?: React.ReactNode; title?: string; + breadCrumbs?: BreadCrumb[]; slots?: PageContainerSlots; slotProps?: PageContainerSlotProps; } + /** * * Demos: @@ -111,28 +57,11 @@ export interface PageContainerProps extends ContainerProps { */ function PageContainer(props: PageContainerProps) { const { children, slots, slotProps, ...rest } = props; - const routerContext = React.useContext(RouterContext); - const navigationContext = React.useContext(NavigationContext); - const pathname = routerContext?.pathname ?? '/'; - const applicationTitle = useApplicationTitle(); - const breadCrumbs = React.useMemo(() => { - let crumbs = matchPath(navigationContext, pathname) ?? []; - if (crumbs.length <= 0 || crumbs[0].path !== '/') { - crumbs = [ - { - segment: '', - path: '/', - title: applicationTitle, - }, - ...crumbs, - ]; - } - return crumbs; - }, [navigationContext, pathname, applicationTitle]); + const activePage = useActivePage(); - const title = - (breadCrumbs ? getItemTitle(breadCrumbs[breadCrumbs.length - 1]) : '') ?? props.title; + const breadCrumbs = props.breadCrumbs ?? activePage?.breadCrumbs ?? []; + const title = props.title ?? activePage?.title ?? ''; const ToolbarComponent = props?.slots?.toolbar ?? PageContainerToolbar; const toolbarSlotProps = useSlotProps({ diff --git a/packages/toolpad-core/src/index.ts b/packages/toolpad-core/src/index.ts index c8ff0dade87..dcb98090703 100644 --- a/packages/toolpad-core/src/index.ts +++ b/packages/toolpad-core/src/index.ts @@ -8,6 +8,8 @@ export * from './Account'; export * from './PageContainer'; +export * from './useActivePage'; + export * from './useDialogs'; export * from './useNotifications'; diff --git a/packages/toolpad-core/src/internals/demo.tsx b/packages/toolpad-core/src/internals/demo.tsx index 0b41c363166..bb7fe12105a 100644 --- a/packages/toolpad-core/src/internals/demo.tsx +++ b/packages/toolpad-core/src/internals/demo.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Router } from '../AppProvider'; /** @@ -30,3 +31,52 @@ export function useDemoRouter(initialUrl: string = '/') { return router; } + +const BrowserRoot = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + alignItems: 'stretch', + borderRadius: theme.shape.borderRadius, + overflow: 'hidden', +})); + +const BrowserBar = styled('div')(({ theme }) => ({ + padding: theme.spacing(1), + backgroundColor: theme.palette.grey[400], +})); + +const UrlField = styled('div')(({ theme }) => ({ + fontSize: theme.typography.caption.fontSize, + color: 'black', + padding: '0.1rem 12px', + backgroundColor: 'white', + borderRadius: '1em', +})); + +const BrowserContent = styled('div')(({ theme }) => ({ + flex: 1, + border: `1px solid ${theme.palette.divider}`, + borderTop: 0, + backgroundColor: theme.palette.background.default, +})); + +export interface DemoBrowserProps { + router: Router; + children?: React.ReactNode; +} + +export function DemoBrowser({ router, children }: DemoBrowserProps) { + const search = router.searchParams.toString(); + return ( + + + {router.pathname + (search ? `?${search}` : '')} + + {children} + + ); +} + +export { Link } from '../shared/Link'; diff --git a/packages/toolpad-core/src/shared/navigation.tsx b/packages/toolpad-core/src/shared/navigation.tsx index 851f7902af2..38c14af4a8d 100644 --- a/packages/toolpad-core/src/shared/navigation.tsx +++ b/packages/toolpad-core/src/shared/navigation.tsx @@ -1,5 +1,11 @@ import { pathToRegexp } from 'path-to-regexp'; -import type { NavigationItem, NavigationPageItem, NavigationSubheaderItem } from '../AppProvider'; +import invariant from 'invariant'; +import type { + Navigation, + NavigationItem, + NavigationPageItem, + NavigationSubheaderItem, +} from '../AppProvider'; export const getItemKind = (item: NavigationItem) => item.kind ?? 'page'; @@ -51,3 +57,97 @@ export function hasSelectedNavigationChildren( return false; } + +// maps navigation items to their full path +function buildItemToPathMap(navigation: Navigation): Map { + const map = new Map(); + + const visit = (item: NavigationItem, base: string) => { + if (isPageItem(item)) { + const path = `${base}${item.segment ? `/${item.segment}` : ''}` || '/'; + map.set(item, path); + if (item.children) { + for (const child of item.children) { + visit(child, path); + } + } + } + }; + + for (const item of navigation) { + visit(item, ''); + } + + return map; +} + +const itemToPathMapCache = new WeakMap>(); +function getItemToPathMap(navigation: Navigation) { + let map = itemToPathMapCache.get(navigation); + if (!map) { + map = buildItemToPathMap(navigation); + itemToPathMapCache.set(navigation, map); + } + return map; +} + +function buildItemLookup(navigation: Navigation) { + const map = new Map(); + const visit = (item: NavigationItem) => { + if (isPageItem(item)) { + const path = getItemPath(navigation, item); + if (map.has(path)) { + console.warn(`Duplicate path in navigation: ${path}`); + } + map.set(path, item); + if (item.pattern) { + map.set(pathToRegexp(item.pattern), item); + } + if (item.children) { + for (const child of item.children) { + visit(child); + } + } + } + }; + for (const item of navigation) { + visit(item); + } + return map; +} +const itemLookupMapCache = new WeakMap>(); +function getItemLookup(navigation: Navigation) { + let map = itemLookupMapCache.get(navigation); + if (!map) { + map = buildItemLookup(navigation); + itemLookupMapCache.set(navigation, map); + } + return map; +} + +export function matchPath(navigation: Navigation, path: string): NavigationPageItem | null { + const lookup = getItemLookup(navigation); + + for (const [key, item] of lookup.entries()) { + if (typeof key === 'string' && key === path) { + return item; + } + if (key instanceof RegExp && key.test(path)) { + return item; + } + } + + return null; +} + +export function getItemByPath(navigation: Navigation, path: string): NavigationPageItem | null { + const map = getItemLookup(navigation); + return map.get(path) ?? null; +} + +export function getItemPath(navigation: Navigation, item: NavigationPageItem): string { + const map = getItemToPathMap(navigation); + const path = map.get(item); + invariant(path, `Item not found in navigation: ${item.title}`); + return path; +} diff --git a/packages/toolpad-core/src/useActivePage/index.ts b/packages/toolpad-core/src/useActivePage/index.ts new file mode 100644 index 00000000000..52662dbd290 --- /dev/null +++ b/packages/toolpad-core/src/useActivePage/index.ts @@ -0,0 +1 @@ +export * from './useActivePage'; diff --git a/packages/toolpad-core/src/useActivePage/useActivePage.ts b/packages/toolpad-core/src/useActivePage/useActivePage.ts new file mode 100644 index 00000000000..e1b6a9762d5 --- /dev/null +++ b/packages/toolpad-core/src/useActivePage/useActivePage.ts @@ -0,0 +1,54 @@ +'use client'; +import * as React from 'react'; +import { NavigationContext, RouterContext } from '../shared/context'; +import { getItemPath, getItemTitle, matchPath } from '../shared/navigation'; +import type { BreadCrumb } from '../PageContainer'; + +export function useActivePage() { + const navigationContext = React.useContext(NavigationContext); + const routerContext = React.useContext(RouterContext); + const pathname = routerContext?.pathname ?? '/'; + const activeItem = matchPath(navigationContext, pathname); + + const rootItem = matchPath(navigationContext, '/'); + + return React.useMemo(() => { + if (!activeItem) { + return null; + } + + const breadCrumbs: BreadCrumb[] = []; + + if (rootItem) { + breadCrumbs.push({ + title: getItemTitle(rootItem), + path: '/', + }); + } + + const segments = pathname.split('/').filter(Boolean); + let prefix = ''; + for (const segment of segments) { + const path = `${prefix}/${segment}`; + prefix = path; + const item = matchPath(navigationContext, path); + if (!item) { + continue; + } + const itemPath = getItemPath(navigationContext, item); + const lastCrumb = breadCrumbs[breadCrumbs.length - 1]; + if (lastCrumb?.path !== itemPath) { + breadCrumbs.push({ + title: getItemTitle(item), + path: itemPath, + }); + } + } + + return { + title: getItemTitle(activeItem), + path: getItemPath(navigationContext, activeItem), + breadCrumbs, + }; + }, [activeItem, rootItem, pathname, navigationContext]); +} From 5affccc5967085872af839fd1cc6e9bccd320dce Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:51:54 +0200 Subject: [PATCH 02/14] fix demo --- .../page-container/CustomPageContainer.js | 35 +++++++++++--- .../page-container/CustomPageContainer.tsx | 11 ++--- packages/toolpad-core/src/internals/demo.tsx | 48 ------------------- 3 files changed, 34 insertions(+), 60 deletions(-) diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js index 6b25087c47c..583e3a37fa3 100644 --- a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js @@ -1,10 +1,12 @@ import * as React from 'react'; +import PropTypes from 'prop-types'; import { PageContainer } from '@toolpad/core/PageContainer'; import { AppProvider } from '@toolpad/core/AppProvider'; -import { DemoBrowser, Link, useDemoRouter } from '@toolpad/core/internals'; +import { Link, useDemoRouter } from '@toolpad/core/internals'; import { useActivePage } from '@toolpad/core/useActivePage'; import { useTheme } from '@mui/material/styles'; import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; const NAVIGATION = [ { @@ -37,6 +39,29 @@ function Content({ router }) { ); } +Content.propTypes = { + router: PropTypes.shape({ + navigate: PropTypes.func.isRequired, + pathname: PropTypes.string.isRequired, + searchParams: PropTypes.shape({ + '__@iterator@334': PropTypes.func.isRequired, + append: PropTypes.func.isRequired, + delete: PropTypes.func.isRequired, + entries: PropTypes.func.isRequired, + forEach: PropTypes.func.isRequired, + get: PropTypes.func.isRequired, + getAll: PropTypes.func.isRequired, + has: PropTypes.func.isRequired, + keys: PropTypes.func.isRequired, + set: PropTypes.func.isRequired, + size: PropTypes.number.isRequired, + sort: PropTypes.func.isRequired, + toString: PropTypes.func.isRequired, + values: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; + export default function CustomPageContainer() { const router = useDemoRouter('/inbox/123'); @@ -53,10 +78,8 @@ export default function CustomPageContainer() { } return ( - - - {content} - - + + {content} + ); } diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx index ab40fc0d3a4..546a45f0a29 100644 --- a/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import { PageContainer } from '@toolpad/core/PageContainer'; import { AppProvider, Router } from '@toolpad/core/AppProvider'; -import { DemoBrowser, Link, useDemoRouter } from '@toolpad/core/internals'; +import { Link, useDemoRouter } from '@toolpad/core/internals'; import { useActivePage } from '@toolpad/core/useActivePage'; import { useTheme } from '@mui/material/styles'; import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; const NAVIGATION = [ { @@ -57,10 +58,8 @@ export default function CustomPageContainer() { } return ( - - - {content} - - + + {content} + ); } diff --git a/packages/toolpad-core/src/internals/demo.tsx b/packages/toolpad-core/src/internals/demo.tsx index bb7fe12105a..bdda55216fe 100644 --- a/packages/toolpad-core/src/internals/demo.tsx +++ b/packages/toolpad-core/src/internals/demo.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { styled } from '@mui/material/styles'; import { Router } from '../AppProvider'; /** @@ -32,51 +31,4 @@ export function useDemoRouter(initialUrl: string = '/') { return router; } -const BrowserRoot = styled('div')(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - width: '100%', - height: '100%', - alignItems: 'stretch', - borderRadius: theme.shape.borderRadius, - overflow: 'hidden', -})); - -const BrowserBar = styled('div')(({ theme }) => ({ - padding: theme.spacing(1), - backgroundColor: theme.palette.grey[400], -})); - -const UrlField = styled('div')(({ theme }) => ({ - fontSize: theme.typography.caption.fontSize, - color: 'black', - padding: '0.1rem 12px', - backgroundColor: 'white', - borderRadius: '1em', -})); - -const BrowserContent = styled('div')(({ theme }) => ({ - flex: 1, - border: `1px solid ${theme.palette.divider}`, - borderTop: 0, - backgroundColor: theme.palette.background.default, -})); - -export interface DemoBrowserProps { - router: Router; - children?: React.ReactNode; -} - -export function DemoBrowser({ router, children }: DemoBrowserProps) { - const search = router.searchParams.toString(); - return ( - - - {router.pathname + (search ? `?${search}` : '')} - - {children} - - ); -} - export { Link } from '../shared/Link'; From 7aef586864bbbc8c2725cbcc27e30ed0a867c807 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:57:20 +0200 Subject: [PATCH 03/14] gfhfg --- .../page-container/CustomPageContainer.js | 14 +++++++++----- .../page-container/CustomPageContainer.tsx | 14 +++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js index 583e3a37fa3..47619a443fb 100644 --- a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js @@ -7,6 +7,7 @@ import { useActivePage } from '@toolpad/core/useActivePage'; import { useTheme } from '@mui/material/styles'; import Box from '@mui/material/Box'; import Paper from '@mui/material/Paper'; +import invariant from 'invariant'; const NAVIGATION = [ { @@ -19,13 +20,16 @@ const NAVIGATION = [ function Content({ router }) { const id = Number(router.pathname.replace('/inbox/', '')); + const activePage = useActivePage(); + invariant( + activePage, + 'This component must be used on a page within the app navigation', + ); + const title = `Item ${id}`; + const path = `${activePage.path}/${id}`; - const activePage = useActivePage(); - const breadCrumbs = [ - ...(activePage?.breadCrumbs ?? []), - { title, path: `/inbox/${id}` }, - ]; + const breadCrumbs = [...activePage.breadCrumbs, { title, path }]; return ( // preview-start diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx index 546a45f0a29..60c02065007 100644 --- a/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx @@ -6,6 +6,7 @@ import { useActivePage } from '@toolpad/core/useActivePage'; import { useTheme } from '@mui/material/styles'; import Box from '@mui/material/Box'; import Paper from '@mui/material/Paper'; +import invariant from 'invariant'; const NAVIGATION = [ { @@ -22,13 +23,16 @@ interface ContentProps { function Content({ router }: ContentProps) { const id = Number(router.pathname.replace('/inbox/', '')); + const activePage = useActivePage(); + invariant( + activePage, + 'This component must be used on a page within the app navigation', + ); + const title = `Item ${id}`; + const path = `${activePage.path}/${id}`; - const activePage = useActivePage(); - const breadCrumbs = [ - ...(activePage?.breadCrumbs ?? []), - { title, path: `/inbox/${id}` }, - ]; + const breadCrumbs = [...activePage.breadCrumbs, { title, path }]; return ( // preview-start From 6c3dcc918a845aa097ece742dbdfddc51eb1cc8f Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:05:54 +0200 Subject: [PATCH 04/14] frwgrw --- .../page-container/CustomPageContainer.js | 5 +---- .../page-container/CustomPageContainer.tsx | 5 +---- .../page-container/page-container.md | 20 ++++++++++++++++++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js index 47619a443fb..6e2a8db1b1d 100644 --- a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js @@ -21,10 +21,7 @@ function Content({ router }) { const id = Number(router.pathname.replace('/inbox/', '')); const activePage = useActivePage(); - invariant( - activePage, - 'This component must be used on a page within the app navigation', - ); + invariant(activePage, 'No navigation match'); const title = `Item ${id}`; const path = `${activePage.path}/${id}`; diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx index 60c02065007..3b913515a49 100644 --- a/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.tsx @@ -24,10 +24,7 @@ function Content({ router }: ContentProps) { const id = Number(router.pathname.replace('/inbox/', '')); const activePage = useActivePage(); - invariant( - activePage, - 'This component must be used on a page within the app navigation', - ); + invariant(activePage, 'No navigation match'); const title = `Item ${id}`; const path = `${activePage.path}/${id}`; diff --git a/docs/data/toolpad/core/components/page-container/page-container.md b/docs/data/toolpad/core/components/page-container/page-container.md index 657c5b30f70..153c0986b77 100644 --- a/docs/data/toolpad/core/components/page-container/page-container.md +++ b/docs/data/toolpad/core/components/page-container/page-container.md @@ -45,10 +45,28 @@ The breadcrumbs contains **ACME / Home / Orders** when you visit the path **/hom ## Dynamic Routes -When you use the `PageContainer` on a dynamic route, you'll likely want to set a title and breadcrumbs belonging to the specific path. You can achieve this with the `title` and `breadCrumbs` property of the `PageContainer`: +When you use the `PageContainer` on a dynamic route, you'll likely want to set a title and breadcrumbs belonging to the specific path. You can achieve this with the `title` and `breadCrumbs` property of the `PageContainer` {{"demo": "CustomPageContainer.js", "height": 300}} +You can use the `useActivePage` hook to retrieve the title and breadcrumbs of the active page. This way you can extend the existing values. + +```tsx +import { useActivePage } from '@toolpad/core/useActivePage'; +import { BreadCrumb } from '@toolpad/core/PageContainer'; + +// Pass the id from your router of choice +function useDynamicBreadCrumbs(id: string): BreadCrumb[] { + const activePage = useActivePage(); + invariant(activePage, 'No navigation match'); + + const title = `Item ${id}`; + const path = `${activePage.path}/${id}`; + + return [...activePage.breadCrumbs, { title, path }]; +} +``` + ## Actions You can configure additional actions in the area that is reserved on the right. To do so provide the `toolbar` slot to the `PageContainer` component. You can wrap the `PageContainerToolbar` component to create a custom toolbar component, as shown here: From d7a762481638ec1b3cce102cf77c442d68df2f64 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:07:01 +0200 Subject: [PATCH 05/14] Update PageContainer.tsx --- .../toolpad-core/src/PageContainer/PageContainer.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/toolpad-core/src/PageContainer/PageContainer.tsx b/packages/toolpad-core/src/PageContainer/PageContainer.tsx index fd3b3f020a7..048d3752888 100644 --- a/packages/toolpad-core/src/PageContainer/PageContainer.tsx +++ b/packages/toolpad-core/src/PageContainer/PageContainer.tsx @@ -113,6 +113,15 @@ PageContainer.propTypes /* remove-proptypes */ = { // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + breadCrumbs: PropTypes.arrayOf( + PropTypes.shape({ + path: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + }), + ), /** * @ignore */ From 06c6f76881db91c4dc41c6379356f8df8659ef1f Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:12:49 +0200 Subject: [PATCH 06/14] Update navigation.tsx --- .../toolpad-core/src/shared/navigation.tsx | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/toolpad-core/src/shared/navigation.tsx b/packages/toolpad-core/src/shared/navigation.tsx index 38c14af4a8d..f986a3fd67b 100644 --- a/packages/toolpad-core/src/shared/navigation.tsx +++ b/packages/toolpad-core/src/shared/navigation.tsx @@ -58,7 +58,10 @@ export function hasSelectedNavigationChildren( return false; } -// maps navigation items to their full path +/** + * Builds a map of navigation page items to their respective paths. This map is used to quickly + * lookup the path of a navigation item. It will be cached for the lifetime of the navigation. + */ function buildItemToPathMap(navigation: Navigation): Map { const map = new Map(); @@ -82,6 +85,10 @@ function buildItemToPathMap(navigation: Navigation): Map>(); + +/** + * Gets the cached map of navigation page items to their respective paths. + */ function getItemToPathMap(navigation: Navigation) { let map = itemToPathMapCache.get(navigation); if (!map) { @@ -91,6 +98,10 @@ function getItemToPathMap(navigation: Navigation) { return map; } +/** + * Build a lookup map of paths to navigation items. This map is used to match paths against + * to find the active page. + */ function buildItemLookup(navigation: Navigation) { const map = new Map(); const visit = (item: NavigationItem) => { @@ -125,6 +136,10 @@ function getItemLookup(navigation: Navigation) { return map; } +/** + * Matches a path against the navigation to find the active page. i.e. the page that should be + * marked as selected in the navigation. + */ export function matchPath(navigation: Navigation, path: string): NavigationPageItem | null { const lookup = getItemLookup(navigation); @@ -140,11 +155,9 @@ export function matchPath(navigation: Navigation, path: string): NavigationPageI return null; } -export function getItemByPath(navigation: Navigation, path: string): NavigationPageItem | null { - const map = getItemLookup(navigation); - return map.get(path) ?? null; -} - +/** + * Gets the path for a specific navigation page item. + */ export function getItemPath(navigation: Navigation, item: NavigationPageItem): string { const map = getItemToPathMap(navigation); const path = map.get(item); From db632e9759f252bfb74611fd50d66c0a80a73d33 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:16:28 +0200 Subject: [PATCH 07/14] Update pnpm-lock.yaml --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9d41f31f46..b89431a8b94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,7 +195,7 @@ importers: version: 7.35.2(eslint@8.57.0) eslint-plugin-react-compiler: specifier: latest - version: 0.0.0-experimental-ca16900-20240916(eslint@8.57.0) + version: 0.0.0-experimental-7670337-20240918(eslint@8.57.0) eslint-plugin-react-hooks: specifier: 4.6.2 version: 4.6.2(eslint@8.57.0) @@ -5824,8 +5824,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@0.0.0-experimental-ca16900-20240916: - resolution: {integrity: sha512-T3iL6Veei4EMNdBtn9p/kUDlXFZAXVQ0scfF1hOjJE88Lmlg7f1zHRAcGQ4Z6ClVPnOikZ1/yGNB4l/ui47Anw==} + eslint-plugin-react-compiler@0.0.0-experimental-7670337-20240918: + resolution: {integrity: sha512-FZXoYqGK3BiJw2BFQIUL0C9GgEF2QS+y6oiVPm4GooX6SXrg/UVtCi1nMAaBhqTpsF/KF2RjobgePY8KgbqXow==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -15598,7 +15598,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@0.0.0-experimental-ca16900-20240916(eslint@8.57.0): + eslint-plugin-react-compiler@0.0.0-experimental-7670337-20240918(eslint@8.57.0): dependencies: '@babel/core': 7.25.2 '@babel/parser': 7.25.6 From 8bafeb910b19b6341fbea646c9d434e1045a3b6b Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:38:22 +0200 Subject: [PATCH 08/14] Update CustomPageContainer.js --- .../core/components/page-container/CustomPageContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js index 6e2a8db1b1d..78b14b2bcc5 100644 --- a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js @@ -45,7 +45,7 @@ Content.propTypes = { navigate: PropTypes.func.isRequired, pathname: PropTypes.string.isRequired, searchParams: PropTypes.shape({ - '__@iterator@334': PropTypes.func.isRequired, + '__@iterator@271': PropTypes.func.isRequired, append: PropTypes.func.isRequired, delete: PropTypes.func.isRequired, entries: PropTypes.func.isRequired, From 35affe3e401e220b91ae0a36bd5149f8f6040c92 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:07:27 +0200 Subject: [PATCH 09/14] sdfs --- .../page-container/CustomPageContainer.js | 17 +------------- package.json | 2 +- .../src/AppProvider/AppProvider.tsx | 4 ++-- pnpm-lock.yaml | 22 ++++++++++++++----- 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js index 78b14b2bcc5..a792a7df846 100644 --- a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js @@ -44,22 +44,7 @@ Content.propTypes = { router: PropTypes.shape({ navigate: PropTypes.func.isRequired, pathname: PropTypes.string.isRequired, - searchParams: PropTypes.shape({ - '__@iterator@271': PropTypes.func.isRequired, - append: PropTypes.func.isRequired, - delete: PropTypes.func.isRequired, - entries: PropTypes.func.isRequired, - forEach: PropTypes.func.isRequired, - get: PropTypes.func.isRequired, - getAll: PropTypes.func.isRequired, - has: PropTypes.func.isRequired, - keys: PropTypes.func.isRequired, - set: PropTypes.func.isRequired, - size: PropTypes.number.isRequired, - sort: PropTypes.func.isRequired, - toString: PropTypes.func.isRequired, - values: PropTypes.func.isRequired, - }).isRequired, + searchParams: PropTypes.instanceOf(URLSearchParams).isRequired, }).isRequired, }; diff --git a/package.json b/package.json index 35a1d598f67..d7bff36d351 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@mui/internal-babel-plugin-resolve-imports": "1.0.18", "@mui/internal-docs-utils": "1.0.13", "@mui/internal-markdown": "1.0.14", - "@mui/internal-scripts": "1.0.21", + "@mui/internal-scripts": "https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts", "@mui/monorepo": "github:mui/material-ui#3732be30e0ae3287b55dc6a96cf1ffcfdc194906", "@mui/x-charts": "7.17.0", "@next/eslint-plugin-next": "14.2.11", diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.tsx index 029960cdadc..ba183f2bc88 100644 --- a/packages/toolpad-core/src/AppProvider/AppProvider.tsx +++ b/packages/toolpad-core/src/AppProvider/AppProvider.tsx @@ -239,10 +239,10 @@ AppProvider.propTypes /* remove-proptypes */ = { * Router implementation used inside Toolpad components. * @default null */ - router: PropTypes /* @typescript-to-proptypes-ignore */.shape({ + router: PropTypes.shape({ navigate: PropTypes.func.isRequired, pathname: PropTypes.string.isRequired, - searchParams: PropTypes.instanceOf(URLSearchParams), + searchParams: PropTypes.instanceOf(URLSearchParams).isRequired, }), /** * Session info about the current user. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0b4090e51f..9471d5a97d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,8 +98,8 @@ importers: specifier: 1.0.14 version: 1.0.14 '@mui/internal-scripts': - specifier: 1.0.21 - version: 1.0.21 + specifier: https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts + version: https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts '@mui/monorepo': specifier: github:mui/material-ui#3732be30e0ae3287b55dc6a96cf1ffcfdc194906 version: https://codeload.github.com/mui/material-ui/tar.gz/3732be30e0ae3287b55dc6a96cf1ffcfdc194906(encoding@0.1.13) @@ -2849,11 +2849,16 @@ packages: '@mui/internal-docs-utils@1.0.13': resolution: {integrity: sha512-q+EVFiJGqAbNtYQrRqRiE6EEtVyIgEPfM0QpMJ5ws/QY8wZterExWHLbtBP5eX60fIXqaVccfMBro8RPaeuvDQ==} + '@mui/internal-docs-utils@https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-docs-utils': + resolution: {tarball: https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-docs-utils} + version: 1.0.13 + '@mui/internal-markdown@1.0.14': resolution: {integrity: sha512-Df8Uo54TyE7Lm3otWWxw2OQzv3ztbdhRJMJ2sCYrTDqluAvGe/65KpVr95/AcY/An3qWpvBrbn5k5Oj/NG/lXw==} - '@mui/internal-scripts@1.0.21': - resolution: {integrity: sha512-nVjt+Yjjff83C+urxDD63LmUhIR1bMbGVexHdFq1v9odyVKXw3tgL1yiCoEETLUfOkBbXECmjuRcbw+meWu5+g==} + '@mui/internal-scripts@https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts': + resolution: {tarball: https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts} + version: 1.0.21 '@mui/joy@5.0.0-beta.48': resolution: {integrity: sha512-OhTvjuGl9I5IvpBr0BQyDehIW/xb2yteW6YglHJMdOb/279nItn76X1NBtPV9ImldNlBjReGwvpOXmBTTGER9w==} @@ -12033,6 +12038,11 @@ snapshots: rimraf: 6.0.1 typescript: 5.6.2 + '@mui/internal-docs-utils@https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-docs-utils': + dependencies: + rimraf: 6.0.1 + typescript: 5.6.2 + '@mui/internal-markdown@1.0.14': dependencies: '@babel/runtime': 7.25.6 @@ -12040,14 +12050,14 @@ snapshots: marked: 13.0.3 prismjs: 1.29.0 - '@mui/internal-scripts@1.0.21': + '@mui/internal-scripts@https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts': dependencies: '@babel/core': 7.25.2 '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) '@babel/plugin-syntax-typescript': 7.25.4(@babel/core@7.25.2) '@babel/types': 7.25.6 - '@mui/internal-docs-utils': 1.0.13 + '@mui/internal-docs-utils': https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-docs-utils doctrine: 3.0.0 lodash: 4.17.21 typescript: 5.6.2 From b22f4cb708a53c1f513a1a16a8ec42b7208e0696 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:14:36 +0200 Subject: [PATCH 10/14] kfjwh --- package.json | 2 +- pnpm-lock.yaml | 22 ++++++---------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index d7bff36d351..eabb25451d2 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@mui/internal-babel-plugin-resolve-imports": "1.0.18", "@mui/internal-docs-utils": "1.0.13", "@mui/internal-markdown": "1.0.14", - "@mui/internal-scripts": "https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts", + "@mui/internal-scripts": "1.0.21-dev.20240919-130050-82a6448768", "@mui/monorepo": "github:mui/material-ui#3732be30e0ae3287b55dc6a96cf1ffcfdc194906", "@mui/x-charts": "7.17.0", "@next/eslint-plugin-next": "14.2.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9471d5a97d2..0824f1f9da0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,8 +98,8 @@ importers: specifier: 1.0.14 version: 1.0.14 '@mui/internal-scripts': - specifier: https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts - version: https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts + specifier: 1.0.21-dev.20240919-130050-82a6448768 + version: 1.0.21-dev.20240919-130050-82a6448768 '@mui/monorepo': specifier: github:mui/material-ui#3732be30e0ae3287b55dc6a96cf1ffcfdc194906 version: https://codeload.github.com/mui/material-ui/tar.gz/3732be30e0ae3287b55dc6a96cf1ffcfdc194906(encoding@0.1.13) @@ -2849,16 +2849,11 @@ packages: '@mui/internal-docs-utils@1.0.13': resolution: {integrity: sha512-q+EVFiJGqAbNtYQrRqRiE6EEtVyIgEPfM0QpMJ5ws/QY8wZterExWHLbtBP5eX60fIXqaVccfMBro8RPaeuvDQ==} - '@mui/internal-docs-utils@https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-docs-utils': - resolution: {tarball: https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-docs-utils} - version: 1.0.13 - '@mui/internal-markdown@1.0.14': resolution: {integrity: sha512-Df8Uo54TyE7Lm3otWWxw2OQzv3ztbdhRJMJ2sCYrTDqluAvGe/65KpVr95/AcY/An3qWpvBrbn5k5Oj/NG/lXw==} - '@mui/internal-scripts@https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts': - resolution: {tarball: https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts} - version: 1.0.21 + '@mui/internal-scripts@1.0.21-dev.20240919-130050-82a6448768': + resolution: {integrity: sha512-feyp1s9pHvSOdO+ERv8oPGp2qGUCnngTt0+Yb+yTBo1tH8DN1Ks5JA7gIQR/elTnDusrbBQPpI7IAG5ZPrYlzQ==} '@mui/joy@5.0.0-beta.48': resolution: {integrity: sha512-OhTvjuGl9I5IvpBr0BQyDehIW/xb2yteW6YglHJMdOb/279nItn76X1NBtPV9ImldNlBjReGwvpOXmBTTGER9w==} @@ -12038,11 +12033,6 @@ snapshots: rimraf: 6.0.1 typescript: 5.6.2 - '@mui/internal-docs-utils@https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-docs-utils': - dependencies: - rimraf: 6.0.1 - typescript: 5.6.2 - '@mui/internal-markdown@1.0.14': dependencies: '@babel/runtime': 7.25.6 @@ -12050,14 +12040,14 @@ snapshots: marked: 13.0.3 prismjs: 1.29.0 - '@mui/internal-scripts@https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-scripts': + '@mui/internal-scripts@1.0.21-dev.20240919-130050-82a6448768': dependencies: '@babel/core': 7.25.2 '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) '@babel/plugin-syntax-typescript': 7.25.4(@babel/core@7.25.2) '@babel/types': 7.25.6 - '@mui/internal-docs-utils': https://pkg.csb.dev/mui/material-ui/commit/9bd6acae/@mui/internal-docs-utils + '@mui/internal-docs-utils': 1.0.13 doctrine: 3.0.0 lodash: 4.17.21 typescript: 5.6.2 From 05a8b50293174fbf6e8625c3f49b7f8db7def3bc Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:34:29 +0200 Subject: [PATCH 11/14] Update app-provider.json --- docs/pages/toolpad/core/api/app-provider.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/toolpad/core/api/app-provider.json b/docs/pages/toolpad/core/api/app-provider.json index 222066ae3ea..27ff3b8a3f4 100644 --- a/docs/pages/toolpad/core/api/app-provider.json +++ b/docs/pages/toolpad/core/api/app-provider.json @@ -19,7 +19,7 @@ "router": { "type": { "name": "shape", - "description": "{ navigate: func, pathname: string, searchParams?: URLSearchParams }" + "description": "{ navigate: func, pathname: string, searchParams: URLSearchParams }" }, "default": "null" }, From 8ab84c43b8bb716c3317a6c183ac30525831d347 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:39:05 +0200 Subject: [PATCH 12/14] comments --- .../toolpad/core/api/page-container.json | 12 ++++++++- .../page-container/page-container.json | 11 ++++++-- .../src/PageContainer/PageContainer.tsx | 27 ++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/docs/pages/toolpad/core/api/page-container.json b/docs/pages/toolpad/core/api/page-container.json index 82219960b02..79293eba86a 100644 --- a/docs/pages/toolpad/core/api/page-container.json +++ b/docs/pages/toolpad/core/api/page-container.json @@ -1,5 +1,15 @@ { - "props": {}, + "props": { + "breadCrumbs": { + "type": { "name": "arrayOf", "description": "Array<{ path: string, title: string }>" } + }, + "slotProps": { "type": { "name": "shape", "description": "{ toolbar: { children?: node } }" } }, + "slots": { + "type": { "name": "shape", "description": "{ toolbar?: elementType }" }, + "additionalInfo": { "slotsApi": true } + }, + "title": { "type": { "name": "string" } } + }, "name": "PageContainer", "imports": [ "import { PageContainer } from '@toolpad/core/PageContainer';", diff --git a/docs/translations/api-docs/page-container/page-container.json b/docs/translations/api-docs/page-container/page-container.json index 643fe8e0f28..39823940d46 100644 --- a/docs/translations/api-docs/page-container/page-container.json +++ b/docs/translations/api-docs/page-container/page-container.json @@ -1,6 +1,13 @@ { - "componentDescription": "", - "propDescriptions": {}, + "componentDescription": "A container component to provide a title and breadcrumbs for your pages.", + "propDescriptions": { + "breadCrumbs": { + "description": "The breadcrumbs of the page. Leave blank to use the active page breadcrumbs." + }, + "slotProps": { "description": "The props used for each slot inside." }, + "slots": { "description": "The components used for each slot inside." }, + "title": { "description": "The title of the page. Leave blank to use the active page title." } + }, "classDescriptions": { "disableGutters": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", diff --git a/packages/toolpad-core/src/PageContainer/PageContainer.tsx b/packages/toolpad-core/src/PageContainer/PageContainer.tsx index 048d3752888..7ff87f6caf2 100644 --- a/packages/toolpad-core/src/PageContainer/PageContainer.tsx +++ b/packages/toolpad-core/src/PageContainer/PageContainer.tsx @@ -33,19 +33,38 @@ export interface PageContainerSlots { } export interface BreadCrumb { + /** + * The title of the breadcrumb segment. + */ title: string; + /** + * The path the breadcrumb links to. + */ path: string; } export interface PageContainerProps extends ContainerProps { children?: React.ReactNode; + /** + * The title of the page. Leave blank to use the active page title. + */ title?: string; + /** + * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs. + */ breadCrumbs?: BreadCrumb[]; + /** + * The components used for each slot inside. + */ slots?: PageContainerSlots; + /** + * The props used for each slot inside. + */ slotProps?: PageContainerSlotProps; } /** + * A container component to provide a title and breadcrumbs for your pages. * * Demos: * @@ -114,7 +133,7 @@ PageContainer.propTypes /* remove-proptypes */ = { // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ // └─────────────────────────────────────────────────────────────────────┘ /** - * @ignore + * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs. */ breadCrumbs: PropTypes.arrayOf( PropTypes.shape({ @@ -127,7 +146,7 @@ PageContainer.propTypes /* remove-proptypes */ = { */ children: PropTypes.node, /** - * @ignore + * The props used for each slot inside. */ slotProps: PropTypes.shape({ toolbar: PropTypes.shape({ @@ -135,13 +154,13 @@ PageContainer.propTypes /* remove-proptypes */ = { }).isRequired, }), /** - * @ignore + * The components used for each slot inside. */ slots: PropTypes.shape({ toolbar: PropTypes.elementType, }), /** - * @ignore + * The title of the page. Leave blank to use the active page title. */ title: PropTypes.string, } as any; From 02c6b01f28307bf6bdfaad8991418cf6b7c54977 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Fri, 20 Sep 2024 08:43:26 +0200 Subject: [PATCH 13/14] Add note --- .../page-container/page-container.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/data/toolpad/core/components/page-container/page-container.md b/docs/data/toolpad/core/components/page-container/page-container.md index 153c0986b77..81cd357c368 100644 --- a/docs/data/toolpad/core/components/page-container/page-container.md +++ b/docs/data/toolpad/core/components/page-container/page-container.md @@ -67,6 +67,35 @@ function useDynamicBreadCrumbs(id: string): BreadCrumb[] { } ``` +For example, under the Next.js app router you would be able to obtain breadcrumbs for a dynamic route as follows: + +```tsx +// ./src/app/example/[id]/page.tsx +'use client'; + +import { useParams } from 'next/navigation'; +import { PageContainer } from '@toolpad/core/PageContainer'; +import invariant from 'invariant'; +import { useActivePage } from '@toolpad/core/useActivePage'; + +export default function Example() { + const params = useParams<{ id: string }>(); + const activePage = useActivePage(); + invariant(activePage, 'No navigation match'); + + const title = `Item ${params.id}`; + const path = `${activePage.path}/${params.id}`; + + const breadCrumbs = [...activePage.breadCrumbs, { title, path }]; + + return ( + + ... + + ); +} +``` + ## Actions You can configure additional actions in the area that is reserved on the right. To do so provide the `toolbar` slot to the `PageContainer` component. You can wrap the `PageContainerToolbar` component to create a custom toolbar component, as shown here: From 7bbe5bfcfa770e29e2002ce89b91fc0239c52ab2 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:18:13 +0200 Subject: [PATCH 14/14] Update pnpm-lock.yaml --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0824f1f9da0..0af1141a20b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,7 +195,7 @@ importers: version: 7.35.2(eslint@8.57.0) eslint-plugin-react-compiler: specifier: latest - version: 0.0.0-experimental-7670337-20240918(eslint@8.57.0) + version: 0.0.0-experimental-92aaa43-20240919(eslint@8.57.0) eslint-plugin-react-hooks: specifier: 4.6.2 version: 4.6.2(eslint@8.57.0) @@ -5824,8 +5824,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@0.0.0-experimental-7670337-20240918: - resolution: {integrity: sha512-FZXoYqGK3BiJw2BFQIUL0C9GgEF2QS+y6oiVPm4GooX6SXrg/UVtCi1nMAaBhqTpsF/KF2RjobgePY8KgbqXow==} + eslint-plugin-react-compiler@0.0.0-experimental-92aaa43-20240919: + resolution: {integrity: sha512-l1tEUmxnZcMNkpUffbyNPAV91kZMZYLVeCCRZajK/s1QdP9FquunJQ9uT4c3f0RqdV6n0kloVfnJ0lPD1FTPlg==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -15628,7 +15628,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@0.0.0-experimental-7670337-20240918(eslint@8.57.0): + eslint-plugin-react-compiler@0.0.0-experimental-92aaa43-20240919(eslint@8.57.0): dependencies: '@babel/core': 7.25.2 '@babel/parser': 7.25.6