diff --git a/docs/data/toolpad/core/components/app-provider/app-provider.md b/docs/data/toolpad/core/components/app-provider/app-provider.md
index 62b4b38b6e9..bffc6d437fd 100644
--- a/docs/data/toolpad/core/components/app-provider/app-provider.md
+++ b/docs/data/toolpad/core/components/app-provider/app-provider.md
@@ -31,8 +31,6 @@ The `AppProvider` for Next.js applications includes some Next.js integrations ou
By using the specific `AppProvider` for Next.js you do not have to manually configure the integration between some Toolpad features and the corresponding Next.js features (such as routing), making the integration automatic and seamless.
```tsx
-import { AppProvider } from '@toolpad/core/nextjs/AppProvider';
-// or
import { AppProvider } from '@toolpad/core/nextjs';
```
@@ -43,7 +41,7 @@ When using the **Next.js App Router**, the most typical file where to import and
```tsx
// app/layout.tsx
-import { AppProvider } from '@toolpad/core/nextjs/AppProvider';
+import { AppProvider } from '@toolpad/core/nextjs';
export default function Layout(props) {
const { children } = props;
@@ -65,7 +63,7 @@ When using the **Next.js Pages Router**, the most typical file where to import a
```tsx
// pages/_app.tsx
-import { AppProvider } from '@toolpad/core/nextjs/AppProvider';
+import { AppProvider } from '@toolpad/core/nextjs';
export default function App(props) {
const { Component, pageProps } = props;
@@ -78,6 +76,16 @@ export default function App(props) {
}
```
+## Client-side Routing
+
+The `AppProvider` for React Router includes routing out-of-the-box for projects using [react-router-dom](https://www.npmjs.com/package/react-router-dom).
+
+This specific `AppProvider` is recommended when building single-page applications with tools such as [Vite](https://vite.dev/), as you do not have to manually configure your app routing, making the integration automatic and seamless.
+
+```tsx
+import { AppProvider } from '@toolpad/core/react-router-dom';
+```
+
## Theming
An `AppProvider` can set a visual theme for all elements inside it to adopt via the `theme` prop. This prop can be set in a few distinct ways with different advantages and disadvantages:
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutFullScreen.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutFullScreen.js
new file mode 100644
index 00000000000..2f71df3e348
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutFullScreen.js
@@ -0,0 +1,76 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { createTheme } from '@mui/material/styles';
+import MapIcon from '@mui/icons-material/Map';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+
+const NAVIGATION = [
+ {
+ segment: 'map',
+ title: 'Map',
+ icon: ,
+ },
+];
+
+const demoTheme = createTheme({
+ cssVariables: {
+ colorSchemeSelector: 'data-toolpad-color-scheme',
+ },
+ colorSchemes: { light: true, dark: true },
+ breakpoints: {
+ values: {
+ xs: 0,
+ sm: 600,
+ md: 600,
+ lg: 1200,
+ xl: 1536,
+ },
+ },
+});
+
+function DashboardLayoutFullScreen(props) {
+ const { window } = props;
+
+ const [pathname, setPathname] = React.useState('/map');
+
+ const router = React.useMemo(() => {
+ return {
+ pathname,
+ searchParams: new URLSearchParams(),
+ navigate: (path) => setPathname(String(path)),
+ };
+ }, [pathname]);
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+
+
+
+
+
+ );
+}
+
+DashboardLayoutFullScreen.propTypes = {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window: PropTypes.func,
+};
+
+export default DashboardLayoutFullScreen;
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutFullScreen.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutFullScreen.tsx
new file mode 100644
index 00000000000..313782ce9db
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutFullScreen.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react';
+import { createTheme } from '@mui/material/styles';
+import MapIcon from '@mui/icons-material/Map';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import type { Router, Navigation } from '@toolpad/core';
+
+const NAVIGATION: Navigation = [
+ {
+ segment: 'map',
+ title: 'Map',
+ icon: ,
+ },
+];
+
+const demoTheme = createTheme({
+ cssVariables: {
+ colorSchemeSelector: 'data-toolpad-color-scheme',
+ },
+ colorSchemes: { light: true, dark: true },
+ breakpoints: {
+ values: {
+ xs: 0,
+ sm: 600,
+ md: 600,
+ lg: 1200,
+ xl: 1536,
+ },
+ },
+});
+
+interface DemoProps {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window?: () => Window;
+}
+
+export default function DashboardLayoutFullScreen(props: DemoProps) {
+ const { window } = props;
+
+ const [pathname, setPathname] = React.useState('/map');
+
+ const router = React.useMemo(() => {
+ return {
+ pathname,
+ searchParams: new URLSearchParams(),
+ navigate: (path) => setPathname(String(path)),
+ };
+ }, [pathname]);
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutFullScreen.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutFullScreen.tsx.preview
new file mode 100644
index 00000000000..d14789aaa5e
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutFullScreen.tsx.preview
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
index 59e061e5309..f41a8b2e3da 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
+++ b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
@@ -103,13 +103,19 @@ This feature is built on top of the [path-to-regexp](https://www.npmjs.com/packa
{{"demo": "DashboardLayoutPattern.js", "height": 400, "iframe": true}}
-### Disabling collapsible sidebar
+### Disable collapsible sidebar
The layout sidebar is collapsible to a mini-drawer (with icons only) in desktop and tablet viewports. This behavior can be disabled with the `disableCollapsibleSidebar` prop.
{{"demo": "DashboardLayoutNoMiniSidebar.js", "height": 400, "iframe": true}}
-### Hiding the sidebar
+### Full-size content
+
+The layout content can take up the full available area with styles such as `flex: 1` or `height: 100%`.
+
+{{"demo": "DashboardLayoutFullScreen.js", "height": 400, "iframe": true}}
+
+### Hide navigation
The layout sidebar can be hidden if needed with the `hideNavigation` prop.
diff --git a/docs/data/toolpad/core/introduction/base-concepts.md b/docs/data/toolpad/core/introduction/base-concepts.md
index f742f389b75..72e52f80e16 100644
--- a/docs/data/toolpad/core/introduction/base-concepts.md
+++ b/docs/data/toolpad/core/introduction/base-concepts.md
@@ -67,7 +67,11 @@ You can pass the router implementation to the `AppProvider` component using the
:::
:::success
-If you are using Next.js, use the `AppProvider` exported from `@toolpad/core/nextjs`. This automatically sets up the router for you and you do not need to pass the `router` prop.
+If you are using Next.js, use the `AppProvider` exported from `@toolpad/core/nextjs`.
+
+If you are building a single-page application with React Router for routing, use the `AppProvider` exported from `@toolpad/core/react-router-dom`.
+
+This automatically sets up the router for you, so that you don't need to pass the `router` prop.
:::
## Slots
diff --git a/docs/pages/toolpad/core/api/app-provider.json b/docs/pages/toolpad/core/api/app-provider.json
index 27ff3b8a3f4..a439e4bee49 100644
--- a/docs/pages/toolpad/core/api/app-provider.json
+++ b/docs/pages/toolpad/core/api/app-provider.json
@@ -35,8 +35,8 @@
},
"name": "AppProvider",
"imports": [
- "import { AppProvider } from '@toolpad/core/AppProvider';\nimport { AppProvider } from '@toolpad/core/nextjs/AppProvider'; // Next.js",
- "import { AppProvider } from '@toolpad/core';\nimport { AppProvider } from '@toolpad/core/nextjs'; // Next.js"
+ "import { AppProvider } from '@toolpad/core/AppProvider';",
+ "import { AppProvider } from '@toolpad/core';\nimport { AppProvider } from '@toolpad/core/nextjs'; // Next.js\nimport { AppProvider } from '@toolpad/core/react-router-dom'; // React Router"
],
"classes": [],
"spread": true,
diff --git a/docs/pages/toolpad/core/api/dashboard-layout.json b/docs/pages/toolpad/core/api/dashboard-layout.json
index 582c78a5088..004fa1829a9 100644
--- a/docs/pages/toolpad/core/api/dashboard-layout.json
+++ b/docs/pages/toolpad/core/api/dashboard-layout.json
@@ -17,6 +17,13 @@
},
"default": "{}",
"additionalInfo": { "slotsApi": true }
+ },
+ "sx": {
+ "type": {
+ "name": "union",
+ "description": "Array<func
| object
| bool>
| func
| object"
+ },
+ "additionalInfo": { "sx": true }
}
},
"name": "DashboardLayout",
diff --git a/docs/translations/api-docs/dashboard-layout/dashboard-layout.json b/docs/translations/api-docs/dashboard-layout/dashboard-layout.json
index d84ffaffa4a..fdc7dfc59c7 100644
--- a/docs/translations/api-docs/dashboard-layout/dashboard-layout.json
+++ b/docs/translations/api-docs/dashboard-layout/dashboard-layout.json
@@ -9,7 +9,10 @@
"description": "Whether the navigation bar and menu icon should be hidden"
},
"slotProps": { "description": "The props used for each slot inside." },
- "slots": { "description": "The components used for each slot inside." }
+ "slots": { "description": "The components used for each slot inside." },
+ "sx": {
+ "description": "The system prop that allows defining system overrides as well as additional CSS styles."
+ }
},
"classDescriptions": {},
"slotDescriptions": {
diff --git a/packages/toolpad-core/package.json b/packages/toolpad-core/package.json
index 15b3e46ab3e..9e63f4be11f 100644
--- a/packages/toolpad-core/package.json
+++ b/packages/toolpad-core/package.json
@@ -40,14 +40,13 @@
"scripts": {
"clean": "rimraf build",
"prebuild": "pnpm clean",
- "build": "pnpm build:node && pnpm build:stable && pnpm build:types && pnpm build:copy-files && pnpm esmify",
- "esmify": "rm -rf ./build/*/package.json",
+ "build": "pnpm build:node && pnpm build:stable && pnpm build:types && pnpm build:copy-files",
"build:node": "node ../../scripts/build.mjs node",
"build:stable": "node ../../scripts/build.mjs stable",
"build:copy-files": "node ../../scripts/copyFiles.mjs",
"build:types": "tsc -b tsconfig.build.json",
"predev": "pnpm clean",
- "dev": "concurrently \"pnpm build:stable --watch\" \"pnpm build:types --watch --preserveWatchOutput\"",
+ "dev": "mkdir -p build && concurrently \"pnpm build:stable --watch\" \"pnpm build:types --watch --preserveWatchOutput\" \"pnpm build:copy-files\"",
"check-types": "pnpm build:types && tsc --noEmit",
"test": "vitest run --coverage",
"test:dev": "vitest",
@@ -77,6 +76,7 @@
"next": "^14.2.14",
"next-router-mock": "^0.9.13",
"playwright": "^1.47.2",
+ "react-router-dom": "6.26.2",
"sinon": "^19.0.2",
"vitest": "2.1.2"
},
@@ -84,11 +84,15 @@
"@mui/icons-material": "5 - 6",
"@mui/material": "5 - 6",
"next": "^14",
- "react": "^18"
+ "react": "^18",
+ "react-router-dom": "^6"
},
"peerDependenciesMeta": {
"next": {
"optional": true
+ },
+ "react-router-dom": {
+ "optional": true
}
},
"sideEffects": false,
diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx
index 056e57c3eb2..44062bd5490 100644
--- a/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx
+++ b/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx
@@ -5,6 +5,7 @@
import * as React from 'react';
import { describe, test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
+import { createTheme } from '@mui/material/styles';
import { AppProvider } from './AppProvider';
describe('AppProvider', () => {
@@ -13,4 +14,12 @@ describe('AppProvider', () => {
expect(screen.getByText('Hello world')).toBeTruthy();
});
+
+ test('renders content correctly when using legacy theme', async () => {
+ const legacyTheme = createTheme();
+
+ render(Hello world);
+
+ expect(screen.getByText('Hello world')).toBeTruthy();
+ });
});
diff --git a/packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx b/packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx
index 6fb86db9679..444fcb889d1 100644
--- a/packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx
+++ b/packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx
@@ -71,14 +71,7 @@ function LegacyThemeProvider(props: LegacyThemeProviderProps) {
);
return (
-
+
{children}
diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
index f117e2af6c3..da98589246e 100644
--- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
+++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
-import { styled, useTheme, type Theme } from '@mui/material';
+import { styled, useTheme, type Theme, SxProps } from '@mui/material';
import MuiAppBar from '@mui/material/AppBar';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
@@ -347,6 +347,11 @@ export interface DashboardLayoutProps {
* @default false
*/
disableCollapsibleSidebar?: boolean;
+ /**
+ * Whether the navigation bar and menu icon should be hidden
+ * @default false
+ */
+ hideNavigation?: boolean;
/**
* The components used for each slot inside.
* @default {}
@@ -361,10 +366,9 @@ export interface DashboardLayoutProps {
toolbarAccount?: AccountProps;
};
/**
- * Whether the navigation bar and menu icon should be hidden
- * @default false
+ * The system prop that allows defining system overrides as well as additional CSS styles.
*/
- hideNavigation?: boolean;
+ sx?: SxProps;
}
/**
@@ -381,9 +385,10 @@ function DashboardLayout(props: DashboardLayoutProps) {
const {
children,
disableCollapsibleSidebar = false,
+ hideNavigation = false,
slots,
slotProps,
- hideNavigation = false,
+ sx,
} = props;
const theme = useTheme();
@@ -526,14 +531,16 @@ function DashboardLayout(props: DashboardLayoutProps) {
);
const getDrawerSharedSx = React.useCallback(
- (isMini: boolean) => {
+ (isMini: boolean, isTemporary: boolean) => {
const drawerWidth = isMini ? 64 : 320;
return {
width: drawerWidth,
flexShrink: 0,
...getDrawerWidthTransitionMixin(isNavigationExpanded),
+ ...(isTemporary ? { position: 'absolute' } : {}),
[`& .MuiDrawer-paper`]: {
+ position: 'absolute',
width: drawerWidth,
boxSizing: 'border-box',
backgroundImage: 'none',
@@ -547,9 +554,21 @@ function DashboardLayout(props: DashboardLayoutProps) {
const ToolbarActionsSlot = slots?.toolbarActions ?? ToolbarActions;
const ToolbarAccountSlot = slots?.toolbarAccount ?? Account;
+ const layoutRef = React.useRef(null);
+
return (
-
-
+
+
{
// TODO: (minWidth: 100vw) Temporary fix to issue reported in https://github.com/mui/material-ui/issues/43244
}
@@ -609,10 +628,11 @@ function DashboardLayout(props: DashboardLayoutProps) {
+
{!hideNavigation ? (
{getDrawerContent(false, 'Phone')}
@@ -638,14 +658,17 @@ function DashboardLayout(props: DashboardLayoutProps) {
sm: disableCollapsibleSidebar ? 'none' : 'block',
md: 'none',
},
- ...getDrawerSharedSx(isMobileMini),
+ ...getDrawerSharedSx(isMobileMini, false),
}}
>
{getDrawerContent(isMobileMini, 'Tablet')}
{getDrawerContent(isDesktopMini, 'Desktop')}
@@ -653,9 +676,10 @@ function DashboardLayout(props: DashboardLayoutProps) {
) : null}
- {children}
+
+ {children}
+
);
@@ -720,6 +754,14 @@ DashboardLayout.propTypes /* remove-proptypes */ = {
toolbarAccount: PropTypes.elementType,
toolbarActions: PropTypes.elementType,
}),
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
+ PropTypes.func,
+ PropTypes.object,
+ ]),
} as any;
export { DashboardLayout };
diff --git a/packages/toolpad-core/src/PageContainer/PageContainer.tsx b/packages/toolpad-core/src/PageContainer/PageContainer.tsx
index ebfea62f62b..4f8d3dd56d4 100644
--- a/packages/toolpad-core/src/PageContainer/PageContainer.tsx
+++ b/packages/toolpad-core/src/PageContainer/PageContainer.tsx
@@ -17,7 +17,7 @@ import { useActivePage } from '../useActivePage';
const PageContentHeader = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
- jusifyContent: 'space-between',
+ justifyContent: 'space-between',
gap: theme.spacing(2),
}));
diff --git a/packages/toolpad-core/src/react-router-dom/AppProvider.test.tsx b/packages/toolpad-core/src/react-router-dom/AppProvider.test.tsx
new file mode 100644
index 00000000000..bde0bfc7743
--- /dev/null
+++ b/packages/toolpad-core/src/react-router-dom/AppProvider.test.tsx
@@ -0,0 +1,22 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import * as React from 'react';
+import { describe, test, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { BrowserRouter } from 'react-router-dom';
+import { AppProvider } from './AppProvider';
+
+describe('React Router AppProvider', () => {
+ test('renders content correctly', async () => {
+ // placeholder test
+ render(
+
+ Hello
+ ,
+ );
+
+ expect(screen.getByText('Hello')).toBeTruthy();
+ });
+});
diff --git a/packages/toolpad-core/src/react-router-dom/AppProvider.tsx b/packages/toolpad-core/src/react-router-dom/AppProvider.tsx
new file mode 100644
index 00000000000..8977c032337
--- /dev/null
+++ b/packages/toolpad-core/src/react-router-dom/AppProvider.tsx
@@ -0,0 +1,44 @@
+'use client';
+import * as React from 'react';
+import { useSearchParams, useLocation, useNavigate } from 'react-router-dom';
+import {
+ AppProvider as AppProviderComponent,
+ type AppProviderProps,
+ Navigate,
+ Router,
+} from '../AppProvider';
+
+/**
+ * @ignore - internal component.
+ */
+function AppProvider(props: AppProviderProps) {
+ const { pathname } = useLocation();
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
+
+ const navigateImpl = React.useCallback(
+ (url, { history = 'auto' } = {}) => {
+ if (history === 'auto' || history === 'push') {
+ return navigate(url);
+ }
+ if (history === 'replace') {
+ return navigate(url, { replace: true });
+ }
+ throw new Error(`Invalid history option: ${history}`);
+ },
+ [navigate],
+ );
+
+ const routerImpl = React.useMemo(
+ () => ({
+ pathname,
+ searchParams,
+ navigate: navigateImpl,
+ }),
+ [pathname, searchParams, navigateImpl],
+ );
+
+ return ;
+}
+
+export { AppProvider };
diff --git a/packages/toolpad-core/src/react-router-dom/index.tsx b/packages/toolpad-core/src/react-router-dom/index.tsx
new file mode 100644
index 00000000000..bd2c0cdccb2
--- /dev/null
+++ b/packages/toolpad-core/src/react-router-dom/index.tsx
@@ -0,0 +1 @@
+export * from './AppProvider';
diff --git a/packages/toolpad-studio/package.json b/packages/toolpad-studio/package.json
index 7d501eddc1b..a4bd520924d 100644
--- a/packages/toolpad-studio/package.json
+++ b/packages/toolpad-studio/package.json
@@ -77,6 +77,7 @@
"@mui/x-tree-view": "7.19.0",
"@tanstack/react-query": "5.59.0",
"@tanstack/react-query-devtools": "5.59.0",
+ "@toolpad/core": "workspace:*",
"@toolpad/studio-components": "workspace:*",
"@toolpad/studio-runtime": "workspace:*",
"@toolpad/utils": "workspace:*",
diff --git a/packages/toolpad-studio/src/runtime/AppLayout.tsx b/packages/toolpad-studio/src/runtime/AppLayout.tsx
index e806d32f796..35f81c63b6e 100644
--- a/packages/toolpad-studio/src/runtime/AppLayout.tsx
+++ b/packages/toolpad-studio/src/runtime/AppLayout.tsx
@@ -1,38 +1,9 @@
import * as React from 'react';
-import {
- Box,
- Drawer,
- Stack,
- List,
- ListItem,
- ListItemButton,
- ListItemText,
- AppBar,
- Toolbar,
- Avatar,
- Typography,
- Menu,
- MenuItem,
- Tooltip,
- Button,
- useTheme,
- Link as MuiLink,
-} from '@mui/material';
-import { Link, useSearchParams } from 'react-router-dom';
-import { PREVIEW_HEADER_HEIGHT } from './constants';
+import { Box, useTheme } from '@mui/material';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import { AppProvider } from '@toolpad/core/react-router-dom';
import { AuthContext } from './useAuth';
-import productIconDark from '../../public/product-icon-dark.svg';
-import productIconLight from '../../public/product-icon-light.svg';
-
-function interopNextImg(img: any) {
- if (typeof img.src === 'string') {
- return img.src;
- }
- return img;
-}
-
-const productIconDarkSrc = interopNextImg(productIconDark);
-const productIconLightSrc = interopNextImg(productIconLight);
const TOOLPAD_DISPLAY_MODE_URL_PARAM = 'toolpad-display';
@@ -45,230 +16,81 @@ export interface NavigationEntry {
hasShell?: boolean;
}
-const DRAWER_WIDTH = 250; // px
-
-interface AppPagesNavigationProps {
- activePageSlug?: string;
- pages: NavigationEntry[];
- clipped?: boolean;
- search?: string;
-}
-
-function AppPagesNavigation({
- activePageSlug,
- pages,
- clipped = false,
- search,
-}: AppPagesNavigationProps) {
- const navListSubheaderId = React.useId();
-
- const theme = useTheme();
-
- const productIcon = theme.palette.mode === 'dark' ? productIconDarkSrc : productIconLightSrc;
-
- return (
-
- {clipped ? : null}
-
-
-
- Toolpad Studio
-
-
-
- {pages.map((page) => (
-
-
-
-
-
- ))}
-
-
- );
-}
-
export interface ToolpadAppLayoutProps {
activePageSlug?: string;
pages?: NavigationEntry[];
- hasNavigation?: boolean;
- hasHeader?: boolean;
+ hasLayout?: boolean;
children?: React.ReactNode;
- clipped?: boolean;
}
export function AppLayout({
activePageSlug,
pages = [],
- hasNavigation: hasNavigationProp = true,
- hasHeader = false,
+ hasLayout: hasLayoutProp = true,
children,
- clipped,
}: ToolpadAppLayoutProps) {
const theme = useTheme();
- const [urlParams] = useSearchParams();
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
const retainedSearch = React.useMemo(() => {
- for (const name of urlParams.keys()) {
+ for (const name of searchParams.keys()) {
if (!RETAINED_URL_PARAMS.has(name)) {
- urlParams.delete(name);
+ searchParams.delete(name);
}
}
- return urlParams.size > 0 ? `?${urlParams.toString()}` : '';
- }, [urlParams]);
+ return searchParams.size > 0 ? `?${searchParams.toString()}` : '';
+ }, [searchParams]);
const navEntry = pages.find((page) => page.slug === activePageSlug);
- const displayMode = urlParams.get(TOOLPAD_DISPLAY_MODE_URL_PARAM);
+ const displayMode = searchParams.get(TOOLPAD_DISPLAY_MODE_URL_PARAM);
const hasShell = navEntry?.hasShell !== false && displayMode !== 'standalone';
- const hasNavigation = hasNavigationProp && hasShell;
+ const hasLayout = hasLayoutProp && hasShell;
- const { session, signOut, isSigningIn } = React.useContext(AuthContext);
+ const { session, signOut } = React.useContext(AuthContext);
- const [anchorElUser, setAnchorElUser] = React.useState(null);
- const handleOpenUserMenu = (event: React.MouseEvent) => {
- setAnchorElUser(event.currentTarget);
- };
- const handleCloseUserMenu = () => {
- setAnchorElUser(null);
- };
+ const navigation = React.useMemo(
+ () =>
+ pages.map(({ slug, displayName }) => ({
+ segment: `pages/${slug}${retainedSearch}`,
+ title: displayName,
+ })),
+ [pages, retainedSearch],
+ );
- const handleSignOut = React.useCallback(() => {
- signOut();
- handleCloseUserMenu();
- }, [signOut]);
+ const signIn = React.useCallback(() => {
+ navigate('/signin');
+ }, [navigate]);
- return (
-
- {hasNavigation ? (
-
- ) : null}
-
- {hasHeader ? (
-
-
- {clipped ? : null}
-
-
- {session?.user && !isSigningIn ? (
-
-
-
-
- ) : null}
-
-
-
-
-
- ) : null}
- {children}
-
+ const layoutContent = (
+
+ {children}
);
+
+ return (
+
+ {hasLayout ? (
+ {layoutContent}
+ ) : (
+ layoutContent
+ )}
+
+ );
}
diff --git a/packages/toolpad-studio/src/runtime/PreviewHeader.tsx b/packages/toolpad-studio/src/runtime/PreviewHeader.tsx
index b612fdd89eb..e411ab74638 100644
--- a/packages/toolpad-studio/src/runtime/PreviewHeader.tsx
+++ b/packages/toolpad-studio/src/runtime/PreviewHeader.tsx
@@ -3,7 +3,6 @@ import { Button, Typography, Box, useTheme, Alert, ButtonProps } from '@mui/mate
import EditIcon from '@mui/icons-material/Edit';
import { Link, useMatch } from 'react-router-dom';
import { useAppHost } from '@toolpad/studio-runtime';
-import { PREVIEW_HEADER_HEIGHT } from './constants';
function OpenInEditorButton({
children = 'Open in editor',
@@ -36,9 +35,8 @@ export default function PreviewHeader() {
return appContext ? (
diff --git a/packages/toolpad-studio/src/runtime/ToolpadApp.tsx b/packages/toolpad-studio/src/runtime/ToolpadApp.tsx
index 363ffcc1ce3..7f095315a08 100644
--- a/packages/toolpad-studio/src/runtime/ToolpadApp.tsx
+++ b/packages/toolpad-studio/src/runtime/ToolpadApp.tsx
@@ -84,7 +84,6 @@ import evalJsBindings, {
EvaluatedBinding,
ParsedBinding,
} from './evalJsBindings';
-import { PREVIEW_HEADER_HEIGHT } from './constants';
import { layoutBoxArgTypes } from './toolpadComponents/layoutBox';
import { useDataQuery, UseFetch } from './useDataQuery';
import { CanvasHooksContext, NavigateToPage } from './CanvasHooksContext';
@@ -173,8 +172,8 @@ function isEqual(
const AppRoot = styled('div')({
overflow: 'auto' /* Prevents margins from collapsing into root */,
position: 'relative' /* Makes sure that the editor overlay that renders inside sizes correctly */,
- minHeight: '100vh',
display: 'flex',
+ flex: 1,
flexDirection: 'column',
});
@@ -1561,20 +1560,12 @@ function ToolpadAppLayout({ children }: ToolpadAppLayoutProps) {
const appHost = useAppHost();
- const clipped = shouldShowPreviewHeader(appHost);
-
if (!appHost.isCanvas && !session?.user && hasAuthentication) {
return ;
}
return (
-
+
{children}
);
@@ -1679,18 +1670,21 @@ export function ToolpadAppProvider({
- {showPreviewHeader ? : null}
-
-
- }>{children}
-
-
-
+ {showPreviewHeader ? : null}
+
+
+ }>{children}
+
+
+
+
diff --git a/packages/toolpad-studio/src/runtime/constants.ts b/packages/toolpad-studio/src/runtime/constants.ts
index ea0ac1dfcce..1a27937c3b9 100644
--- a/packages/toolpad-studio/src/runtime/constants.ts
+++ b/packages/toolpad-studio/src/runtime/constants.ts
@@ -1,4 +1,2 @@
-export const PREVIEW_HEADER_HEIGHT = 52;
-
export const FONTS_URL =
'https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap';
diff --git a/packages/toolpad-studio/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx b/packages/toolpad-studio/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx
index c48bdaad26d..fec9328b984 100644
--- a/packages/toolpad-studio/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx
+++ b/packages/toolpad-studio/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx
@@ -31,6 +31,7 @@ import {
import { PageViewState, NodeInfo, SlotsState } from '../../../types';
import { useAppStateApi } from '../../AppState';
import { FONTS_URL } from '../../../runtime/constants';
+import { scrollIntoViewIfNeeded } from '../../../utils/dom';
// Interface to communicate between editor and canvas
export interface ToolpadBridge {
@@ -254,7 +255,10 @@ export default function EditorCanvasHost({
return;
}
const node = appRoot.querySelector(`[data-node-id='${nodeId}']`);
- node?.scrollIntoView({ behavior: 'instant', block: 'end', inline: 'end' });
+
+ if (node) {
+ scrollIntoViewIfNeeded(node, { behavior: 'instant', block: 'center', inline: 'end' });
+ }
},
},
};
diff --git a/packages/toolpad-studio/src/utils/dom.ts b/packages/toolpad-studio/src/utils/dom.ts
index d0f8d48fa1e..18ed5aeed28 100644
--- a/packages/toolpad-studio/src/utils/dom.ts
+++ b/packages/toolpad-studio/src/utils/dom.ts
@@ -1,8 +1,8 @@
-export function scrollIntoViewIfNeeded(target: Element) {
+export function scrollIntoViewIfNeeded(target: Element, options?: boolean | ScrollIntoViewOptions) {
if (target.getBoundingClientRect().bottom > window.innerHeight) {
- target.scrollIntoView(false);
+ target.scrollIntoView(options ?? false);
}
if (target.getBoundingClientRect().top < 0) {
- target.scrollIntoView();
+ target.scrollIntoView(options);
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 46a9bee59b1..c5af7dce672 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -195,7 +195,7 @@ importers:
version: 7.37.1(eslint@8.57.1)
eslint-plugin-react-compiler:
specifier: latest
- version: 0.0.0-experimental-4c9207a-20241007(eslint@8.57.1)
+ version: 0.0.0-experimental-7c1344f-20241009(eslint@8.57.1)
eslint-plugin-react-hooks:
specifier: 4.6.2
version: 4.6.2(eslint@8.57.1)
@@ -668,6 +668,9 @@ importers:
playwright:
specifier: ^1.47.2
version: 1.47.2
+ react-router-dom:
+ specifier: 6.26.2
+ version: 6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
sinon:
specifier: ^19.0.2
version: 19.0.2
@@ -741,6 +744,9 @@ importers:
'@tanstack/react-query-devtools':
specifier: 5.59.0
version: 5.59.0(@tanstack/react-query@5.59.0(react@18.3.1))(react@18.3.1)
+ '@toolpad/core':
+ specifier: workspace:*
+ version: link:../toolpad-core/build
'@toolpad/studio-components':
specifier: workspace:*
version: link:../toolpad-studio-components
@@ -6054,8 +6060,8 @@ packages:
peerDependencies:
eslint: '>=7.0.0'
- eslint-plugin-react-compiler@0.0.0-experimental-4c9207a-20241007:
- resolution: {integrity: sha512-1NEoOWBeJfzfTLVrj5qsLELs9kEmTLAmTBbJb4XY8HmDblQkJjVOe0XRhI72SINGs4QFZ6EKbQ960PaI6viwcw==}
+ eslint-plugin-react-compiler@0.0.0-experimental-7c1344f-20241009:
+ resolution: {integrity: sha512-ZhUn6vMU5OJjP1kJhuGA+NDrVPy3pcbok5CblWJaRXH+JzD6bqh5hy1ieep/I92S3ikWTn/5I6KqcXchAYfL4g==}
engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0}
peerDependencies:
eslint: '>=7'
@@ -15934,7 +15940,7 @@ snapshots:
globals: 13.24.0
rambda: 7.5.0
- eslint-plugin-react-compiler@0.0.0-experimental-4c9207a-20241007(eslint@8.57.1):
+ eslint-plugin-react-compiler@0.0.0-experimental-7c1344f-20241009(eslint@8.57.1):
dependencies:
'@babel/core': 7.25.7
'@babel/parser': 7.25.7
diff --git a/scripts/copyFiles.mjs b/scripts/copyFiles.mjs
index b5a4cf080de..6959e516352 100644
--- a/scripts/copyFiles.mjs
+++ b/scripts/copyFiles.mjs
@@ -1,7 +1,6 @@
/* eslint-disable no-console */
import path from 'path';
import {
- createModulePackages,
createPackageFile,
includeFileInBuild,
prepend,
@@ -53,8 +52,6 @@ async function run() {
);
await addLicense(packageData);
-
- await createModulePackages({ from: srcPath, to: buildPath });
} catch (err) {
console.error(err);
process.exit(1);
diff --git a/scripts/copyFilesUtils.mjs b/scripts/copyFilesUtils.mjs
index f9a55addab0..945b0bb0eca 100644
--- a/scripts/copyFilesUtils.mjs
+++ b/scripts/copyFilesUtils.mjs
@@ -13,65 +13,6 @@ export async function includeFileInBuild(file) {
console.log(`Copied ${sourcePath} to ${targetPath}`);
}
-/**
- * Puts a package.json into every immediate child directory of rootDir.
- * That package.json contains information about esm for bundlers so that imports
- * like import Typography from '@mui/material/Typography' are tree-shakeable.
- *
- * It also tests that an this import can be used in TypeScript by checking
- * if an index.d.ts is present at that path.
- * @param {object} param0
- * @param {string} param0.from
- * @param {string} param0.to
- */
-export async function createModulePackages({ from, to }) {
- const directoryPackages = glob.sync('*/index.{js,ts,tsx}', { cwd: from }).map(path.dirname);
-
- await Promise.all(
- directoryPackages.map(async (directoryPackage) => {
- const packageJsonPath = path.join(to, directoryPackage, 'package.json');
- const topLevelPathImportsAreCommonJSModules = await fse.pathExists(
- path.resolve(path.dirname(packageJsonPath), '../esm'),
- );
-
- const packageJson = {
- sideEffects: false,
- module: topLevelPathImportsAreCommonJSModules
- ? path.posix.join('../esm', directoryPackage, 'index.js')
- : './index.js',
- main: topLevelPathImportsAreCommonJSModules
- ? './index.js'
- : path.posix.join('../node', directoryPackage, 'index.js'),
- types: './index.d.ts',
- };
-
- const [typingsEntryExist, moduleEntryExists, mainEntryExists] = await Promise.all([
- fse.pathExists(path.resolve(path.dirname(packageJsonPath), packageJson.types)),
- fse.pathExists(path.resolve(path.dirname(packageJsonPath), packageJson.module)),
- fse.pathExists(path.resolve(path.dirname(packageJsonPath), packageJson.main)),
- fse.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)),
- ]);
-
- const manifestErrorMessages = [];
- if (!typingsEntryExist) {
- manifestErrorMessages.push(`'types' entry '${packageJson.types}' does not exist`);
- }
- if (!moduleEntryExists) {
- manifestErrorMessages.push(`'module' entry '${packageJson.module}' does not exist`);
- }
- if (!mainEntryExists) {
- manifestErrorMessages.push(`'main' entry '${packageJson.main}' does not exist`);
- }
- if (manifestErrorMessages.length > 0) {
- // TODO: AggregateError
- throw new Error(`${packageJsonPath}:\n${manifestErrorMessages.join('\n')}`);
- }
-
- return packageJsonPath;
- }),
- );
-}
-
export async function typescriptCopy({ from, to }) {
if (!(await fse.pathExists(to))) {
console.warn(`path ${to} does not exists`);
diff --git a/scripts/docs/buildCoreApiDocs/config/getComponentImports.ts b/scripts/docs/buildCoreApiDocs/config/getComponentImports.ts
index d04c743dde0..7f1f29ebf29 100644
--- a/scripts/docs/buildCoreApiDocs/config/getComponentImports.ts
+++ b/scripts/docs/buildCoreApiDocs/config/getComponentImports.ts
@@ -23,14 +23,17 @@ export function getComponentImports(name: string, filename: string) {
const nextjsRelativePath = path.resolve(relativePath, '../../nextjs');
const hasNextJsVersion = fs.existsSync(`${nextjsRelativePath}/${name}.tsx`);
+ const reactRouterDOMRelativePath = path.resolve(relativePath, '../../react-router-dom');
+ const hasReactRouterDOMVersion = fs.existsSync(`${reactRouterDOMRelativePath}/${name}.tsx`);
+
return [
- `import { ${name} } from '@toolpad/core/${name}';${
- hasNextJsVersion
- ? `\nimport { ${name} } from '@toolpad/core/nextjs/${name}'; // Next.js`
- : ''
- }`,
+ `import { ${name} } from '@toolpad/core/${name}';`,
`import { ${name} } from '@toolpad/core';${
hasNextJsVersion ? `\nimport { ${name} } from '@toolpad/core/nextjs'; // Next.js` : ''
+ }${
+ hasReactRouterDOMVersion
+ ? `\nimport { ${name} } from '@toolpad/core/react-router-dom'; // React Router`
+ : ''
}`,
];
}
diff --git a/scripts/docs/buildCoreApiDocs/config/projectSettings.ts b/scripts/docs/buildCoreApiDocs/config/projectSettings.ts
index 5d952eec674..bfbfa868749 100644
--- a/scripts/docs/buildCoreApiDocs/config/projectSettings.ts
+++ b/scripts/docs/buildCoreApiDocs/config/projectSettings.ts
@@ -26,7 +26,7 @@ export const projectSettings: ProjectSettings = {
const relativePath = path.relative(repositoryRoot, filename);
const directories = path.dirname(relativePath).split(path.sep);
- return directories[3] === 'nextjs';
+ return directories[3] === 'nextjs' || directories[3] === 'react-router-dom';
},
skipSlotsAndClasses: false,
translationPagesDirectory: 'docs/translations/api-docs',
diff --git a/test/integration/auth/domain.spec.ts b/test/integration/auth/domain.spec.ts
index b0db7ac5315..99e0e22958e 100644
--- a/test/integration/auth/domain.spec.ts
+++ b/test/integration/auth/domain.spec.ts
@@ -58,7 +58,7 @@ test('Must be authenticated with valid domain to access app', async ({ page, req
await page.waitForURL(/\/prod\/pages\/mypage/);
// Sign out
- await page.getByText('Mr. MUI 2024').click();
+ await page.getByRole('button', { name: 'Current User' }).click();
await page.getByText('Sign out').click();
await page.waitForURL(/\/prod\/signin/);
diff --git a/test/integration/auth/roles.spec.ts b/test/integration/auth/roles.spec.ts
index 8d4ba4c8e96..59a745b7e54 100644
--- a/test/integration/auth/roles.spec.ts
+++ b/test/integration/auth/roles.spec.ts
@@ -31,13 +31,15 @@ test('Must have required roles to access pages', async ({ page }) => {
// Sign in without admin role
await tryCredentialsSignIn(page, 'test', 'test');
- await expect(page.getByText('Admin Page')).toBeHidden();
+ const desktopNavigation = page.getByRole('navigation', { name: 'Desktop' });
+
+ await expect(desktopNavigation.getByText('Admin Page')).toBeHidden();
// Sign in with admin role
- await page.getByText('Miss Test').click();
+ await page.getByRole('button', { name: 'Current User' }).click();
await page.getByText('Sign out').click();
await tryCredentialsSignIn(page, 'admin', 'admin');
- await expect(page.getByText('Admin Page')).toBeVisible();
+ await expect(desktopNavigation.getByText('Admin Page')).toBeVisible();
await expect(page.getByText('message: hello world')).toBeVisible();
});
diff --git a/test/integration/backend-basic/fixture/toolpad/application.yml b/test/integration/backend-basic/fixture/toolpad/application.yml
index 6b008228134..28e38b7d353 100644
--- a/test/integration/backend-basic/fixture/toolpad/application.yml
+++ b/test/integration/backend-basic/fixture/toolpad/application.yml
@@ -1,3 +1,5 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.6.0/docs/schemas/v1/definitions.json#properties/Application
+
apiVersion: v1
kind: application
-spec: {}
+spec: { authentication: {}, authorization: {} }
diff --git a/test/integration/backend-basic/fixture/toolpad/pages/basic/page.yml b/test/integration/backend-basic/fixture/toolpad/pages/basic/page.yml
index 9b2e1ab6e31..a652f1b036f 100644
--- a/test/integration/backend-basic/fixture/toolpad/pages/basic/page.yml
+++ b/test/integration/backend-basic/fixture/toolpad/pages/basic/page.yml
@@ -1,4 +1,4 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page
+# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.6.0/docs/schemas/v1/definitions.json#properties/Page
apiVersion: v1
kind: page
@@ -247,3 +247,5 @@ spec:
kind: rest
headers: []
method: GET
+ searchParams: []
+ display: shell
diff --git a/test/integration/backend-basic/fixture/toolpad/pages/crud/page.yml b/test/integration/backend-basic/fixture/toolpad/pages/crud/page.yml
index 8fa359b5309..442b0c38e5c 100644
--- a/test/integration/backend-basic/fixture/toolpad/pages/crud/page.yml
+++ b/test/integration/backend-basic/fixture/toolpad/pages/crud/page.yml
@@ -1,4 +1,4 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page
+# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.6.0/docs/schemas/v1/definitions.json#properties/Page
apiVersion: v1
kind: page
diff --git a/test/integration/backend-basic/fixture/toolpad/pages/dataProviders/page.yml b/test/integration/backend-basic/fixture/toolpad/pages/dataProviders/page.yml
index f9d2190094f..48c71ef719e 100644
--- a/test/integration/backend-basic/fixture/toolpad/pages/dataProviders/page.yml
+++ b/test/integration/backend-basic/fixture/toolpad/pages/dataProviders/page.yml
@@ -1,4 +1,4 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page
+# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.6.0/docs/schemas/v1/definitions.json#properties/Page
apiVersion: v1
kind: page
diff --git a/test/integration/backend-basic/fixture/toolpad/pages/extractedTypes/page.yml b/test/integration/backend-basic/fixture/toolpad/pages/extractedTypes/page.yml
index bcef86eb0da..0df4d3b063d 100644
--- a/test/integration/backend-basic/fixture/toolpad/pages/extractedTypes/page.yml
+++ b/test/integration/backend-basic/fixture/toolpad/pages/extractedTypes/page.yml
@@ -1,4 +1,4 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page
+# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.6.0/docs/schemas/v1/definitions.json#properties/Page
apiVersion: v1
kind: page
diff --git a/test/integration/backend-basic/fixture/toolpad/pages/serialization/page.yml b/test/integration/backend-basic/fixture/toolpad/pages/serialization/page.yml
index 6e097e6fbbf..70f6e96b660 100644
--- a/test/integration/backend-basic/fixture/toolpad/pages/serialization/page.yml
+++ b/test/integration/backend-basic/fixture/toolpad/pages/serialization/page.yml
@@ -1,4 +1,4 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page
+# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/toolpad/v0.6.0/docs/schemas/v1/definitions.json#properties/Page
apiVersion: v1
kind: page