@etsoo/toolpad 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,7 @@ export interface NavigationPageItem {
27
27
  pattern?: string;
28
28
  action?: React.ReactNode;
29
29
  children?: Navigation;
30
+ subs?: string[];
30
31
  }
31
32
  export interface NavigationSubheaderItem {
32
33
  kind: "header";
@@ -97,12 +97,10 @@ function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0
97
97
  const isNestedNavigationExpanded = expandedSidebarItemIds.includes(navigationItemId);
98
98
  const nestedNavigationCollapseIcon = isNestedNavigationExpanded ? (_jsx(ExpandLessIcon, {})) : (_jsx(ExpandMoreIcon, {}));
99
99
  const listItemIconSize = 34;
100
- const isSelected = isPageItemSelected(navigationItem, basePath, pathname);
101
- if (process.env.NODE_ENV !== "production" &&
102
- isSelected &&
103
- selectedItemId) {
104
- console.warn(`Duplicate selected path in navigation: ${navigationItemFullPath}`);
105
- }
100
+ // If the item is selected, we don't want to select more
101
+ const isSelected = selectedItemId
102
+ ? false
103
+ : isPageItemSelected(navigationItem, basePath, pathname);
106
104
  if (isSelected && !selectedItemId) {
107
105
  selectedItemId = navigationItemId;
108
106
  }
@@ -21,20 +21,18 @@ export interface Breadcrumb {
21
21
  */
22
22
  path: string;
23
23
  }
24
- /**
25
- * @deprecated Use `Breadcrumb` instead.
26
- */
27
- export type BreadCrumb = Breadcrumb;
28
- export interface PageContainerProps extends ContainerProps {
29
- children?: React.ReactNode;
30
- /**
31
- * The title of the page. Leave blank to use the active page title.
32
- */
24
+ export type PageData = {
33
25
  title?: string;
34
- /**
35
- * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs.
36
- */
26
+ page?: string;
37
27
  breadcrumbs?: Breadcrumb[];
28
+ };
29
+ type PageDataAction = PageData | true;
30
+ export declare const PageDataContext: React.Context<{
31
+ state: PageData;
32
+ dispatch: React.Dispatch<PageDataAction>;
33
+ }>;
34
+ export declare function PageDataContextProvider(props: React.PropsWithChildren<PageData>): import("react/jsx-runtime").JSX.Element;
35
+ export type PageContainerProps = React.PropsWithChildren<ContainerProps & {
38
36
  /**
39
37
  * The components used for each slot inside.
40
38
  */
@@ -43,7 +41,7 @@ export interface PageContainerProps extends ContainerProps {
43
41
  * The props used for each slot inside.
44
42
  */
45
43
  slotProps?: PageContainerSlotProps;
46
- }
44
+ }>;
47
45
  /**
48
46
  * A container component to provide a title and breadcrumbs for your pages.
49
47
  *
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
3
4
  import Breadcrumbs from "@mui/material/Breadcrumbs";
4
5
  import Container from "@mui/material/Container";
5
6
  import Link from "@mui/material/Link";
@@ -17,6 +18,29 @@ const PageContentHeader = styled("div")(({ theme }) => ({
17
18
  justifyContent: "space-between",
18
19
  gap: theme.spacing(2)
19
20
  }));
21
+ export const PageDataContext = React.createContext({ state: {}, dispatch: (value) => value });
22
+ function reducer(state, action) {
23
+ if (action === true) {
24
+ // Reset the state
25
+ if (state.breadcrumbs == null &&
26
+ state.title == null &&
27
+ state.page == null) {
28
+ return state;
29
+ }
30
+ else {
31
+ return {};
32
+ }
33
+ }
34
+ return { ...state, ...action };
35
+ }
36
+ export function PageDataContextProvider(props) {
37
+ // Destruct
38
+ const { title, breadcrumbs, ...rest } = props;
39
+ // useReducer hook to manage state with our reducer function and initial state
40
+ const [state, dispatch] = React.useReducer(reducer, { title, breadcrumbs });
41
+ // Provide the state and dispatch function to the context value
42
+ return _jsx(PageDataContext.Provider, { value: { state, dispatch }, ...rest });
43
+ }
20
44
  /**
21
45
  * A container component to provide a title and breadcrumbs for your pages.
22
46
  *
@@ -29,11 +53,26 @@ const PageContentHeader = styled("div")(({ theme }) => ({
29
53
  * - [PageContainer API](https://mui.com/toolpad/core/api/page-container)
30
54
  */
31
55
  function PageContainer(props) {
32
- const { children, slots, slotProps, breadcrumbs, ...rest } = props;
56
+ const { children, slots, slotProps, ...rest } = props;
57
+ const loaded = React.useRef(false);
58
+ const { state, dispatch } = React.useContext(PageDataContext);
33
59
  const activePage = useActivePage();
34
- // TODO: Remove `props.breadCrumbs` in the next major version
35
- const resolvedBreadcrumbs = breadcrumbs ?? activePage?.breadcrumbs ?? [];
36
- const title = props.title ?? activePage?.title ?? "";
60
+ React.useLayoutEffect(() => {
61
+ if (loaded.current) {
62
+ dispatch(true);
63
+ }
64
+ else {
65
+ loaded.current = true;
66
+ }
67
+ }, [activePage?.sourcePath]);
68
+ let resolvedBreadcrumbs = state.breadcrumbs ?? activePage?.breadcrumbs ?? [];
69
+ const title = state.title ?? activePage?.title ?? "";
70
+ if (state.page) {
71
+ resolvedBreadcrumbs = [
72
+ ...resolvedBreadcrumbs,
73
+ { title: state.page, path: "#" }
74
+ ];
75
+ }
37
76
  const ToolbarComponent = props?.slots?.toolbar ?? PageContainerToolbar;
38
77
  const toolbarSlotProps = useSlotProps({
39
78
  elementType: ToolbarComponent,
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { expect, describe, test, vi } from "vitest";
3
- import { render, within, screen } from "@testing-library/react";
3
+ import { render, within, screen, act } from "@testing-library/react";
4
4
  import { userEvent } from "@testing-library/user-event";
5
- import { PageContainer } from "./PageContainer";
5
+ import { PageContainer, PageDataContextProvider } from "./PageContainer";
6
6
  import describeConformance from "../utils/describeConformance";
7
7
  import { AppProvider } from "../AppProvider/AppProviderComponent";
8
8
  describe("PageContainer", () => {
@@ -13,16 +13,18 @@ describe("PageContainer", () => {
13
13
  }
14
14
  }));
15
15
  test("renders page container correctly", async () => {
16
- const user = await userEvent.setup();
16
+ const user = userEvent.setup();
17
17
  const router = {
18
18
  pathname: "/orders",
19
19
  searchParams: new URLSearchParams(),
20
20
  navigate: vi.fn()
21
21
  };
22
- render(_jsx(AppProvider, { navigation: [
23
- { segment: "", title: "Home" },
24
- { segment: "orders", title: "Orders" }
25
- ], router: router, children: _jsx(PageContainer, {}) }));
22
+ act(() => {
23
+ render(_jsx(AppProvider, { navigation: [
24
+ { segment: "", title: "Home" },
25
+ { segment: "orders", title: "Orders" }
26
+ ], router: router, children: _jsx(PageDataContextProvider, { children: _jsx(PageContainer, {}) }) }));
27
+ });
26
28
  const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
27
29
  const homeLink = within(breadcrumbs).getByRole("link", { name: "Home" });
28
30
  await user.click(homeLink);
@@ -51,23 +53,27 @@ describe("PageContainer", () => {
51
53
  navigate: vi.fn()
52
54
  };
53
55
  const branding = { title: "ACME" };
54
- render(_jsx(AppProvider, { branding: branding, navigation: navigation, router: router, children: _jsx(PageContainer, {}) }));
56
+ act(() => {
57
+ render(_jsx(AppProvider, { branding: branding, navigation: navigation, router: router, children: _jsx(PageDataContextProvider, { children: _jsx(PageContainer, {}) }) }));
58
+ });
55
59
  const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
56
60
  expect(within(breadcrumbs).getByText("ACME")).toBeTruthy();
57
61
  expect(within(breadcrumbs).getByText("Home")).toBeTruthy();
58
62
  expect(within(breadcrumbs).getByText("Orders")).toBeTruthy();
59
63
  });
60
64
  test("renders dynamic correctly", async () => {
61
- const user = await userEvent.setup();
65
+ const user = userEvent.setup();
62
66
  const router = {
63
67
  pathname: "/orders/123",
64
68
  searchParams: new URLSearchParams(),
65
69
  navigate: vi.fn()
66
70
  };
67
- render(_jsx(AppProvider, { navigation: [
68
- { segment: "", title: "Home" },
69
- { segment: "orders", title: "Orders", pattern: "orders/:id" }
70
- ], router: router, children: _jsx(PageContainer, {}) }));
71
+ act(() => {
72
+ render(_jsx(AppProvider, { navigation: [
73
+ { segment: "", title: "Home" },
74
+ { segment: "orders", title: "Orders", pattern: "orders/:id" }
75
+ ], router: router, children: _jsx(PageDataContextProvider, { children: _jsx(PageContainer, {}) }) }));
76
+ });
71
77
  const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
72
78
  const homeLink = within(breadcrumbs).getByRole("link", { name: "Home" });
73
79
  await user.click(homeLink);
@@ -82,29 +88,33 @@ describe("PageContainer", () => {
82
88
  searchParams: new URLSearchParams(),
83
89
  navigate: vi.fn()
84
90
  };
85
- render(_jsx(AppProvider, { navigation: [
86
- {
87
- segment: "users",
88
- title: "Users",
89
- children: [
90
- {
91
- segment: "invoices",
92
- title: "Invoices",
93
- pattern: "invoices/:id"
94
- }
95
- ]
96
- }
97
- ], router: router, children: _jsx(PageContainer, {}) }));
91
+ act(() => {
92
+ render(_jsx(AppProvider, { navigation: [
93
+ {
94
+ segment: "users",
95
+ title: "Users",
96
+ children: [
97
+ {
98
+ segment: "invoices",
99
+ title: "Invoices",
100
+ pattern: "invoices/:id"
101
+ }
102
+ ]
103
+ }
104
+ ], router: router, children: _jsx(PageDataContextProvider, { children: _jsx(PageContainer, {}) }) }));
105
+ });
98
106
  const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
99
107
  const homeLink = within(breadcrumbs).getByRole("link", { name: "Users" });
100
108
  expect(homeLink.getAttribute("href")).toBe("/users");
101
109
  expect(within(breadcrumbs).getByText("Invoices")).toBeTruthy();
102
110
  });
103
111
  test("renders custom breadcrumbs", async () => {
104
- render(_jsx(PageContainer, { breadcrumbs: [
105
- { title: "Hello", path: "/hello" },
106
- { title: "World", path: "/world" }
107
- ] }));
112
+ act(() => {
113
+ render(_jsx(PageDataContextProvider, { breadcrumbs: [
114
+ { title: "Hello", path: "/hello" },
115
+ { title: "World", path: "/world" }
116
+ ], children: _jsx(PageContainer, {}) }));
117
+ });
108
118
  const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
109
119
  const helloLink = within(breadcrumbs).getByRole("link", { name: "Hello" });
110
120
  expect(helloLink.getAttribute("href")).toBe("/hello");
@@ -9,9 +9,13 @@ export function getPageItemFullPath(basePath, navigationItem) {
9
9
  return `${basePath}${basePath && !navigationItem.segment ? "" : "/"}${navigationItem.segment ?? ""}`;
10
10
  }
11
11
  export function isPageItemSelected(navigationItem, basePath, pathname) {
12
- return navigationItem.pattern
13
- ? pathToRegexp(`${basePath}/${navigationItem.pattern}`).test(pathname)
14
- : getPageItemFullPath(basePath, navigationItem) === pathname;
12
+ if (navigationItem.pattern) {
13
+ return pathToRegexp(`${basePath}/${navigationItem.pattern}`).test(pathname);
14
+ }
15
+ if (navigationItem.subs) {
16
+ return navigationItem.subs.some((sub) => new RegExp(sub).test(pathname));
17
+ }
18
+ return getPageItemFullPath(basePath, navigationItem) === pathname;
15
19
  }
16
20
  export function hasSelectedNavigationChildren(navigationItem, basePath, pathname) {
17
21
  if (isPageItem(navigationItem) && navigationItem.children) {
@@ -109,11 +113,15 @@ function getItemLookup(navigation) {
109
113
  export function matchPath(navigation, path) {
110
114
  const lookup = getItemLookup(navigation);
111
115
  for (const [key, item] of lookup.entries()) {
112
- if (typeof key === "string" && key === path) {
113
- return item;
116
+ if (typeof key === "string") {
117
+ if (key === path)
118
+ return item;
119
+ else if (item.subs?.some((sub) => new RegExp(sub).test(path)))
120
+ return item;
114
121
  }
115
- if (key instanceof RegExp && key.test(path)) {
116
- return item;
122
+ else if (key instanceof RegExp) {
123
+ if (key.test(path))
124
+ return item;
117
125
  }
118
126
  }
119
127
  return null;
@@ -2,10 +2,7 @@ import type { Breadcrumb } from "../PageContainer";
2
2
  export interface ActivePage {
3
3
  title: string;
4
4
  path: string;
5
- /**
6
- * @deprecated Use `breadcrumbs` instead.
7
- */
8
- breadCrumbs: Breadcrumb[];
5
+ sourcePath: string;
9
6
  breadcrumbs: Breadcrumb[];
10
7
  }
11
8
  export declare function useActivePage(): ActivePage | null;
@@ -40,9 +40,8 @@ export function useActivePage() {
40
40
  return {
41
41
  title: getItemTitle(activeItem),
42
42
  path: getItemPath(navigationContext, activeItem),
43
- breadcrumbs,
44
- // TODO: Remove in the next major version
45
- breadCrumbs: breadcrumbs
43
+ sourcePath: pathname,
44
+ breadcrumbs
46
45
  };
47
46
  }, [activeItem, rootItem, pathname, navigationContext]);
48
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/toolpad",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "author": "ETSOO",
5
5
  "description": "Dashboard framework extention based on Toolpad Core",
6
6
  "main": "build/index.js",
@@ -35,6 +35,7 @@ export interface NavigationPageItem {
35
35
  pattern?: string;
36
36
  action?: React.ReactNode;
37
37
  children?: Navigation;
38
+ subs?: string[];
38
39
  }
39
40
 
40
41
  export interface NavigationSubheaderItem {
@@ -175,21 +175,10 @@ function DashboardSidebarSubNavigation({
175
175
 
176
176
  const listItemIconSize = 34;
177
177
 
178
- const isSelected = isPageItemSelected(
179
- navigationItem,
180
- basePath,
181
- pathname
182
- );
183
-
184
- if (
185
- process.env.NODE_ENV !== "production" &&
186
- isSelected &&
187
- selectedItemId
188
- ) {
189
- console.warn(
190
- `Duplicate selected path in navigation: ${navigationItemFullPath}`
191
- );
192
- }
178
+ // If the item is selected, we don't want to select more
179
+ const isSelected = selectedItemId
180
+ ? false
181
+ : isPageItemSelected(navigationItem, basePath, pathname);
193
182
 
194
183
  if (isSelected && !selectedItemId) {
195
184
  selectedItemId = navigationItemId;
@@ -1,7 +1,7 @@
1
1
  import { expect, describe, test, vi } from "vitest";
2
- import { render, within, screen } from "@testing-library/react";
2
+ import { render, within, screen, act } from "@testing-library/react";
3
3
  import { userEvent } from "@testing-library/user-event";
4
- import { PageContainer } from "./PageContainer";
4
+ import { PageContainer, PageDataContextProvider } from "./PageContainer";
5
5
  import describeConformance from "../utils/describeConformance";
6
6
  import { AppProvider } from "../AppProvider/AppProviderComponent";
7
7
 
@@ -14,23 +14,28 @@ describe("PageContainer", () => {
14
14
  }));
15
15
 
16
16
  test("renders page container correctly", async () => {
17
- const user = await userEvent.setup();
17
+ const user = userEvent.setup();
18
18
  const router = {
19
19
  pathname: "/orders",
20
20
  searchParams: new URLSearchParams(),
21
21
  navigate: vi.fn()
22
22
  };
23
- render(
24
- <AppProvider
25
- navigation={[
26
- { segment: "", title: "Home" },
27
- { segment: "orders", title: "Orders" }
28
- ]}
29
- router={router}
30
- >
31
- <PageContainer />
32
- </AppProvider>
33
- );
23
+
24
+ act(() => {
25
+ render(
26
+ <AppProvider
27
+ navigation={[
28
+ { segment: "", title: "Home" },
29
+ { segment: "orders", title: "Orders" }
30
+ ]}
31
+ router={router}
32
+ >
33
+ <PageDataContextProvider>
34
+ <PageContainer />
35
+ </PageDataContextProvider>
36
+ </AppProvider>
37
+ );
38
+ });
34
39
 
35
40
  const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
36
41
 
@@ -70,11 +75,20 @@ describe("PageContainer", () => {
70
75
  };
71
76
 
72
77
  const branding = { title: "ACME" };
73
- render(
74
- <AppProvider branding={branding} navigation={navigation} router={router}>
75
- <PageContainer />
76
- </AppProvider>
77
- );
78
+
79
+ act(() => {
80
+ render(
81
+ <AppProvider
82
+ branding={branding}
83
+ navigation={navigation}
84
+ router={router}
85
+ >
86
+ <PageDataContextProvider>
87
+ <PageContainer />
88
+ </PageDataContextProvider>
89
+ </AppProvider>
90
+ );
91
+ });
78
92
 
79
93
  const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
80
94
 
@@ -84,27 +98,33 @@ describe("PageContainer", () => {
84
98
  });
85
99
 
86
100
  test("renders dynamic correctly", async () => {
87
- const user = await userEvent.setup();
101
+ const user = userEvent.setup();
88
102
  const router = {
89
103
  pathname: "/orders/123",
90
104
  searchParams: new URLSearchParams(),
91
105
  navigate: vi.fn()
92
106
  };
93
- render(
94
- <AppProvider
95
- navigation={[
96
- { segment: "", title: "Home" },
97
- { segment: "orders", title: "Orders", pattern: "orders/:id" }
98
- ]}
99
- router={router}
100
- >
101
- <PageContainer />
102
- </AppProvider>
103
- );
107
+
108
+ act(() => {
109
+ render(
110
+ <AppProvider
111
+ navigation={[
112
+ { segment: "", title: "Home" },
113
+ { segment: "orders", title: "Orders", pattern: "orders/:id" }
114
+ ]}
115
+ router={router}
116
+ >
117
+ <PageDataContextProvider>
118
+ <PageContainer />
119
+ </PageDataContextProvider>
120
+ </AppProvider>
121
+ );
122
+ });
104
123
 
105
124
  const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
106
125
 
107
126
  const homeLink = within(breadcrumbs).getByRole("link", { name: "Home" });
127
+
108
128
  await user.click(homeLink);
109
129
 
110
130
  expect(router.navigate).toHaveBeenCalledWith(
@@ -124,26 +144,31 @@ describe("PageContainer", () => {
124
144
  searchParams: new URLSearchParams(),
125
145
  navigate: vi.fn()
126
146
  };
127
- render(
128
- <AppProvider
129
- navigation={[
130
- {
131
- segment: "users",
132
- title: "Users",
133
- children: [
134
- {
135
- segment: "invoices",
136
- title: "Invoices",
137
- pattern: "invoices/:id"
138
- }
139
- ]
140
- }
141
- ]}
142
- router={router}
143
- >
144
- <PageContainer />
145
- </AppProvider>
146
- );
147
+
148
+ act(() => {
149
+ render(
150
+ <AppProvider
151
+ navigation={[
152
+ {
153
+ segment: "users",
154
+ title: "Users",
155
+ children: [
156
+ {
157
+ segment: "invoices",
158
+ title: "Invoices",
159
+ pattern: "invoices/:id"
160
+ }
161
+ ]
162
+ }
163
+ ]}
164
+ router={router}
165
+ >
166
+ <PageDataContextProvider>
167
+ <PageContainer />
168
+ </PageDataContextProvider>
169
+ </AppProvider>
170
+ );
171
+ });
147
172
 
148
173
  const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
149
174
 
@@ -153,14 +178,18 @@ describe("PageContainer", () => {
153
178
  });
154
179
 
155
180
  test("renders custom breadcrumbs", async () => {
156
- render(
157
- <PageContainer
158
- breadcrumbs={[
159
- { title: "Hello", path: "/hello" },
160
- { title: "World", path: "/world" }
161
- ]}
162
- />
163
- );
181
+ act(() => {
182
+ render(
183
+ <PageDataContextProvider
184
+ breadcrumbs={[
185
+ { title: "Hello", path: "/hello" },
186
+ { title: "World", path: "/world" }
187
+ ]}
188
+ >
189
+ <PageContainer />
190
+ </PageDataContextProvider>
191
+ );
192
+ });
164
193
 
165
194
  const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
166
195
 
@@ -45,32 +45,62 @@ export interface Breadcrumb {
45
45
  path: string;
46
46
  }
47
47
 
48
- // TODO: Remove in the next major version
49
- /**
50
- * @deprecated Use `Breadcrumb` instead.
51
- */
52
- export type BreadCrumb = Breadcrumb;
53
-
54
- export interface PageContainerProps extends ContainerProps {
55
- children?: React.ReactNode;
56
- /**
57
- * The title of the page. Leave blank to use the active page title.
58
- */
48
+ export type PageData = {
59
49
  title?: string;
60
- /**
61
- * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs.
62
- */
50
+ page?: string;
63
51
  breadcrumbs?: Breadcrumb[];
64
- /**
65
- * The components used for each slot inside.
66
- */
67
- slots?: PageContainerSlots;
68
- /**
69
- * The props used for each slot inside.
70
- */
71
- slotProps?: PageContainerSlotProps;
52
+ };
53
+
54
+ type PageDataAction = PageData | true;
55
+
56
+ export const PageDataContext = React.createContext<{
57
+ state: PageData;
58
+ dispatch: React.Dispatch<PageDataAction>;
59
+ }>({ state: {}, dispatch: (value) => value });
60
+
61
+ function reducer(state: PageData, action: PageDataAction) {
62
+ if (action === true) {
63
+ // Reset the state
64
+ if (
65
+ state.breadcrumbs == null &&
66
+ state.title == null &&
67
+ state.page == null
68
+ ) {
69
+ return state;
70
+ } else {
71
+ return {};
72
+ }
73
+ }
74
+
75
+ return { ...state, ...action };
72
76
  }
73
77
 
78
+ export function PageDataContextProvider(
79
+ props: React.PropsWithChildren<PageData>
80
+ ) {
81
+ // Destruct
82
+ const { title, breadcrumbs, ...rest } = props;
83
+
84
+ // useReducer hook to manage state with our reducer function and initial state
85
+ const [state, dispatch] = React.useReducer(reducer, { title, breadcrumbs });
86
+
87
+ // Provide the state and dispatch function to the context value
88
+ return <PageDataContext.Provider value={{ state, dispatch }} {...rest} />;
89
+ }
90
+
91
+ export type PageContainerProps = React.PropsWithChildren<
92
+ ContainerProps & {
93
+ /**
94
+ * The components used for each slot inside.
95
+ */
96
+ slots?: PageContainerSlots;
97
+ /**
98
+ * The props used for each slot inside.
99
+ */
100
+ slotProps?: PageContainerSlotProps;
101
+ }
102
+ >;
103
+
74
104
  /**
75
105
  * A container component to provide a title and breadcrumbs for your pages.
76
106
  *
@@ -83,13 +113,30 @@ export interface PageContainerProps extends ContainerProps {
83
113
  * - [PageContainer API](https://mui.com/toolpad/core/api/page-container)
84
114
  */
85
115
  function PageContainer(props: PageContainerProps) {
86
- const { children, slots, slotProps, breadcrumbs, ...rest } = props;
116
+ const { children, slots, slotProps, ...rest } = props;
117
+
118
+ const loaded = React.useRef(false);
119
+ const { state, dispatch } = React.useContext(PageDataContext);
87
120
 
88
121
  const activePage = useActivePage();
89
122
 
90
- // TODO: Remove `props.breadCrumbs` in the next major version
91
- const resolvedBreadcrumbs = breadcrumbs ?? activePage?.breadcrumbs ?? [];
92
- const title = props.title ?? activePage?.title ?? "";
123
+ React.useLayoutEffect(() => {
124
+ if (loaded.current) {
125
+ dispatch(true);
126
+ } else {
127
+ loaded.current = true;
128
+ }
129
+ }, [activePage?.sourcePath]);
130
+
131
+ let resolvedBreadcrumbs = state.breadcrumbs ?? activePage?.breadcrumbs ?? [];
132
+ const title = state.title ?? activePage?.title ?? "";
133
+
134
+ if (state.page) {
135
+ resolvedBreadcrumbs = [
136
+ ...resolvedBreadcrumbs,
137
+ { title: state.page, path: "#" }
138
+ ];
139
+ }
93
140
 
94
141
  const ToolbarComponent = props?.slots?.toolbar ?? PageContainerToolbar;
95
142
  const toolbarSlotProps = useSlotProps({
@@ -32,9 +32,15 @@ export function isPageItemSelected(
32
32
  basePath: string,
33
33
  pathname: string
34
34
  ) {
35
- return navigationItem.pattern
36
- ? pathToRegexp(`${basePath}/${navigationItem.pattern}`).test(pathname)
37
- : getPageItemFullPath(basePath, navigationItem) === pathname;
35
+ if (navigationItem.pattern) {
36
+ return pathToRegexp(`${basePath}/${navigationItem.pattern}`).test(pathname);
37
+ }
38
+
39
+ if (navigationItem.subs) {
40
+ return navigationItem.subs.some((sub) => new RegExp(sub).test(pathname));
41
+ }
42
+
43
+ return getPageItemFullPath(basePath, navigationItem) === pathname;
38
44
  }
39
45
 
40
46
  export function hasSelectedNavigationChildren(
@@ -172,11 +178,12 @@ export function matchPath(
172
178
  const lookup = getItemLookup(navigation);
173
179
 
174
180
  for (const [key, item] of lookup.entries()) {
175
- if (typeof key === "string" && key === path) {
176
- return item;
177
- }
178
- if (key instanceof RegExp && key.test(path)) {
179
- return item;
181
+ if (typeof key === "string") {
182
+ if (key === path) return item;
183
+ else if (item.subs?.some((sub) => new RegExp(sub).test(path)))
184
+ return item;
185
+ } else if (key instanceof RegExp) {
186
+ if (key.test(path)) return item;
180
187
  }
181
188
  }
182
189
 
@@ -7,10 +7,7 @@ import type { Breadcrumb } from "../PageContainer";
7
7
  export interface ActivePage {
8
8
  title: string;
9
9
  path: string;
10
- /**
11
- * @deprecated Use `breadcrumbs` instead.
12
- */
13
- breadCrumbs: Breadcrumb[];
10
+ sourcePath: string;
14
11
  breadcrumbs: Breadcrumb[];
15
12
  }
16
13
 
@@ -58,9 +55,8 @@ export function useActivePage(): ActivePage | null {
58
55
  return {
59
56
  title: getItemTitle(activeItem),
60
57
  path: getItemPath(navigationContext, activeItem),
61
- breadcrumbs,
62
- // TODO: Remove in the next major version
63
- breadCrumbs: breadcrumbs
58
+ sourcePath: pathname,
59
+ breadcrumbs
64
60
  };
65
61
  }, [activeItem, rootItem, pathname, navigationContext]);
66
62
  }