@etsoo/toolpad 1.0.9 → 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.
@@ -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.9",
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",
@@ -50,7 +50,6 @@
50
50
  "@mui/utils": "6.3.1",
51
51
  "invariant": "2.2.4",
52
52
  "path-to-regexp": "6.3.0",
53
- "prop-types": "15.8.1",
54
53
  "react": "^18.3.1"
55
54
  },
56
55
  "overrides": {
@@ -62,7 +61,6 @@
62
61
  "@testing-library/jest-dom": "^6.6.3",
63
62
  "@testing-library/react": "^16.1.0",
64
63
  "@types/invariant": "2.2.37",
65
- "@types/prop-types": "15.7.14",
66
64
  "@types/react": "18.3.12",
67
65
  "@types/react-dom": "18.3.1",
68
66
  "@vitejs/plugin-react": "4.3.4",
@@ -1,5 +1,4 @@
1
1
  import * as React from "react";
2
- import PropTypes from "prop-types";
3
2
  import Button from "@mui/material/Button";
4
3
  import Popover from "@mui/material/Popover";
5
4
  import Divider from "@mui/material/Divider";
@@ -164,45 +163,4 @@ function Account(props: AccountProps) {
164
163
  );
165
164
  }
166
165
 
167
- Account.propTypes /* remove-proptypes */ = {
168
- // ┌────────────────────────────── Warning ──────────────────────────────┐
169
- // │ These PropTypes are generated from the TypeScript type definitions. │
170
- // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
171
- // └─────────────────────────────────────────────────────────────────────┘
172
- /**
173
- * The props used for each slot inside.
174
- */
175
- slotProps: PropTypes.shape({
176
- popover: PropTypes.object,
177
- popoverContent: PropTypes.object,
178
- preview: PropTypes.shape({
179
- handleClick: PropTypes.func,
180
- open: PropTypes.bool,
181
- slotProps: PropTypes.shape({
182
- avatar: PropTypes.object,
183
- avatarIconButton: PropTypes.object,
184
- moreIconButton: PropTypes.object
185
- }),
186
- slots: PropTypes.shape({
187
- avatar: PropTypes.elementType,
188
- avatarIconButton: PropTypes.elementType,
189
- moreIconButton: PropTypes.elementType
190
- }),
191
- variant: PropTypes.oneOf(["condensed", "expanded"])
192
- }),
193
- signInButton: PropTypes.object,
194
- signOutButton: PropTypes.object
195
- }),
196
- /**
197
- * The components used for each slot inside.
198
- */
199
- slots: PropTypes.shape({
200
- popover: PropTypes.elementType,
201
- popoverContent: PropTypes.elementType,
202
- preview: PropTypes.elementType,
203
- signInButton: PropTypes.elementType,
204
- signOutButton: PropTypes.elementType
205
- })
206
- } as any;
207
-
208
166
  export { Account };
@@ -1,5 +1,4 @@
1
1
  import * as React from "react";
2
- import PropTypes from "prop-types";
3
2
  import Avatar, { AvatarProps } from "@mui/material/Avatar";
4
3
  import Typography from "@mui/material/Typography";
5
4
  import Tooltip from "@mui/material/Tooltip";
@@ -172,41 +171,4 @@ function AccountPreview(props: AccountPreviewProps) {
172
171
  );
173
172
  }
174
173
 
175
- AccountPreview.propTypes /* remove-proptypes */ = {
176
- // ┌────────────────────────────── Warning ──────────────────────────────┐
177
- // │ These PropTypes are generated from the TypeScript type definitions. │
178
- // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
179
- // └─────────────────────────────────────────────────────────────────────┘
180
- /**
181
- * The handler used when the preview is expanded
182
- */
183
- handleClick: PropTypes.func,
184
- /**
185
- * The state of the Account popover
186
- * @default false
187
- */
188
- open: PropTypes.bool,
189
- /**
190
- * The props used for each slot inside.
191
- */
192
- slotProps: PropTypes.shape({
193
- avatar: PropTypes.object,
194
- avatarIconButton: PropTypes.object,
195
- moreIconButton: PropTypes.object
196
- }),
197
- /**
198
- * The components used for each slot inside.
199
- */
200
- slots: PropTypes.shape({
201
- avatar: PropTypes.elementType
202
- }),
203
- /**
204
- * The type of account details to display.
205
- * @property {'condensed'} condensed - Shows only the user's avatar.
206
- * @property {'expanded'} expanded - Displays the user's avatar, name, and email if available.
207
- * @default 'condensed'
208
- */
209
- variant: PropTypes.oneOf(["condensed", "expanded"])
210
- } as any;
211
-
212
174
  export { AccountPreview };
@@ -22,7 +22,6 @@ export interface Router {
22
22
 
23
23
  export interface Branding {
24
24
  title?:
25
- | string
26
25
  | React.ReactNode
27
26
  | [string, (handler: React.MouseEvent<HTMLSpanElement>) => void];
28
27
  logo?: React.ReactNode;
@@ -36,6 +35,7 @@ export interface NavigationPageItem {
36
35
  pattern?: string;
37
36
  action?: React.ReactNode;
38
37
  children?: Navigation;
38
+ subs?: string[];
39
39
  }
40
40
 
41
41
  export interface NavigationSubheaderItem {
@@ -1,5 +1,4 @@
1
1
  "use client";
2
- import PropTypes from "prop-types";
3
2
  import {
4
3
  BrandingContext,
5
4
  NavigationContext,
@@ -69,98 +68,4 @@ function AppProvider(props: AppProviderProps) {
69
68
  );
70
69
  }
71
70
 
72
- AppProvider.propTypes /* remove-proptypes */ = {
73
- // ┌────────────────────────────── Warning ──────────────────────────────┐
74
- // │ These PropTypes are generated from the TypeScript type definitions. │
75
- // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
76
- // └─────────────────────────────────────────────────────────────────────┘
77
- /**
78
- * Authentication methods.
79
- * @default null
80
- */
81
- authentication: PropTypes.shape({
82
- signIn: PropTypes.func.isRequired,
83
- signOut: PropTypes.func.isRequired
84
- }),
85
- /**
86
- * Branding options for the app.
87
- * @default null
88
- */
89
- branding: PropTypes.shape({
90
- logo: PropTypes.node,
91
- title: PropTypes.node
92
- }),
93
- /**
94
- * The content of the app provider.
95
- */
96
- children: PropTypes.node,
97
- /**
98
- * Navigation definition for the app.
99
- * @default []
100
- */
101
- navigation: PropTypes.arrayOf(
102
- PropTypes.oneOfType([
103
- PropTypes.shape({
104
- action: PropTypes.node,
105
- children: PropTypes.arrayOf(
106
- PropTypes.oneOfType([
107
- PropTypes.object,
108
- PropTypes.shape({
109
- kind: PropTypes.oneOf(["header"]).isRequired,
110
- title: PropTypes.string.isRequired
111
- }),
112
- PropTypes.shape({
113
- kind: PropTypes.oneOf(["divider"]).isRequired
114
- })
115
- ]).isRequired
116
- ),
117
- icon: PropTypes.node,
118
- kind: PropTypes.oneOf(["page"]),
119
- pattern: PropTypes.string,
120
- segment: PropTypes.string,
121
- title: PropTypes.string
122
- }),
123
- PropTypes.shape({
124
- kind: PropTypes.oneOf(["header"]).isRequired,
125
- title: PropTypes.string.isRequired
126
- }),
127
- PropTypes.shape({
128
- kind: PropTypes.oneOf(["divider"]).isRequired
129
- })
130
- ]).isRequired
131
- ),
132
- /**
133
- * Router implementation used inside Toolpad components.
134
- * @default null
135
- */
136
- router: PropTypes.shape({
137
- navigate: PropTypes.func.isRequired,
138
- pathname: PropTypes.string.isRequired,
139
- searchParams: PropTypes.instanceOf(URLSearchParams).isRequired
140
- }),
141
- /**
142
- * Session info about the current user.
143
- * @default null
144
- */
145
- session: PropTypes.shape({
146
- user: PropTypes.shape({
147
- email: PropTypes.string,
148
- id: PropTypes.string,
149
- image: PropTypes.string,
150
- name: PropTypes.string
151
- })
152
- }),
153
- /**
154
- * [Theme or themes](https://mui.com/toolpad/core/react-app-provider/#theming) to be used by the app in light/dark mode. A [CSS variables theme](https://mui.com/material-ui/customization/css-theme-variables/overview/) is recommended.
155
- * @default createTheme()
156
- */
157
- theme: PropTypes.object,
158
- /**
159
- * The window where the application is rendered.
160
- * This is needed when rendering the app inside an iframe, for example.
161
- * @default window
162
- */
163
- window: PropTypes.object
164
- } as any;
165
-
166
71
  export { AppProvider };
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
- import PropTypes from "prop-types";
4
3
  import { styled, useTheme, type Theme, SxProps } from "@mui/material";
5
4
  import MuiAppBar from "@mui/material/AppBar";
6
5
  import Box from "@mui/material/Box";
@@ -9,12 +8,10 @@ import IconButton from "@mui/material/IconButton";
9
8
  import Stack from "@mui/material/Stack";
10
9
  import Toolbar from "@mui/material/Toolbar";
11
10
  import Tooltip from "@mui/material/Tooltip";
12
- import Typography from "@mui/material/Typography";
13
11
  import useMediaQuery from "@mui/material/useMediaQuery";
14
12
  import type {} from "@mui/material/themeCssVarsAugmentation";
15
13
  import MenuIcon from "@mui/icons-material/Menu";
16
14
  import MenuOpenIcon from "@mui/icons-material/MenuOpen";
17
- import { Link } from "../shared/Link";
18
15
  import { NavigationContext, WindowContext } from "../shared/context";
19
16
  import { Account, type AccountProps } from "../Account";
20
17
  import { DashboardSidebarSubNavigation } from "./DashboardSidebarSubNavigation";
@@ -456,88 +453,4 @@ function DashboardLayout(props: DashboardLayoutProps) {
456
453
  );
457
454
  }
458
455
 
459
- DashboardLayout.propTypes /* remove-proptypes */ = {
460
- // ┌────────────────────────────── Warning ──────────────────────────────┐
461
- // │ These PropTypes are generated from the TypeScript type definitions. │
462
- // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
463
- // └─────────────────────────────────────────────────────────────────────┘
464
- /**
465
- * The content of the dashboard.
466
- */
467
- children: PropTypes.node,
468
- /**
469
- * Whether the sidebar should start collapsed in desktop size screens.
470
- * @default false
471
- */
472
- defaultSidebarCollapsed: PropTypes.bool,
473
- /**
474
- * Whether the sidebar should not be collapsible to a mini variant in desktop and tablet viewports.
475
- * @default false
476
- */
477
- disableCollapsibleSidebar: PropTypes.bool,
478
- /**
479
- * Whether the navigation bar and menu icon should be hidden
480
- * @default false
481
- */
482
- hideNavigation: PropTypes.bool,
483
- /**
484
- * Width of the sidebar when expanded.
485
- * @default 320
486
- */
487
- sidebarExpandedWidth: PropTypes.oneOfType([
488
- PropTypes.number,
489
- PropTypes.string
490
- ]),
491
- /**
492
- * The props used for each slot inside.
493
- * @default {}
494
- */
495
- slotProps: PropTypes.shape({
496
- sidebarFooter: PropTypes.shape({
497
- mini: PropTypes.bool.isRequired
498
- }),
499
- toolbarAccount: PropTypes.shape({
500
- localeText: PropTypes.shape({
501
- iconButtonAriaLabel: PropTypes.string,
502
- signInLabel: PropTypes.string,
503
- signOutLabel: PropTypes.string
504
- }),
505
- slotProps: PropTypes.shape({
506
- popover: PropTypes.object,
507
- popoverContent: PropTypes.object,
508
- preview: PropTypes.object,
509
- signInButton: PropTypes.object,
510
- signOutButton: PropTypes.object
511
- }),
512
- slots: PropTypes.shape({
513
- popover: PropTypes.elementType,
514
- popoverContent: PropTypes.elementType,
515
- preview: PropTypes.elementType,
516
- signInButton: PropTypes.elementType,
517
- signOutButton: PropTypes.elementType
518
- })
519
- }),
520
- toolbarActions: PropTypes.object
521
- }),
522
- /**
523
- * The components used for each slot inside.
524
- * @default {}
525
- */
526
- slots: PropTypes.shape({
527
- sidebarFooter: PropTypes.elementType,
528
- toolbarAccount: PropTypes.elementType,
529
- toolbarActions: PropTypes.elementType
530
- }),
531
- /**
532
- * The system prop that allows defining system overrides as well as additional CSS styles.
533
- */
534
- sx: PropTypes.oneOfType([
535
- PropTypes.arrayOf(
536
- PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])
537
- ),
538
- PropTypes.func,
539
- PropTypes.object
540
- ])
541
- } as any;
542
-
543
456
  export { DashboardLayout };
@@ -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;
@@ -54,14 +54,16 @@ export function TitleBar() {
54
54
  return [title, false];
55
55
  }, [title]);
56
56
 
57
- if (!hasLink) return titleUI;
58
-
59
- return (
60
- <Link href="/" style={{ color: "inherit", textDecoration: "none" }}>
61
- <Stack direction="row" alignItems="center">
62
- {branding?.logo && <LogoContainer>{branding.logo}</LogoContainer>}
63
- {titleUI}
64
- </Stack>
65
- </Link>
66
- );
57
+ if (hasLink) {
58
+ return (
59
+ <Link href="/" style={{ color: "inherit", textDecoration: "none" }}>
60
+ <Stack direction="row" alignItems="center">
61
+ {branding?.logo && <LogoContainer>{branding.logo}</LogoContainer>}
62
+ {titleUI}
63
+ </Stack>
64
+ </Link>
65
+ );
66
+ } else {
67
+ return titleUI;
68
+ }
67
69
  }
@@ -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