@etsoo/toolpad 1.0.28 → 1.0.29

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,7 +27,7 @@ export interface NavigationPageItem {
27
27
  pattern?: string;
28
28
  action?: React.ReactNode;
29
29
  children?: Navigation;
30
- subs?: string[];
30
+ hidden?: boolean;
31
31
  }
32
32
  export interface NavigationSubheaderItem {
33
33
  kind: "header";
@@ -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 routerContext = React.useContext(context_1.RouterContext);
91
- const pathname = routerContext?.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 && (!navigationItem.children || isMini), sx: {
160
+ }, children: (0, jsx_runtime_1.jsxs)(NavigationListItemButton, { selected: isSelected && (!children || isMini), sx: {
151
161
  px: 1.4,
152
162
  height: 48
153
- }, ...(navigationItem.children && !isMini
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, navigationItem.children && !isMini && isFullyExpanded
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), navigationItem.children && !isMini ? ((0, jsx_runtime_1.jsx)(Collapse_1.default, { in: isNestedNavigationExpanded, timeout: "auto", unmountOnExit: true, children: (0, jsx_runtime_1.jsx)(DashboardSidebarSubNavigation, { subNavigation: navigationItem.children, basePath: navigationItemFullPath, depth: depth + 1, onLinkClick: onLinkClick, selectedItemId: selectedItemId }) })) : null] }, navigationItemId));
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
  }
@@ -90,10 +90,10 @@ function PageDataContextProvider(props) {
90
90
  }
91
91
  function PageContainerBar(props) {
92
92
  const { defaultTitle, slots, slotProps } = props;
93
- const { state, dispatch } = React.useContext(exports.PageDataContext);
93
+ const { state } = React.useContext(exports.PageDataContext);
94
94
  const activePage = (0, useActivePage_1.useActivePage)();
95
95
  React.useLayoutEffect(() => {
96
- // Reset the state when the active page changes without rerendering
96
+ // Reset the state without rerendering
97
97
  state.breadcrumbs = undefined;
98
98
  state.noBreadcrumbs = undefined;
99
99
  state.noPageHeader = undefined;
@@ -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
- if (key === path)
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
- else if (key instanceof RegExp) {
137
- if (key.test(path))
138
- return item;
130
+ if (key instanceof RegExp && key.test(path)) {
131
+ return item;
139
132
  }
140
133
  }
141
134
  return null;
@@ -41,13 +41,14 @@ const navigation_1 = require("../shared/navigation");
41
41
  function useActivePage() {
42
42
  const navigationContext = React.useContext(context_1.NavigationContext);
43
43
  const routerContext = React.useContext(context_1.RouterContext);
44
- const pathname = routerContext?.pathname ?? "/";
44
+ const pageRef = React.useRef(null);
45
+ let pathname = routerContext?.pathname ?? "/";
45
46
  const activeItem = (0, navigation_1.matchPath)(navigationContext, pathname);
46
47
  const rootItem = (0, navigation_1.matchPath)(navigationContext, "/");
47
- return React.useMemo(() => {
48
- if (!activeItem) {
49
- return null;
50
- }
48
+ if (!activeItem) {
49
+ pageRef.current = null;
50
+ }
51
+ else {
51
52
  const breadcrumbs = [];
52
53
  if (rootItem) {
53
54
  breadcrumbs.push({
@@ -73,11 +74,19 @@ function useActivePage() {
73
74
  });
74
75
  }
75
76
  }
76
- return {
77
- title: (0, navigation_1.getItemTitle)(activeItem),
78
- path: (0, navigation_1.getItemPath)(navigationContext, activeItem),
79
- sourcePath: pathname,
80
- breadcrumbs
81
- };
82
- }, [activeItem, rootItem, pathname, navigationContext]);
77
+ const title = (0, navigation_1.getItemTitle)(activeItem);
78
+ const path = (0, navigation_1.getItemPath)(navigationContext, activeItem);
79
+ if (pageRef.current == null ||
80
+ pageRef.current.title !== title ||
81
+ pageRef.current.path !== path ||
82
+ pageRef.current.sourcePath !== pathname) {
83
+ pageRef.current = {
84
+ title,
85
+ path,
86
+ sourcePath: pathname,
87
+ breadcrumbs
88
+ };
89
+ }
90
+ }
91
+ return pageRef.current;
83
92
  }
@@ -27,7 +27,7 @@ export interface NavigationPageItem {
27
27
  pattern?: string;
28
28
  action?: React.ReactNode;
29
29
  children?: Navigation;
30
- subs?: string[];
30
+ hidden?: boolean;
31
31
  }
32
32
  export interface NavigationSubheaderItem {
33
33
  kind: "header";
@@ -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 routerContext = React.useContext(RouterContext);
52
- const pathname = routerContext?.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 && (!navigationItem.children || isMini), sx: {
121
+ }, children: _jsxs(NavigationListItemButton, { selected: isSelected && (!children || isMini), sx: {
112
122
  px: 1.4,
113
123
  height: 48
114
- }, ...(navigationItem.children && !isMini
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, navigationItem.children && !isMini && isFullyExpanded
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), navigationItem.children && !isMini ? (_jsx(Collapse, { in: isNestedNavigationExpanded, timeout: "auto", unmountOnExit: true, children: _jsx(DashboardSidebarSubNavigation, { subNavigation: navigationItem.children, basePath: navigationItemFullPath, depth: depth + 1, onLinkClick: onLinkClick, selectedItemId: selectedItemId }) })) : null] }, navigationItemId));
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 };
@@ -49,10 +49,10 @@ export function PageDataContextProvider(props) {
49
49
  }
50
50
  function PageContainerBar(props) {
51
51
  const { defaultTitle, slots, slotProps } = props;
52
- const { state, dispatch } = React.useContext(PageDataContext);
52
+ const { state } = React.useContext(PageDataContext);
53
53
  const activePage = useActivePage();
54
54
  React.useLayoutEffect(() => {
55
- // Reset the state when the active page changes without rerendering
55
+ // Reset the state without rerendering
56
56
  state.breadcrumbs = undefined;
57
57
  state.noBreadcrumbs = undefined;
58
58
  state.noPageHeader = undefined;
@@ -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
- if (key === path)
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
- else if (key instanceof RegExp) {
123
- if (key.test(path))
124
- return item;
116
+ if (key instanceof RegExp && key.test(path)) {
117
+ return item;
125
118
  }
126
119
  }
127
120
  return null;
@@ -5,13 +5,14 @@ import { getItemPath, getItemTitle, matchPath } from "../shared/navigation";
5
5
  export function useActivePage() {
6
6
  const navigationContext = React.useContext(NavigationContext);
7
7
  const routerContext = React.useContext(RouterContext);
8
- const pathname = routerContext?.pathname ?? "/";
8
+ const pageRef = React.useRef(null);
9
+ let pathname = routerContext?.pathname ?? "/";
9
10
  const activeItem = matchPath(navigationContext, pathname);
10
11
  const rootItem = matchPath(navigationContext, "/");
11
- return React.useMemo(() => {
12
- if (!activeItem) {
13
- return null;
14
- }
12
+ if (!activeItem) {
13
+ pageRef.current = null;
14
+ }
15
+ else {
15
16
  const breadcrumbs = [];
16
17
  if (rootItem) {
17
18
  breadcrumbs.push({
@@ -37,11 +38,19 @@ export function useActivePage() {
37
38
  });
38
39
  }
39
40
  }
40
- return {
41
- title: getItemTitle(activeItem),
42
- path: getItemPath(navigationContext, activeItem),
43
- sourcePath: pathname,
44
- breadcrumbs
45
- };
46
- }, [activeItem, rootItem, pathname, navigationContext]);
41
+ const title = getItemTitle(activeItem);
42
+ const path = getItemPath(navigationContext, activeItem);
43
+ if (pageRef.current == null ||
44
+ pageRef.current.title !== title ||
45
+ pageRef.current.path !== path ||
46
+ pageRef.current.sourcePath !== pathname) {
47
+ pageRef.current = {
48
+ title,
49
+ path,
50
+ sourcePath: pathname,
51
+ breadcrumbs
52
+ };
53
+ }
54
+ }
55
+ return pageRef.current;
47
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/toolpad",
3
- "version": "1.0.28",
3
+ "version": "1.0.29",
4
4
  "author": "ETSOO",
5
5
  "description": "Dashboard framework extention based on Toolpad Core",
6
6
  "main": "build/cjs/index.js",
@@ -35,7 +35,7 @@ export interface NavigationPageItem {
35
35
  pattern?: string;
36
36
  action?: React.ReactNode;
37
37
  children?: Navigation;
38
- subs?: string[];
38
+ hidden?: boolean;
39
39
  }
40
40
 
41
41
  export interface NavigationSubheaderItem {
@@ -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 routerContext = React.useContext(RouterContext);
79
+ const activePage = useActivePage();
80
80
 
81
- const pathname = routerContext?.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 && (!navigationItem.children || isMini)}
214
+ selected={isSelected && (!children || isMini)}
197
215
  sx={{
198
216
  px: 1.4,
199
217
  height: 48
200
218
  }}
201
- {...(navigationItem.children && !isMini
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
- {navigationItem.children && !isMini && isFullyExpanded
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
- {navigationItem.children && !isMini ? (
286
+ {children && !isMini ? (
269
287
  <Collapse
270
288
  in={isNestedNavigationExpanded}
271
289
  timeout="auto"
272
290
  unmountOnExit
273
291
  >
274
292
  <DashboardSidebarSubNavigation
275
- subNavigation={navigationItem.children}
293
+ subNavigation={children}
276
294
  basePath={navigationItemFullPath}
277
295
  depth={depth + 1}
278
296
  onLinkClick={onLinkClick}
@@ -115,12 +115,12 @@ type PageContainerBarProps = {
115
115
  function PageContainerBar(props: PageContainerBarProps) {
116
116
  const { defaultTitle, slots, slotProps } = props;
117
117
 
118
- const { state, dispatch } = React.useContext(PageDataContext);
118
+ const { state } = React.useContext(PageDataContext);
119
119
 
120
120
  const activePage = useActivePage();
121
121
 
122
122
  React.useLayoutEffect(() => {
123
- // Reset the state when the active page changes without rerendering
123
+ // Reset the state without rerendering
124
124
  state.breadcrumbs = undefined;
125
125
  state.noBreadcrumbs = undefined;
126
126
  state.noPageHeader = undefined;
@@ -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
- 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;
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
 
@@ -14,16 +14,17 @@ export interface ActivePage {
14
14
  export function useActivePage(): ActivePage | null {
15
15
  const navigationContext = React.useContext(NavigationContext);
16
16
  const routerContext = React.useContext(RouterContext);
17
- const pathname = routerContext?.pathname ?? "/";
17
+
18
+ const pageRef = React.useRef<ActivePage>(null);
19
+
20
+ let pathname = routerContext?.pathname ?? "/";
18
21
  const activeItem = matchPath(navigationContext, pathname);
19
22
 
20
23
  const rootItem = matchPath(navigationContext, "/");
21
24
 
22
- return React.useMemo(() => {
23
- if (!activeItem) {
24
- return null;
25
- }
26
-
25
+ if (!activeItem) {
26
+ pageRef.current = null;
27
+ } else {
27
28
  const breadcrumbs: Breadcrumb[] = [];
28
29
 
29
30
  if (rootItem) {
@@ -52,11 +53,23 @@ export function useActivePage(): ActivePage | null {
52
53
  }
53
54
  }
54
55
 
55
- return {
56
- title: getItemTitle(activeItem),
57
- path: getItemPath(navigationContext, activeItem),
58
- sourcePath: pathname,
59
- breadcrumbs
60
- };
61
- }, [activeItem, rootItem, pathname, navigationContext]);
56
+ const title = getItemTitle(activeItem);
57
+ const path = getItemPath(navigationContext, activeItem);
58
+
59
+ if (
60
+ pageRef.current == null ||
61
+ pageRef.current.title !== title ||
62
+ pageRef.current.path !== path ||
63
+ pageRef.current.sourcePath !== pathname
64
+ ) {
65
+ pageRef.current = {
66
+ title,
67
+ path,
68
+ sourcePath: pathname,
69
+ breadcrumbs
70
+ };
71
+ }
72
+ }
73
+
74
+ return pageRef.current;
62
75
  }