@etsoo/toolpad 1.0.28 → 1.0.30
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.
- package/build/cjs/AppProvider/AppProvider.d.ts +2 -1
- package/build/cjs/DashboardLayout/DashboardLayout.test.js +17 -1
- package/build/cjs/DashboardLayout/DashboardSidebarSubNavigation.js +18 -8
- package/build/cjs/PageContainer/PageContainer.d.ts +1 -6
- package/build/cjs/PageContainer/PageContainer.js +28 -28
- package/build/cjs/shared/navigation.js +4 -11
- package/build/cjs/useActivePage/useActivePage.d.ts +2 -0
- package/build/cjs/useActivePage/useActivePage.js +2 -1
- package/build/mjs/AppProvider/AppProvider.d.ts +2 -1
- package/build/mjs/DashboardLayout/DashboardLayout.test.js +17 -1
- package/build/mjs/DashboardLayout/DashboardSidebarSubNavigation.js +18 -8
- package/build/mjs/PageContainer/PageContainer.d.ts +1 -6
- package/build/mjs/PageContainer/PageContainer.js +28 -28
- package/build/mjs/shared/navigation.js +4 -11
- package/build/mjs/useActivePage/useActivePage.d.ts +2 -0
- package/build/mjs/useActivePage/useActivePage.js +2 -1
- package/package.json +1 -1
- package/src/AppProvider/AppProvider.tsx +2 -1
- package/src/DashboardLayout/DashboardLayout.test.tsx +20 -1
- package/src/DashboardLayout/DashboardSidebarSubNavigation.tsx +27 -9
- package/src/PageContainer/PageContainer.tsx +47 -58
- package/src/shared/navigation.tsx +5 -10
- package/src/useActivePage/useActivePage.ts +4 -1
|
@@ -131,6 +131,12 @@ const AppProviderComponent_1 = require("../AppProvider/AppProviderComponent");
|
|
|
131
131
|
segment: "traffic",
|
|
132
132
|
title: "Traffic",
|
|
133
133
|
icon: (0, jsx_runtime_1.jsx)(Description_1.default, {})
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
segment: "hidden",
|
|
137
|
+
title: "Hidden",
|
|
138
|
+
icon: (0, jsx_runtime_1.jsx)(Description_1.default, {}),
|
|
139
|
+
hidden: true
|
|
134
140
|
}
|
|
135
141
|
]
|
|
136
142
|
}
|
|
@@ -146,6 +152,7 @@ const AppProviderComponent_1 = require("../AppProvider/AppProviderComponent");
|
|
|
146
152
|
});
|
|
147
153
|
(0, vitest_1.expect)((0, react_1.within)(desktopNavigation).getByText("Sales")).toBeTruthy();
|
|
148
154
|
(0, vitest_1.expect)((0, react_1.within)(desktopNavigation).getByText("Traffic")).toBeTruthy();
|
|
155
|
+
(0, vitest_1.expect)((0, react_1.within)(desktopNavigation).queryByText("Hidden")).toBeNull();
|
|
149
156
|
});
|
|
150
157
|
(0, vitest_1.test)("shows correct selected page item", () => {
|
|
151
158
|
const NAVIGATION = [
|
|
@@ -157,7 +164,14 @@ const AppProviderComponent_1 = require("../AppProvider/AppProviderComponent");
|
|
|
157
164
|
{
|
|
158
165
|
title: "Orders",
|
|
159
166
|
segment: "orders",
|
|
160
|
-
icon: (0, jsx_runtime_1.jsx)(ShoppingCart_1.default, {})
|
|
167
|
+
icon: (0, jsx_runtime_1.jsx)(ShoppingCart_1.default, {}),
|
|
168
|
+
children: [
|
|
169
|
+
{
|
|
170
|
+
segment: "nested",
|
|
171
|
+
title: "Nested",
|
|
172
|
+
hidden: true
|
|
173
|
+
}
|
|
174
|
+
]
|
|
161
175
|
},
|
|
162
176
|
{
|
|
163
177
|
segment: "dynamic",
|
|
@@ -197,6 +211,8 @@ const AppProviderComponent_1 = require("../AppProvider/AppProviderComponent");
|
|
|
197
211
|
rerender((0, jsx_runtime_1.jsx)(AppWithPathname, { pathname: "/orders" }));
|
|
198
212
|
(0, vitest_1.expect)((0, react_1.within)(desktopNavigation).getByRole("link", { name: "Dashboard" })).not.toHaveClass("Mui-selected");
|
|
199
213
|
(0, vitest_1.expect)((0, react_1.within)(desktopNavigation).getByRole("link", { name: "Orders" })).toHaveClass("Mui-selected");
|
|
214
|
+
rerender((0, jsx_runtime_1.jsx)(AppWithPathname, { pathname: "/orders/nested" }));
|
|
215
|
+
(0, vitest_1.expect)((0, react_1.within)(desktopNavigation).getByRole("link", { name: "Orders" })).toHaveClass("Mui-selected");
|
|
200
216
|
rerender((0, jsx_runtime_1.jsx)(AppWithPathname, { pathname: "/dynamic" }));
|
|
201
217
|
(0, vitest_1.expect)((0, react_1.within)(desktopNavigation).getByRole("link", { name: "Dynamic" })).not.toHaveClass("Mui-selected");
|
|
202
218
|
rerender((0, jsx_runtime_1.jsx)(AppWithPathname, { pathname: "/dynamic/123" }));
|
|
@@ -53,10 +53,10 @@ const Tooltip_1 = __importDefault(require("@mui/material/Tooltip"));
|
|
|
53
53
|
const ExpandLess_1 = __importDefault(require("@mui/icons-material/ExpandLess"));
|
|
54
54
|
const ExpandMore_1 = __importDefault(require("@mui/icons-material/ExpandMore"));
|
|
55
55
|
const Link_1 = require("../shared/Link");
|
|
56
|
-
const context_1 = require("../shared/context");
|
|
57
56
|
const navigation_1 = require("../shared/navigation");
|
|
58
57
|
const utils_1 = require("./utils");
|
|
59
58
|
const styles_1 = require("@mui/material/styles");
|
|
59
|
+
const useActivePage_1 = require("../useActivePage/useActivePage");
|
|
60
60
|
const NavigationListItemButton = (0, styles_1.styled)(ListItemButton_1.default)(({ theme }) => ({
|
|
61
61
|
borderRadius: 8,
|
|
62
62
|
"&.Mui-selected": {
|
|
@@ -87,8 +87,8 @@ const NavigationListItemButton = (0, styles_1.styled)(ListItemButton_1.default)(
|
|
|
87
87
|
* @ignore - internal component.
|
|
88
88
|
*/
|
|
89
89
|
function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0, onLinkClick, isMini = false, isFullyExpanded = true, hasDrawerTransitions = false, selectedItemId }) {
|
|
90
|
-
const
|
|
91
|
-
const pathname =
|
|
90
|
+
const activePage = (0, useActivePage_1.useActivePage)();
|
|
91
|
+
const pathname = activePage?.sourcePath ?? "/";
|
|
92
92
|
const initialExpandedSidebarItemIds = React.useMemo(() => subNavigation
|
|
93
93
|
.map((navigationItem, navigationItemIndex) => ({
|
|
94
94
|
navigationItem,
|
|
@@ -130,6 +130,13 @@ function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0
|
|
|
130
130
|
: {})
|
|
131
131
|
} }, `divider-${depth}-${navigationItemIndex}`));
|
|
132
132
|
}
|
|
133
|
+
if (navigationItem.hidden) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
let children = navigationItem.children?.filter((child) => child.kind === "divider" || child.kind === "header" || !child.hidden);
|
|
137
|
+
if (children && children.length === 0) {
|
|
138
|
+
children = undefined;
|
|
139
|
+
}
|
|
133
140
|
const navigationItemFullPath = (0, navigation_1.getPageItemFullPath)(basePath, navigationItem);
|
|
134
141
|
const navigationItemId = `${depth}-${navigationItemIndex}`;
|
|
135
142
|
const navigationItemTitle = (0, navigation_1.getItemTitle)(navigationItem);
|
|
@@ -139,7 +146,10 @@ function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0
|
|
|
139
146
|
// If the item is selected, we don't want to select more
|
|
140
147
|
const isSelected = selectedItemId
|
|
141
148
|
? false
|
|
142
|
-
: (0, navigation_1.isPageItemSelected)(navigationItem, basePath, pathname)
|
|
149
|
+
: (0, navigation_1.isPageItemSelected)(navigationItem, basePath, pathname) ||
|
|
150
|
+
navigationItem.children?.some((child) => (child.kind === "page" || child.kind == null) &&
|
|
151
|
+
child.hidden &&
|
|
152
|
+
(0, navigation_1.isPageItemSelected)(child, navigationItemFullPath, pathname));
|
|
143
153
|
if (isSelected && !selectedItemId) {
|
|
144
154
|
selectedItemId = navigationItemId;
|
|
145
155
|
}
|
|
@@ -147,10 +157,10 @@ function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0
|
|
|
147
157
|
py: 0,
|
|
148
158
|
px: 1,
|
|
149
159
|
overflowX: "hidden"
|
|
150
|
-
}, children: (0, jsx_runtime_1.jsxs)(NavigationListItemButton, { selected: isSelected && (!
|
|
160
|
+
}, children: (0, jsx_runtime_1.jsxs)(NavigationListItemButton, { selected: isSelected && (!children || isMini), sx: {
|
|
151
161
|
px: 1.4,
|
|
152
162
|
height: 48
|
|
153
|
-
}, ...(
|
|
163
|
+
}, ...(children && !isMini
|
|
154
164
|
? {
|
|
155
165
|
onClick: handleOpenFolderClick(navigationItemId)
|
|
156
166
|
}
|
|
@@ -177,9 +187,9 @@ function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0
|
|
|
177
187
|
}
|
|
178
188
|
} }), navigationItem.action && !isMini && isFullyExpanded
|
|
179
189
|
? navigationItem.action
|
|
180
|
-
: null,
|
|
190
|
+
: null, children && !isMini && isFullyExpanded
|
|
181
191
|
? nestedNavigationCollapseIcon
|
|
182
192
|
: null] }) }));
|
|
183
|
-
return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [isMini ? ((0, jsx_runtime_1.jsx)(Tooltip_1.default, { title: navigationItemTitle, placement: "right", children: listItem })) : (listItem),
|
|
193
|
+
return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [isMini ? ((0, jsx_runtime_1.jsx)(Tooltip_1.default, { title: navigationItemTitle, placement: "right", children: listItem })) : (listItem), children && !isMini ? ((0, jsx_runtime_1.jsx)(Collapse_1.default, { in: isNestedNavigationExpanded, timeout: "auto", unmountOnExit: true, children: (0, jsx_runtime_1.jsx)(DashboardSidebarSubNavigation, { subNavigation: children, basePath: navigationItemFullPath, depth: depth + 1, onLinkClick: onLinkClick, selectedItemId: selectedItemId }) })) : null] }, navigationItemId));
|
|
184
194
|
}) }));
|
|
185
195
|
}
|
|
@@ -25,8 +25,7 @@ export type PageData = {
|
|
|
25
25
|
title?: string;
|
|
26
26
|
page?: string;
|
|
27
27
|
breadcrumbs?: Breadcrumb[];
|
|
28
|
-
|
|
29
|
-
noPageHeader?: boolean;
|
|
28
|
+
pageHeader?: React.ReactNode;
|
|
30
29
|
};
|
|
31
30
|
type PageDataAction = PageData;
|
|
32
31
|
export declare const PageDataContext: React.Context<{
|
|
@@ -35,10 +34,6 @@ export declare const PageDataContext: React.Context<{
|
|
|
35
34
|
}>;
|
|
36
35
|
export declare function PageDataContextProvider(props: React.PropsWithChildren<PageData>): import("react/jsx-runtime").JSX.Element;
|
|
37
36
|
type PageContainerBarProps = {
|
|
38
|
-
/**
|
|
39
|
-
* The default title of the page.
|
|
40
|
-
*/
|
|
41
|
-
defaultTitle?: string;
|
|
42
37
|
/**
|
|
43
38
|
* The components used for each slot inside.
|
|
44
39
|
*/
|
|
@@ -64,7 +64,7 @@ function reducer(state, action) {
|
|
|
64
64
|
let key;
|
|
65
65
|
let isSame = true;
|
|
66
66
|
for (key in action) {
|
|
67
|
-
if (action[key]
|
|
67
|
+
if (action[key] != state[key]) {
|
|
68
68
|
isSame = false;
|
|
69
69
|
break;
|
|
70
70
|
}
|
|
@@ -76,38 +76,30 @@ function reducer(state, action) {
|
|
|
76
76
|
}
|
|
77
77
|
function PageDataContextProvider(props) {
|
|
78
78
|
// Destruct
|
|
79
|
-
const { title, page, breadcrumbs,
|
|
79
|
+
const { title, page, breadcrumbs, pageHeader, ...rest } = props;
|
|
80
80
|
// useReducer hook to manage state with our reducer function and initial state
|
|
81
81
|
const [state, dispatch] = React.useReducer(reducer, {
|
|
82
82
|
title,
|
|
83
83
|
page,
|
|
84
84
|
breadcrumbs,
|
|
85
|
-
|
|
86
|
-
noPageHeader
|
|
85
|
+
pageHeader
|
|
87
86
|
});
|
|
88
87
|
// Provide the state and dispatch function to the context value
|
|
89
88
|
return (0, jsx_runtime_1.jsx)(exports.PageDataContext.Provider, { value: { state, dispatch }, ...rest });
|
|
90
89
|
}
|
|
91
90
|
function PageContainerBar(props) {
|
|
92
|
-
const {
|
|
93
|
-
const { state
|
|
91
|
+
const { slots, slotProps } = props;
|
|
92
|
+
const { state } = React.useContext(exports.PageDataContext);
|
|
94
93
|
const activePage = (0, useActivePage_1.useActivePage)();
|
|
95
|
-
React.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
return () => {
|
|
96
|
+
// Reset the state when the component unmounts
|
|
97
|
+
state.breadcrumbs = undefined;
|
|
98
|
+
state.page = undefined;
|
|
99
|
+
state.pageHeader = undefined;
|
|
100
|
+
state.title = undefined;
|
|
101
|
+
};
|
|
102
102
|
}, [activePage?.sourcePath]);
|
|
103
|
-
let resolvedBreadcrumbs = state.breadcrumbs ?? activePage?.breadcrumbs ?? [];
|
|
104
|
-
const title = state.title ?? defaultTitle ?? activePage?.title ?? "";
|
|
105
|
-
if (state.page) {
|
|
106
|
-
resolvedBreadcrumbs = [
|
|
107
|
-
...resolvedBreadcrumbs,
|
|
108
|
-
{ title: state.page, path: "#" }
|
|
109
|
-
];
|
|
110
|
-
}
|
|
111
103
|
const ToolbarComponent = slots?.toolbar ?? PageContainerToolbar_1.PageContainerToolbar;
|
|
112
104
|
const toolbarSlotProps = (0, useSlotProps_1.default)({
|
|
113
105
|
elementType: ToolbarComponent,
|
|
@@ -115,11 +107,19 @@ function PageContainerBar(props) {
|
|
|
115
107
|
externalSlotProps: slotProps?.toolbar,
|
|
116
108
|
additionalProps: {}
|
|
117
109
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
110
|
+
const breadcrumbs = state.breadcrumbs ?? activePage?.breadcrumbs ?? [];
|
|
111
|
+
const title = state.title ?? activePage?.title ?? "";
|
|
112
|
+
const pageHeader = state.pageHeader ?? activePage?.pageHeader ?? null;
|
|
113
|
+
if (pageHeader === false)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (pageHeader != null)
|
|
116
|
+
return pageHeader;
|
|
117
|
+
if (state.page) {
|
|
118
|
+
breadcrumbs.push({ title: state.page, path: "#" });
|
|
119
|
+
}
|
|
120
|
+
return ((0, jsx_runtime_1.jsxs)(Stack_1.default, { children: [breadcrumbs && ((0, jsx_runtime_1.jsx)(Breadcrumbs_1.default, { "aria-label": "breadcrumb", children: breadcrumbs.map((item, index) => {
|
|
121
|
+
return index < breadcrumbs.length - 1 ? ((0, jsx_runtime_1.jsx)(Link_1.default, { component: Link_2.Link, underline: "hover", color: "inherit", href: item.path, children: (0, navigation_1.getItemTitle)(item) }, item.path)) : ((0, jsx_runtime_1.jsx)(Typography_1.default, { color: "text.primary", children: (0, navigation_1.getItemTitle)(item) }, item.path));
|
|
122
|
+
}) })), (0, jsx_runtime_1.jsxs)(PageContentHeader, { children: [title ? (0, jsx_runtime_1.jsx)(Typography_1.default, { variant: "h4", children: title }) : null, (0, jsx_runtime_1.jsx)(ToolbarComponent, { ...toolbarSlotProps })] })] }));
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
125
125
|
* A container component to provide a title and breadcrumbs for your pages.
|
|
@@ -133,6 +133,6 @@ function PageContainerBar(props) {
|
|
|
133
133
|
* - [PageContainer API](https://mui.com/toolpad/core/api/page-container)
|
|
134
134
|
*/
|
|
135
135
|
function PageContainer(props) {
|
|
136
|
-
const { children,
|
|
137
|
-
return ((0, jsx_runtime_1.jsxs)(Stack_1.default, { sx: { mx: 3, my: 2 }, spacing: 2, ...rest, children: [(0, jsx_runtime_1.jsx)(PageContainerBar, {
|
|
136
|
+
const { children, slots, slotProps, ...rest } = props;
|
|
137
|
+
return ((0, jsx_runtime_1.jsxs)(Stack_1.default, { sx: { mx: 3, my: 2 }, spacing: 2, ...rest, children: [(0, jsx_runtime_1.jsx)(PageContainerBar, { slots: slots, slotProps: slotProps }), children] }));
|
|
138
138
|
}
|
|
@@ -26,9 +26,6 @@ function isPageItemSelected(navigationItem, basePath, pathname) {
|
|
|
26
26
|
if (navigationItem.pattern) {
|
|
27
27
|
return (0, path_to_regexp_1.pathToRegexp)(`${basePath}/${navigationItem.pattern}`).test(pathname);
|
|
28
28
|
}
|
|
29
|
-
if (navigationItem.subs) {
|
|
30
|
-
return navigationItem.subs.some((sub) => new RegExp(sub).test(pathname));
|
|
31
|
-
}
|
|
32
29
|
return getPageItemFullPath(basePath, navigationItem) === pathname;
|
|
33
30
|
}
|
|
34
31
|
function hasSelectedNavigationChildren(navigationItem, basePath, pathname) {
|
|
@@ -127,15 +124,11 @@ function getItemLookup(navigation) {
|
|
|
127
124
|
function matchPath(navigation, path) {
|
|
128
125
|
const lookup = getItemLookup(navigation);
|
|
129
126
|
for (const [key, item] of lookup.entries()) {
|
|
130
|
-
if (typeof key === "string") {
|
|
131
|
-
|
|
132
|
-
return item;
|
|
133
|
-
else if (item.subs?.some((sub) => new RegExp(sub).test(path)))
|
|
134
|
-
return item;
|
|
127
|
+
if (typeof key === "string" && key === path) {
|
|
128
|
+
return item;
|
|
135
129
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return item;
|
|
130
|
+
if (key instanceof RegExp && key.test(path)) {
|
|
131
|
+
return item;
|
|
139
132
|
}
|
|
140
133
|
}
|
|
141
134
|
return null;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import type { Breadcrumb } from "../PageContainer";
|
|
2
3
|
export interface ActivePage {
|
|
3
4
|
title: string;
|
|
4
5
|
path: string;
|
|
5
6
|
sourcePath: string;
|
|
6
7
|
breadcrumbs: Breadcrumb[];
|
|
8
|
+
pageHeader?: React.ReactNode;
|
|
7
9
|
}
|
|
8
10
|
export declare function useActivePage(): ActivePage | null;
|
|
@@ -77,7 +77,8 @@ function useActivePage() {
|
|
|
77
77
|
title: (0, navigation_1.getItemTitle)(activeItem),
|
|
78
78
|
path: (0, navigation_1.getItemPath)(navigationContext, activeItem),
|
|
79
79
|
sourcePath: pathname,
|
|
80
|
-
breadcrumbs
|
|
80
|
+
breadcrumbs,
|
|
81
|
+
pageHeader: activeItem.pageHeader
|
|
81
82
|
};
|
|
82
83
|
}, [activeItem, rootItem, pathname, navigationContext]);
|
|
83
84
|
}
|
|
@@ -126,6 +126,12 @@ describe("DashboardLayout", () => {
|
|
|
126
126
|
segment: "traffic",
|
|
127
127
|
title: "Traffic",
|
|
128
128
|
icon: _jsx(DescriptionIcon, {})
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
segment: "hidden",
|
|
132
|
+
title: "Hidden",
|
|
133
|
+
icon: _jsx(DescriptionIcon, {}),
|
|
134
|
+
hidden: true
|
|
129
135
|
}
|
|
130
136
|
]
|
|
131
137
|
}
|
|
@@ -141,6 +147,7 @@ describe("DashboardLayout", () => {
|
|
|
141
147
|
});
|
|
142
148
|
expect(within(desktopNavigation).getByText("Sales")).toBeTruthy();
|
|
143
149
|
expect(within(desktopNavigation).getByText("Traffic")).toBeTruthy();
|
|
150
|
+
expect(within(desktopNavigation).queryByText("Hidden")).toBeNull();
|
|
144
151
|
});
|
|
145
152
|
test("shows correct selected page item", () => {
|
|
146
153
|
const NAVIGATION = [
|
|
@@ -152,7 +159,14 @@ describe("DashboardLayout", () => {
|
|
|
152
159
|
{
|
|
153
160
|
title: "Orders",
|
|
154
161
|
segment: "orders",
|
|
155
|
-
icon: _jsx(ShoppingCartIcon, {})
|
|
162
|
+
icon: _jsx(ShoppingCartIcon, {}),
|
|
163
|
+
children: [
|
|
164
|
+
{
|
|
165
|
+
segment: "nested",
|
|
166
|
+
title: "Nested",
|
|
167
|
+
hidden: true
|
|
168
|
+
}
|
|
169
|
+
]
|
|
156
170
|
},
|
|
157
171
|
{
|
|
158
172
|
segment: "dynamic",
|
|
@@ -192,6 +206,8 @@ describe("DashboardLayout", () => {
|
|
|
192
206
|
rerender(_jsx(AppWithPathname, { pathname: "/orders" }));
|
|
193
207
|
expect(within(desktopNavigation).getByRole("link", { name: "Dashboard" })).not.toHaveClass("Mui-selected");
|
|
194
208
|
expect(within(desktopNavigation).getByRole("link", { name: "Orders" })).toHaveClass("Mui-selected");
|
|
209
|
+
rerender(_jsx(AppWithPathname, { pathname: "/orders/nested" }));
|
|
210
|
+
expect(within(desktopNavigation).getByRole("link", { name: "Orders" })).toHaveClass("Mui-selected");
|
|
195
211
|
rerender(_jsx(AppWithPathname, { pathname: "/dynamic" }));
|
|
196
212
|
expect(within(desktopNavigation).getByRole("link", { name: "Dynamic" })).not.toHaveClass("Mui-selected");
|
|
197
213
|
rerender(_jsx(AppWithPathname, { pathname: "/dynamic/123" }));
|
|
@@ -14,10 +14,10 @@ import Tooltip from "@mui/material/Tooltip";
|
|
|
14
14
|
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
|
15
15
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
16
16
|
import { Link } from "../shared/Link";
|
|
17
|
-
import { RouterContext } from "../shared/context";
|
|
18
17
|
import { getItemTitle, getPageItemFullPath, hasSelectedNavigationChildren, isPageItemSelected } from "../shared/navigation";
|
|
19
18
|
import { getDrawerSxTransitionMixin } from "./utils";
|
|
20
19
|
import { styled } from "@mui/material/styles";
|
|
20
|
+
import { useActivePage } from "../useActivePage/useActivePage";
|
|
21
21
|
const NavigationListItemButton = styled(ListItemButton)(({ theme }) => ({
|
|
22
22
|
borderRadius: 8,
|
|
23
23
|
"&.Mui-selected": {
|
|
@@ -48,8 +48,8 @@ const NavigationListItemButton = styled(ListItemButton)(({ theme }) => ({
|
|
|
48
48
|
* @ignore - internal component.
|
|
49
49
|
*/
|
|
50
50
|
function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0, onLinkClick, isMini = false, isFullyExpanded = true, hasDrawerTransitions = false, selectedItemId }) {
|
|
51
|
-
const
|
|
52
|
-
const pathname =
|
|
51
|
+
const activePage = useActivePage();
|
|
52
|
+
const pathname = activePage?.sourcePath ?? "/";
|
|
53
53
|
const initialExpandedSidebarItemIds = React.useMemo(() => subNavigation
|
|
54
54
|
.map((navigationItem, navigationItemIndex) => ({
|
|
55
55
|
navigationItem,
|
|
@@ -91,6 +91,13 @@ function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0
|
|
|
91
91
|
: {})
|
|
92
92
|
} }, `divider-${depth}-${navigationItemIndex}`));
|
|
93
93
|
}
|
|
94
|
+
if (navigationItem.hidden) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
let children = navigationItem.children?.filter((child) => child.kind === "divider" || child.kind === "header" || !child.hidden);
|
|
98
|
+
if (children && children.length === 0) {
|
|
99
|
+
children = undefined;
|
|
100
|
+
}
|
|
94
101
|
const navigationItemFullPath = getPageItemFullPath(basePath, navigationItem);
|
|
95
102
|
const navigationItemId = `${depth}-${navigationItemIndex}`;
|
|
96
103
|
const navigationItemTitle = getItemTitle(navigationItem);
|
|
@@ -100,7 +107,10 @@ function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0
|
|
|
100
107
|
// If the item is selected, we don't want to select more
|
|
101
108
|
const isSelected = selectedItemId
|
|
102
109
|
? false
|
|
103
|
-
: isPageItemSelected(navigationItem, basePath, pathname)
|
|
110
|
+
: isPageItemSelected(navigationItem, basePath, pathname) ||
|
|
111
|
+
navigationItem.children?.some((child) => (child.kind === "page" || child.kind == null) &&
|
|
112
|
+
child.hidden &&
|
|
113
|
+
isPageItemSelected(child, navigationItemFullPath, pathname));
|
|
104
114
|
if (isSelected && !selectedItemId) {
|
|
105
115
|
selectedItemId = navigationItemId;
|
|
106
116
|
}
|
|
@@ -108,10 +118,10 @@ function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0
|
|
|
108
118
|
py: 0,
|
|
109
119
|
px: 1,
|
|
110
120
|
overflowX: "hidden"
|
|
111
|
-
}, children: _jsxs(NavigationListItemButton, { selected: isSelected && (!
|
|
121
|
+
}, children: _jsxs(NavigationListItemButton, { selected: isSelected && (!children || isMini), sx: {
|
|
112
122
|
px: 1.4,
|
|
113
123
|
height: 48
|
|
114
|
-
}, ...(
|
|
124
|
+
}, ...(children && !isMini
|
|
115
125
|
? {
|
|
116
126
|
onClick: handleOpenFolderClick(navigationItemId)
|
|
117
127
|
}
|
|
@@ -138,10 +148,10 @@ function DashboardSidebarSubNavigation({ subNavigation, basePath = "", depth = 0
|
|
|
138
148
|
}
|
|
139
149
|
} }), navigationItem.action && !isMini && isFullyExpanded
|
|
140
150
|
? navigationItem.action
|
|
141
|
-
: null,
|
|
151
|
+
: null, children && !isMini && isFullyExpanded
|
|
142
152
|
? nestedNavigationCollapseIcon
|
|
143
153
|
: null] }) }));
|
|
144
|
-
return (_jsxs(React.Fragment, { children: [isMini ? (_jsx(Tooltip, { title: navigationItemTitle, placement: "right", children: listItem })) : (listItem),
|
|
154
|
+
return (_jsxs(React.Fragment, { children: [isMini ? (_jsx(Tooltip, { title: navigationItemTitle, placement: "right", children: listItem })) : (listItem), children && !isMini ? (_jsx(Collapse, { in: isNestedNavigationExpanded, timeout: "auto", unmountOnExit: true, children: _jsx(DashboardSidebarSubNavigation, { subNavigation: children, basePath: navigationItemFullPath, depth: depth + 1, onLinkClick: onLinkClick, selectedItemId: selectedItemId }) })) : null] }, navigationItemId));
|
|
145
155
|
}) }));
|
|
146
156
|
}
|
|
147
157
|
export { DashboardSidebarSubNavigation };
|
|
@@ -25,8 +25,7 @@ export type PageData = {
|
|
|
25
25
|
title?: string;
|
|
26
26
|
page?: string;
|
|
27
27
|
breadcrumbs?: Breadcrumb[];
|
|
28
|
-
|
|
29
|
-
noPageHeader?: boolean;
|
|
28
|
+
pageHeader?: React.ReactNode;
|
|
30
29
|
};
|
|
31
30
|
type PageDataAction = PageData;
|
|
32
31
|
export declare const PageDataContext: React.Context<{
|
|
@@ -35,10 +34,6 @@ export declare const PageDataContext: React.Context<{
|
|
|
35
34
|
}>;
|
|
36
35
|
export declare function PageDataContextProvider(props: React.PropsWithChildren<PageData>): import("react/jsx-runtime").JSX.Element;
|
|
37
36
|
type PageContainerBarProps = {
|
|
38
|
-
/**
|
|
39
|
-
* The default title of the page.
|
|
40
|
-
*/
|
|
41
|
-
defaultTitle?: string;
|
|
42
37
|
/**
|
|
43
38
|
* The components used for each slot inside.
|
|
44
39
|
*/
|
|
@@ -23,7 +23,7 @@ function reducer(state, action) {
|
|
|
23
23
|
let key;
|
|
24
24
|
let isSame = true;
|
|
25
25
|
for (key in action) {
|
|
26
|
-
if (action[key]
|
|
26
|
+
if (action[key] != state[key]) {
|
|
27
27
|
isSame = false;
|
|
28
28
|
break;
|
|
29
29
|
}
|
|
@@ -35,38 +35,30 @@ function reducer(state, action) {
|
|
|
35
35
|
}
|
|
36
36
|
export function PageDataContextProvider(props) {
|
|
37
37
|
// Destruct
|
|
38
|
-
const { title, page, breadcrumbs,
|
|
38
|
+
const { title, page, breadcrumbs, pageHeader, ...rest } = props;
|
|
39
39
|
// useReducer hook to manage state with our reducer function and initial state
|
|
40
40
|
const [state, dispatch] = React.useReducer(reducer, {
|
|
41
41
|
title,
|
|
42
42
|
page,
|
|
43
43
|
breadcrumbs,
|
|
44
|
-
|
|
45
|
-
noPageHeader
|
|
44
|
+
pageHeader
|
|
46
45
|
});
|
|
47
46
|
// Provide the state and dispatch function to the context value
|
|
48
47
|
return _jsx(PageDataContext.Provider, { value: { state, dispatch }, ...rest });
|
|
49
48
|
}
|
|
50
49
|
function PageContainerBar(props) {
|
|
51
|
-
const {
|
|
52
|
-
const { state
|
|
50
|
+
const { slots, slotProps } = props;
|
|
51
|
+
const { state } = React.useContext(PageDataContext);
|
|
53
52
|
const activePage = useActivePage();
|
|
54
|
-
React.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
return () => {
|
|
55
|
+
// Reset the state when the component unmounts
|
|
56
|
+
state.breadcrumbs = undefined;
|
|
57
|
+
state.page = undefined;
|
|
58
|
+
state.pageHeader = undefined;
|
|
59
|
+
state.title = undefined;
|
|
60
|
+
};
|
|
61
61
|
}, [activePage?.sourcePath]);
|
|
62
|
-
let resolvedBreadcrumbs = state.breadcrumbs ?? activePage?.breadcrumbs ?? [];
|
|
63
|
-
const title = state.title ?? defaultTitle ?? activePage?.title ?? "";
|
|
64
|
-
if (state.page) {
|
|
65
|
-
resolvedBreadcrumbs = [
|
|
66
|
-
...resolvedBreadcrumbs,
|
|
67
|
-
{ title: state.page, path: "#" }
|
|
68
|
-
];
|
|
69
|
-
}
|
|
70
62
|
const ToolbarComponent = slots?.toolbar ?? PageContainerToolbar;
|
|
71
63
|
const toolbarSlotProps = useSlotProps({
|
|
72
64
|
elementType: ToolbarComponent,
|
|
@@ -74,11 +66,19 @@ function PageContainerBar(props) {
|
|
|
74
66
|
externalSlotProps: slotProps?.toolbar,
|
|
75
67
|
additionalProps: {}
|
|
76
68
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
const breadcrumbs = state.breadcrumbs ?? activePage?.breadcrumbs ?? [];
|
|
70
|
+
const title = state.title ?? activePage?.title ?? "";
|
|
71
|
+
const pageHeader = state.pageHeader ?? activePage?.pageHeader ?? null;
|
|
72
|
+
if (pageHeader === false)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (pageHeader != null)
|
|
75
|
+
return pageHeader;
|
|
76
|
+
if (state.page) {
|
|
77
|
+
breadcrumbs.push({ title: state.page, path: "#" });
|
|
78
|
+
}
|
|
79
|
+
return (_jsxs(Stack, { children: [breadcrumbs && (_jsx(Breadcrumbs, { "aria-label": "breadcrumb", children: breadcrumbs.map((item, index) => {
|
|
80
|
+
return index < breadcrumbs.length - 1 ? (_jsx(Link, { component: ToolpadLink, underline: "hover", color: "inherit", href: item.path, children: getItemTitle(item) }, item.path)) : (_jsx(Typography, { color: "text.primary", children: getItemTitle(item) }, item.path));
|
|
81
|
+
}) })), _jsxs(PageContentHeader, { children: [title ? _jsx(Typography, { variant: "h4", children: title }) : null, _jsx(ToolbarComponent, { ...toolbarSlotProps })] })] }));
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
84
|
* A container component to provide a title and breadcrumbs for your pages.
|
|
@@ -92,7 +92,7 @@ function PageContainerBar(props) {
|
|
|
92
92
|
* - [PageContainer API](https://mui.com/toolpad/core/api/page-container)
|
|
93
93
|
*/
|
|
94
94
|
function PageContainer(props) {
|
|
95
|
-
const { children,
|
|
96
|
-
return (_jsxs(Stack, { sx: { mx: 3, my: 2 }, spacing: 2, ...rest, children: [_jsx(PageContainerBar, {
|
|
95
|
+
const { children, slots, slotProps, ...rest } = props;
|
|
96
|
+
return (_jsxs(Stack, { sx: { mx: 3, my: 2 }, spacing: 2, ...rest, children: [_jsx(PageContainerBar, { slots: slots, slotProps: slotProps }), children] }));
|
|
97
97
|
}
|
|
98
98
|
export { PageContainer };
|
|
@@ -12,9 +12,6 @@ export function isPageItemSelected(navigationItem, basePath, pathname) {
|
|
|
12
12
|
if (navigationItem.pattern) {
|
|
13
13
|
return pathToRegexp(`${basePath}/${navigationItem.pattern}`).test(pathname);
|
|
14
14
|
}
|
|
15
|
-
if (navigationItem.subs) {
|
|
16
|
-
return navigationItem.subs.some((sub) => new RegExp(sub).test(pathname));
|
|
17
|
-
}
|
|
18
15
|
return getPageItemFullPath(basePath, navigationItem) === pathname;
|
|
19
16
|
}
|
|
20
17
|
export function hasSelectedNavigationChildren(navigationItem, basePath, pathname) {
|
|
@@ -113,15 +110,11 @@ function getItemLookup(navigation) {
|
|
|
113
110
|
export function matchPath(navigation, path) {
|
|
114
111
|
const lookup = getItemLookup(navigation);
|
|
115
112
|
for (const [key, item] of lookup.entries()) {
|
|
116
|
-
if (typeof key === "string") {
|
|
117
|
-
|
|
118
|
-
return item;
|
|
119
|
-
else if (item.subs?.some((sub) => new RegExp(sub).test(path)))
|
|
120
|
-
return item;
|
|
113
|
+
if (typeof key === "string" && key === path) {
|
|
114
|
+
return item;
|
|
121
115
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return item;
|
|
116
|
+
if (key instanceof RegExp && key.test(path)) {
|
|
117
|
+
return item;
|
|
125
118
|
}
|
|
126
119
|
}
|
|
127
120
|
return null;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import type { Breadcrumb } from "../PageContainer";
|
|
2
3
|
export interface ActivePage {
|
|
3
4
|
title: string;
|
|
4
5
|
path: string;
|
|
5
6
|
sourcePath: string;
|
|
6
7
|
breadcrumbs: Breadcrumb[];
|
|
8
|
+
pageHeader?: React.ReactNode;
|
|
7
9
|
}
|
|
8
10
|
export declare function useActivePage(): ActivePage | null;
|
|
@@ -41,7 +41,8 @@ export function useActivePage() {
|
|
|
41
41
|
title: getItemTitle(activeItem),
|
|
42
42
|
path: getItemPath(navigationContext, activeItem),
|
|
43
43
|
sourcePath: pathname,
|
|
44
|
-
breadcrumbs
|
|
44
|
+
breadcrumbs,
|
|
45
|
+
pageHeader: activeItem.pageHeader
|
|
45
46
|
};
|
|
46
47
|
}, [activeItem, rootItem, pathname, navigationContext]);
|
|
47
48
|
}
|
package/package.json
CHANGED
|
@@ -175,6 +175,12 @@ describe("DashboardLayout", () => {
|
|
|
175
175
|
segment: "traffic",
|
|
176
176
|
title: "Traffic",
|
|
177
177
|
icon: <DescriptionIcon />
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
segment: "hidden",
|
|
181
|
+
title: "Hidden",
|
|
182
|
+
icon: <DescriptionIcon />,
|
|
183
|
+
hidden: true
|
|
178
184
|
}
|
|
179
185
|
]
|
|
180
186
|
}
|
|
@@ -198,6 +204,7 @@ describe("DashboardLayout", () => {
|
|
|
198
204
|
|
|
199
205
|
expect(within(desktopNavigation).getByText("Sales")).toBeTruthy();
|
|
200
206
|
expect(within(desktopNavigation).getByText("Traffic")).toBeTruthy();
|
|
207
|
+
expect(within(desktopNavigation).queryByText("Hidden")).toBeNull();
|
|
201
208
|
});
|
|
202
209
|
|
|
203
210
|
test("shows correct selected page item", () => {
|
|
@@ -210,7 +217,14 @@ describe("DashboardLayout", () => {
|
|
|
210
217
|
{
|
|
211
218
|
title: "Orders",
|
|
212
219
|
segment: "orders",
|
|
213
|
-
icon: <ShoppingCartIcon
|
|
220
|
+
icon: <ShoppingCartIcon />,
|
|
221
|
+
children: [
|
|
222
|
+
{
|
|
223
|
+
segment: "nested",
|
|
224
|
+
title: "Nested",
|
|
225
|
+
hidden: true
|
|
226
|
+
}
|
|
227
|
+
]
|
|
214
228
|
},
|
|
215
229
|
{
|
|
216
230
|
segment: "dynamic",
|
|
@@ -268,6 +282,11 @@ describe("DashboardLayout", () => {
|
|
|
268
282
|
within(desktopNavigation).getByRole("link", { name: "Orders" })
|
|
269
283
|
).toHaveClass("Mui-selected");
|
|
270
284
|
|
|
285
|
+
rerender(<AppWithPathname pathname="/orders/nested" />);
|
|
286
|
+
expect(
|
|
287
|
+
within(desktopNavigation).getByRole("link", { name: "Orders" })
|
|
288
|
+
).toHaveClass("Mui-selected");
|
|
289
|
+
|
|
271
290
|
rerender(<AppWithPathname pathname="/dynamic" />);
|
|
272
291
|
expect(
|
|
273
292
|
within(desktopNavigation).getByRole("link", { name: "Dynamic" })
|
|
@@ -14,7 +14,6 @@ import type {} from "@mui/material/themeCssVarsAugmentation";
|
|
|
14
14
|
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
|
15
15
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
16
16
|
import { Link } from "../shared/Link";
|
|
17
|
-
import { RouterContext } from "../shared/context";
|
|
18
17
|
import type { Navigation } from "../AppProvider";
|
|
19
18
|
import {
|
|
20
19
|
getItemTitle,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
} from "../shared/navigation";
|
|
25
24
|
import { getDrawerSxTransitionMixin } from "./utils";
|
|
26
25
|
import { styled } from "@mui/material/styles";
|
|
26
|
+
import { useActivePage } from "../useActivePage/useActivePage";
|
|
27
27
|
|
|
28
28
|
const NavigationListItemButton = styled(ListItemButton)(({ theme }) => ({
|
|
29
29
|
borderRadius: 8,
|
|
@@ -76,9 +76,9 @@ function DashboardSidebarSubNavigation({
|
|
|
76
76
|
hasDrawerTransitions = false,
|
|
77
77
|
selectedItemId
|
|
78
78
|
}: DashboardSidebarSubNavigationProps) {
|
|
79
|
-
const
|
|
79
|
+
const activePage = useActivePage();
|
|
80
80
|
|
|
81
|
-
const pathname =
|
|
81
|
+
const pathname = activePage?.sourcePath ?? "/";
|
|
82
82
|
|
|
83
83
|
const initialExpandedSidebarItemIds = React.useMemo(
|
|
84
84
|
() =>
|
|
@@ -157,6 +157,18 @@ function DashboardSidebarSubNavigation({
|
|
|
157
157
|
);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
if (navigationItem.hidden) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let children = navigationItem.children?.filter(
|
|
165
|
+
(child) =>
|
|
166
|
+
child.kind === "divider" || child.kind === "header" || !child.hidden
|
|
167
|
+
);
|
|
168
|
+
if (children && children.length === 0) {
|
|
169
|
+
children = undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
160
172
|
const navigationItemFullPath = getPageItemFullPath(
|
|
161
173
|
basePath,
|
|
162
174
|
navigationItem
|
|
@@ -178,7 +190,13 @@ function DashboardSidebarSubNavigation({
|
|
|
178
190
|
// If the item is selected, we don't want to select more
|
|
179
191
|
const isSelected = selectedItemId
|
|
180
192
|
? false
|
|
181
|
-
: isPageItemSelected(navigationItem, basePath, pathname)
|
|
193
|
+
: isPageItemSelected(navigationItem, basePath, pathname) ||
|
|
194
|
+
navigationItem.children?.some(
|
|
195
|
+
(child) =>
|
|
196
|
+
(child.kind === "page" || child.kind == null) &&
|
|
197
|
+
child.hidden &&
|
|
198
|
+
isPageItemSelected(child, navigationItemFullPath, pathname)
|
|
199
|
+
);
|
|
182
200
|
|
|
183
201
|
if (isSelected && !selectedItemId) {
|
|
184
202
|
selectedItemId = navigationItemId;
|
|
@@ -193,12 +211,12 @@ function DashboardSidebarSubNavigation({
|
|
|
193
211
|
}}
|
|
194
212
|
>
|
|
195
213
|
<NavigationListItemButton
|
|
196
|
-
selected={isSelected && (!
|
|
214
|
+
selected={isSelected && (!children || isMini)}
|
|
197
215
|
sx={{
|
|
198
216
|
px: 1.4,
|
|
199
217
|
height: 48
|
|
200
218
|
}}
|
|
201
|
-
{...(
|
|
219
|
+
{...(children && !isMini
|
|
202
220
|
? {
|
|
203
221
|
onClick: handleOpenFolderClick(navigationItemId)
|
|
204
222
|
}
|
|
@@ -248,7 +266,7 @@ function DashboardSidebarSubNavigation({
|
|
|
248
266
|
{navigationItem.action && !isMini && isFullyExpanded
|
|
249
267
|
? navigationItem.action
|
|
250
268
|
: null}
|
|
251
|
-
{
|
|
269
|
+
{children && !isMini && isFullyExpanded
|
|
252
270
|
? nestedNavigationCollapseIcon
|
|
253
271
|
: null}
|
|
254
272
|
</NavigationListItemButton>
|
|
@@ -265,14 +283,14 @@ function DashboardSidebarSubNavigation({
|
|
|
265
283
|
listItem
|
|
266
284
|
)}
|
|
267
285
|
|
|
268
|
-
{
|
|
286
|
+
{children && !isMini ? (
|
|
269
287
|
<Collapse
|
|
270
288
|
in={isNestedNavigationExpanded}
|
|
271
289
|
timeout="auto"
|
|
272
290
|
unmountOnExit
|
|
273
291
|
>
|
|
274
292
|
<DashboardSidebarSubNavigation
|
|
275
|
-
subNavigation={
|
|
293
|
+
subNavigation={children}
|
|
276
294
|
basePath={navigationItemFullPath}
|
|
277
295
|
depth={depth + 1}
|
|
278
296
|
onLinkClick={onLinkClick}
|
|
@@ -48,8 +48,7 @@ export type PageData = {
|
|
|
48
48
|
title?: string;
|
|
49
49
|
page?: string;
|
|
50
50
|
breadcrumbs?: Breadcrumb[];
|
|
51
|
-
|
|
52
|
-
noPageHeader?: boolean;
|
|
51
|
+
pageHeader?: React.ReactNode;
|
|
53
52
|
};
|
|
54
53
|
|
|
55
54
|
type PageDataAction = PageData;
|
|
@@ -64,7 +63,7 @@ function reducer(state: PageData, action: PageDataAction) {
|
|
|
64
63
|
let key: keyof PageDataAction;
|
|
65
64
|
let isSame = true;
|
|
66
65
|
for (key in action) {
|
|
67
|
-
if (action[key]
|
|
66
|
+
if (action[key] != state[key]) {
|
|
68
67
|
isSame = false;
|
|
69
68
|
break;
|
|
70
69
|
}
|
|
@@ -81,16 +80,14 @@ export function PageDataContextProvider(
|
|
|
81
80
|
props: React.PropsWithChildren<PageData>
|
|
82
81
|
) {
|
|
83
82
|
// Destruct
|
|
84
|
-
const { title, page, breadcrumbs,
|
|
85
|
-
props;
|
|
83
|
+
const { title, page, breadcrumbs, pageHeader, ...rest } = props;
|
|
86
84
|
|
|
87
85
|
// useReducer hook to manage state with our reducer function and initial state
|
|
88
86
|
const [state, dispatch] = React.useReducer(reducer, {
|
|
89
87
|
title,
|
|
90
88
|
page,
|
|
91
89
|
breadcrumbs,
|
|
92
|
-
|
|
93
|
-
noPageHeader
|
|
90
|
+
pageHeader
|
|
94
91
|
});
|
|
95
92
|
|
|
96
93
|
// Provide the state and dispatch function to the context value
|
|
@@ -98,10 +95,6 @@ export function PageDataContextProvider(
|
|
|
98
95
|
}
|
|
99
96
|
|
|
100
97
|
type PageContainerBarProps = {
|
|
101
|
-
/**
|
|
102
|
-
* The default title of the page.
|
|
103
|
-
*/
|
|
104
|
-
defaultTitle?: string;
|
|
105
98
|
/**
|
|
106
99
|
* The components used for each slot inside.
|
|
107
100
|
*/
|
|
@@ -113,31 +106,22 @@ type PageContainerBarProps = {
|
|
|
113
106
|
};
|
|
114
107
|
|
|
115
108
|
function PageContainerBar(props: PageContainerBarProps) {
|
|
116
|
-
const {
|
|
109
|
+
const { slots, slotProps } = props;
|
|
117
110
|
|
|
118
|
-
const { state
|
|
111
|
+
const { state } = React.useContext(PageDataContext);
|
|
119
112
|
|
|
120
113
|
const activePage = useActivePage();
|
|
121
114
|
|
|
122
|
-
React.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
return () => {
|
|
117
|
+
// Reset the state when the component unmounts
|
|
118
|
+
state.breadcrumbs = undefined;
|
|
119
|
+
state.page = undefined;
|
|
120
|
+
state.pageHeader = undefined;
|
|
121
|
+
state.title = undefined;
|
|
122
|
+
};
|
|
129
123
|
}, [activePage?.sourcePath]);
|
|
130
124
|
|
|
131
|
-
let resolvedBreadcrumbs = state.breadcrumbs ?? activePage?.breadcrumbs ?? [];
|
|
132
|
-
const title = state.title ?? defaultTitle ?? activePage?.title ?? "";
|
|
133
|
-
|
|
134
|
-
if (state.page) {
|
|
135
|
-
resolvedBreadcrumbs = [
|
|
136
|
-
...resolvedBreadcrumbs,
|
|
137
|
-
{ title: state.page, path: "#" }
|
|
138
|
-
];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
125
|
const ToolbarComponent = slots?.toolbar ?? PageContainerToolbar;
|
|
142
126
|
const toolbarSlotProps = useSlotProps({
|
|
143
127
|
elementType: ToolbarComponent,
|
|
@@ -146,29 +130,38 @@ function PageContainerBar(props: PageContainerBarProps) {
|
|
|
146
130
|
additionalProps: {}
|
|
147
131
|
});
|
|
148
132
|
|
|
149
|
-
|
|
133
|
+
const breadcrumbs = state.breadcrumbs ?? activePage?.breadcrumbs ?? [];
|
|
134
|
+
const title = state.title ?? activePage?.title ?? "";
|
|
135
|
+
const pageHeader = state.pageHeader ?? activePage?.pageHeader ?? null;
|
|
136
|
+
|
|
137
|
+
if (pageHeader === false) return undefined;
|
|
138
|
+
if (pageHeader != null) return pageHeader;
|
|
139
|
+
|
|
140
|
+
if (state.page) {
|
|
141
|
+
breadcrumbs.push({ title: state.page, path: "#" });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
150
145
|
<Stack>
|
|
151
|
-
{
|
|
146
|
+
{breadcrumbs && (
|
|
152
147
|
<Breadcrumbs aria-label="breadcrumb">
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
})
|
|
171
|
-
: null}
|
|
148
|
+
{breadcrumbs.map((item, index) => {
|
|
149
|
+
return index < breadcrumbs.length - 1 ? (
|
|
150
|
+
<Link
|
|
151
|
+
key={item.path}
|
|
152
|
+
component={ToolpadLink}
|
|
153
|
+
underline="hover"
|
|
154
|
+
color="inherit"
|
|
155
|
+
href={item.path}
|
|
156
|
+
>
|
|
157
|
+
{getItemTitle(item)}
|
|
158
|
+
</Link>
|
|
159
|
+
) : (
|
|
160
|
+
<Typography key={item.path} color="text.primary">
|
|
161
|
+
{getItemTitle(item)}
|
|
162
|
+
</Typography>
|
|
163
|
+
);
|
|
164
|
+
})}
|
|
172
165
|
</Breadcrumbs>
|
|
173
166
|
)}
|
|
174
167
|
<PageContentHeader>
|
|
@@ -176,7 +169,7 @@ function PageContainerBar(props: PageContainerBarProps) {
|
|
|
176
169
|
<ToolbarComponent {...toolbarSlotProps} />
|
|
177
170
|
</PageContentHeader>
|
|
178
171
|
</Stack>
|
|
179
|
-
)
|
|
172
|
+
);
|
|
180
173
|
}
|
|
181
174
|
|
|
182
175
|
export type PageContainerProps = React.PropsWithChildren<
|
|
@@ -195,15 +188,11 @@ export type PageContainerProps = React.PropsWithChildren<
|
|
|
195
188
|
* - [PageContainer API](https://mui.com/toolpad/core/api/page-container)
|
|
196
189
|
*/
|
|
197
190
|
function PageContainer(props: PageContainerProps) {
|
|
198
|
-
const { children,
|
|
191
|
+
const { children, slots, slotProps, ...rest } = props;
|
|
199
192
|
|
|
200
193
|
return (
|
|
201
194
|
<Stack sx={{ mx: 3, my: 2 }} spacing={2} {...rest}>
|
|
202
|
-
<PageContainerBar
|
|
203
|
-
defaultTitle={defaultTitle}
|
|
204
|
-
slots={slots}
|
|
205
|
-
slotProps={slotProps}
|
|
206
|
-
/>
|
|
195
|
+
<PageContainerBar slots={slots} slotProps={slotProps} />
|
|
207
196
|
{children}
|
|
208
197
|
</Stack>
|
|
209
198
|
);
|
|
@@ -36,10 +36,6 @@ export function isPageItemSelected(
|
|
|
36
36
|
return pathToRegexp(`${basePath}/${navigationItem.pattern}`).test(pathname);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
if (navigationItem.subs) {
|
|
40
|
-
return navigationItem.subs.some((sub) => new RegExp(sub).test(pathname));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
39
|
return getPageItemFullPath(basePath, navigationItem) === pathname;
|
|
44
40
|
}
|
|
45
41
|
|
|
@@ -178,12 +174,11 @@ export function matchPath(
|
|
|
178
174
|
const lookup = getItemLookup(navigation);
|
|
179
175
|
|
|
180
176
|
for (const [key, item] of lookup.entries()) {
|
|
181
|
-
if (typeof key === "string") {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (key.test(path)) return item;
|
|
177
|
+
if (typeof key === "string" && key === path) {
|
|
178
|
+
return item;
|
|
179
|
+
}
|
|
180
|
+
if (key instanceof RegExp && key.test(path)) {
|
|
181
|
+
return item;
|
|
187
182
|
}
|
|
188
183
|
}
|
|
189
184
|
|
|
@@ -9,11 +9,13 @@ export interface ActivePage {
|
|
|
9
9
|
path: string;
|
|
10
10
|
sourcePath: string;
|
|
11
11
|
breadcrumbs: Breadcrumb[];
|
|
12
|
+
pageHeader?: React.ReactNode;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function useActivePage(): ActivePage | null {
|
|
15
16
|
const navigationContext = React.useContext(NavigationContext);
|
|
16
17
|
const routerContext = React.useContext(RouterContext);
|
|
18
|
+
|
|
17
19
|
const pathname = routerContext?.pathname ?? "/";
|
|
18
20
|
const activeItem = matchPath(navigationContext, pathname);
|
|
19
21
|
|
|
@@ -56,7 +58,8 @@ export function useActivePage(): ActivePage | null {
|
|
|
56
58
|
title: getItemTitle(activeItem),
|
|
57
59
|
path: getItemPath(navigationContext, activeItem),
|
|
58
60
|
sourcePath: pathname,
|
|
59
|
-
breadcrumbs
|
|
61
|
+
breadcrumbs,
|
|
62
|
+
pageHeader: activeItem.pageHeader
|
|
60
63
|
};
|
|
61
64
|
}, [activeItem, rootItem, pathname, navigationContext]);
|
|
62
65
|
}
|