diff --git a/docs/data/toolpad/core/components/crud/CrudAdvanced.js b/docs/data/toolpad/core/components/crud/CrudAdvanced.js
index 05cca6d443d..0432b936836 100644
--- a/docs/data/toolpad/core/components/crud/CrudAdvanced.js
+++ b/docs/data/toolpad/core/components/crud/CrudAdvanced.js
@@ -4,7 +4,6 @@ import { createTheme } from '@mui/material/styles';
import StickyNote2Icon from '@mui/icons-material/StickyNote2';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import {
Create,
CrudProvider,
@@ -223,22 +222,6 @@ function CrudAdvanced(props) {
const createPath = `${rootPath}/new`;
const editPath = `${rootPath}/:noteId/edit`;
- const title = React.useMemo(() => {
- if (router.pathname === createPath) {
- return 'New Note';
- }
- const editNoteId = matchPath(editPath, router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath(showPath, router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [createPath, editPath, router.pathname, showPath]);
-
const handleRowClick = React.useCallback(
(noteId) => {
router.navigate(`${rootPath}/${String(noteId)}`);
@@ -282,37 +265,41 @@ function CrudAdvanced(props) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- {router.pathname === listPath ? (
-
- ) : null}
- {router.pathname === createPath ? (
-
- ) : null}
- {router.pathname !== createPath && showNoteId ? (
-
- ) : null}
- {editNoteId ? (
-
- ) : null}
-
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ {router.pathname === listPath ? (
+
+ ) : null}
+ {router.pathname === createPath ? (
+
+ ) : null}
+ {router.pathname !== createPath && showNoteId ? (
+
+ ) : null}
+ {editNoteId ? (
+
+ ) : null}
+
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudAdvanced.tsx b/docs/data/toolpad/core/components/crud/CrudAdvanced.tsx
index 1de9755f0d0..569598cd89e 100644
--- a/docs/data/toolpad/core/components/crud/CrudAdvanced.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudAdvanced.tsx
@@ -3,7 +3,6 @@ import { createTheme } from '@mui/material/styles';
import StickyNote2Icon from '@mui/icons-material/StickyNote2';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import {
Create,
CrudProvider,
@@ -244,22 +243,6 @@ export default function CrudAdvanced(props: DemoProps) {
const createPath = `${rootPath}/new`;
const editPath = `${rootPath}/:noteId/edit`;
- const title = React.useMemo(() => {
- if (router.pathname === createPath) {
- return 'New Note';
- }
- const editNoteId = matchPath(editPath, router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath(showPath, router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [createPath, editPath, router.pathname, showPath]);
-
const handleRowClick = React.useCallback(
(noteId: string | number) => {
router.navigate(`${rootPath}/${String(noteId)}`);
@@ -303,40 +286,44 @@ export default function CrudAdvanced(props: DemoProps) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- dataSource={notesDataSource}
- dataSourceCache={notesCache}
- >
- {router.pathname === listPath ? (
-
- initialPageSize={10}
- onRowClick={handleRowClick}
- onCreateClick={handleCreateClick}
- onEditClick={handleEditClick}
- />
- ) : null}
- {router.pathname === createPath ? (
-
- initialValues={{ title: 'New note' }}
- onSubmitSuccess={handleCreate}
- resetOnSubmit={false}
- />
- ) : null}
- {router.pathname !== createPath && showNoteId ? (
-
- id={showNoteId}
- onEditClick={handleEditClick}
- onDelete={handleDelete}
- />
- ) : null}
- {editNoteId ? (
- id={editNoteId} onSubmitSuccess={handleEdit} />
- ) : null}
-
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ dataSource={notesDataSource}
+ dataSourceCache={notesCache}
+ >
+ {router.pathname === listPath ? (
+
+ initialPageSize={10}
+ onRowClick={handleRowClick}
+ onCreateClick={handleCreateClick}
+ onEditClick={handleEditClick}
+ />
+ ) : null}
+ {router.pathname === createPath ? (
+
+ initialValues={{ title: 'New note' }}
+ onSubmitSuccess={handleCreate}
+ resetOnSubmit={false}
+ pageTitle="New Note"
+ />
+ ) : null}
+ {router.pathname !== createPath && showNoteId ? (
+
+ id={showNoteId}
+ onEditClick={handleEditClick}
+ onDelete={handleDelete}
+ pageTitle={`Note ${showNoteId}`}
+ />
+ ) : null}
+ {editNoteId ? (
+
+ id={editNoteId}
+ onSubmitSuccess={handleEdit}
+ pageTitle={`Note ${editNoteId} - Edit`}
+ />
+ ) : null}
+
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudAdvanced.tsx.preview b/docs/data/toolpad/core/components/crud/CrudAdvanced.tsx.preview
index 063a8372309..0fbd8c45c18 100644
--- a/docs/data/toolpad/core/components/crud/CrudAdvanced.tsx.preview
+++ b/docs/data/toolpad/core/components/crud/CrudAdvanced.tsx.preview
@@ -15,6 +15,7 @@
initialValues={{ title: 'New note' }}
onSubmitSuccess={handleCreate}
resetOnSubmit={false}
+ pageTitle="New Note"
/>
) : null}
{router.pathname !== createPath && showNoteId ? (
@@ -22,9 +23,14 @@
id={showNoteId}
onEditClick={handleEditClick}
onDelete={handleDelete}
+ pageTitle={`Note ${showNoteId}`}
/>
) : null}
{editNoteId ? (
- id={editNoteId} onSubmitSuccess={handleEdit} />
+
+ id={editNoteId}
+ onSubmitSuccess={handleEdit}
+ pageTitle={`Note ${editNoteId} - Edit`}
+ />
) : null}
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/crud/CrudBasic.js b/docs/data/toolpad/core/components/crud/CrudBasic.js
index 289a751a521..385f822b282 100644
--- a/docs/data/toolpad/core/components/crud/CrudBasic.js
+++ b/docs/data/toolpad/core/components/crud/CrudBasic.js
@@ -4,7 +4,6 @@ import { createTheme } from '@mui/material/styles';
import StickyNote2Icon from '@mui/icons-material/StickyNote2';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -211,21 +210,8 @@ function CrudBasic(props) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/notes/new') {
- return 'New Note';
- }
- const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath('/notes/:noteId', router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showNoteId = matchPath('/notes/:noteId', router.pathname);
+ const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -237,17 +223,20 @@ function CrudBasic(props) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudBasic.tsx b/docs/data/toolpad/core/components/crud/CrudBasic.tsx
index 0f1c726c621..5374e060bcf 100644
--- a/docs/data/toolpad/core/components/crud/CrudBasic.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudBasic.tsx
@@ -3,7 +3,6 @@ import { createTheme } from '@mui/material/styles';
import StickyNote2Icon from '@mui/icons-material/StickyNote2';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -230,21 +229,8 @@ export default function CrudBasic(props: DemoProps) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/notes/new') {
- return 'New Note';
- }
- const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath('/notes/:noteId', router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showNoteId = matchPath('/notes/:noteId', router.pathname);
+ const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -256,17 +242,20 @@ export default function CrudBasic(props: DemoProps) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- dataSource={notesDataSource}
- dataSourceCache={notesCache}
- rootPath="/notes"
- initialPageSize={10}
- defaultValues={{ title: 'New note' }}
- />
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ dataSource={notesDataSource}
+ dataSourceCache={notesCache}
+ rootPath="/notes"
+ initialPageSize={10}
+ defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ create: 'New Note',
+ edit: `Note ${editNoteId} - Edit`,
+ show: `Note ${showNoteId}`,
+ }}
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudBasic.tsx.preview b/docs/data/toolpad/core/components/crud/CrudBasic.tsx.preview
index 6fe5e4520ab..cbd61ec328d 100644
--- a/docs/data/toolpad/core/components/crud/CrudBasic.tsx.preview
+++ b/docs/data/toolpad/core/components/crud/CrudBasic.tsx.preview
@@ -4,4 +4,9 @@
rootPath="/notes"
initialPageSize={10}
defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ create: 'New Note',
+ edit: `Note ${editNoteId} - Edit`,
+ show: `Note ${showNoteId}`,
+ }}
/>
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/crud/CrudCreate.js b/docs/data/toolpad/core/components/crud/CrudCreate.js
index 489584269f4..571f59199ce 100644
--- a/docs/data/toolpad/core/components/crud/CrudCreate.js
+++ b/docs/data/toolpad/core/components/crud/CrudCreate.js
@@ -4,7 +4,6 @@ import { createTheme } from '@mui/material/styles';
import PersonIcon from '@mui/icons-material/Person';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Create, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -124,17 +123,16 @@ function CrudCreate(props) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudCreate.tsx b/docs/data/toolpad/core/components/crud/CrudCreate.tsx
index cfb197c6404..de6008e3bb4 100644
--- a/docs/data/toolpad/core/components/crud/CrudCreate.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudCreate.tsx
@@ -3,7 +3,6 @@ import { createTheme } from '@mui/material/styles';
import PersonIcon from '@mui/icons-material/Person';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { DataModel, DataSource, Create, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -139,17 +138,16 @@ export default function CrudCreate(props: DemoProps) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- dataSource={peopleDataSource}
- dataSourceCache={peopleCache}
- initialValues={{ age: 18 }}
- onSubmitSuccess={handleSubmitSuccess}
- resetOnSubmit
- />
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ dataSource={peopleDataSource}
+ dataSourceCache={peopleCache}
+ initialValues={{ age: 18 }}
+ onSubmitSuccess={handleSubmitSuccess}
+ resetOnSubmit
+ pageTitle="New Person"
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudCreate.tsx.preview b/docs/data/toolpad/core/components/crud/CrudCreate.tsx.preview
index 2820651ba42..eaab3bbb808 100644
--- a/docs/data/toolpad/core/components/crud/CrudCreate.tsx.preview
+++ b/docs/data/toolpad/core/components/crud/CrudCreate.tsx.preview
@@ -4,4 +4,5 @@
initialValues={{ age: 18 }}
onSubmitSuccess={handleSubmitSuccess}
resetOnSubmit
+ pageTitle="New Person"
/>
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/crud/CrudCustomFormField.js b/docs/data/toolpad/core/components/crud/CrudCustomFormField.js
index e206c0e16fe..62e7f12febf 100644
--- a/docs/data/toolpad/core/components/crud/CrudCustomFormField.js
+++ b/docs/data/toolpad/core/components/crud/CrudCustomFormField.js
@@ -11,7 +11,6 @@ import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -285,21 +284,8 @@ function CrudCustomFormField(props) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/notes/new') {
- return 'New Note';
- }
- const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath('/notes/:noteId', router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showNoteId = matchPath('/notes/:noteId', router.pathname);
+ const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -311,15 +297,18 @@ function CrudCustomFormField(props) {
window={demoWindow}
>
-
-
-
+
diff --git a/docs/data/toolpad/core/components/crud/CrudCustomFormField.tsx b/docs/data/toolpad/core/components/crud/CrudCustomFormField.tsx
index 6a243ad3c6b..5451dc0de53 100644
--- a/docs/data/toolpad/core/components/crud/CrudCustomFormField.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudCustomFormField.tsx
@@ -10,7 +10,6 @@ import MenuItem from '@mui/material/MenuItem';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -312,21 +311,8 @@ export default function CrudCustomFormField(props: DemoProps) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/notes/new') {
- return 'New Note';
- }
- const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath('/notes/:noteId', router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showNoteId = matchPath('/notes/:noteId', router.pathname);
+ const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -338,15 +324,18 @@ export default function CrudCustomFormField(props: DemoProps) {
window={demoWindow}
>
-
-
- dataSource={notesDataSource}
- dataSourceCache={notesCache}
- rootPath="/notes"
- initialPageSize={10}
- defaultValues={{ title: 'New note' }}
- />
-
+
+ dataSource={notesDataSource}
+ dataSourceCache={notesCache}
+ rootPath="/notes"
+ initialPageSize={10}
+ defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ create: 'New Note',
+ edit: `Note ${editNoteId} - Edit`,
+ show: `Note ${showNoteId}`,
+ }}
+ />
diff --git a/docs/data/toolpad/core/components/crud/CrudCustomPageTitles.js b/docs/data/toolpad/core/components/crud/CrudCustomPageTitles.js
new file mode 100644
index 00000000000..139b6d7db01
--- /dev/null
+++ b/docs/data/toolpad/core/components/crud/CrudCustomPageTitles.js
@@ -0,0 +1,246 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { createTheme } from '@mui/material/styles';
+import StickyNote2Icon from '@mui/icons-material/StickyNote2';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import { Crud, DataSourceCache } from '@toolpad/core/Crud';
+import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
+
+const NAVIGATION = [
+ {
+ segment: 'notes',
+ title: 'Notes',
+ icon: ,
+ pattern: 'notes{/:noteId}*',
+ },
+];
+
+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,
+ },
+ },
+});
+
+let notesStore = [
+ { id: 1, title: 'Grocery List Item', text: 'Buy more coffee.' },
+ { id: 2, title: 'Personal Goal', text: 'Finish reading the book.' },
+];
+
+export const notesDataSource = {
+ fields: [
+ { field: 'id', headerName: 'ID' },
+ { field: 'title', headerName: 'Title', flex: 1 },
+ { field: 'text', headerName: 'Text', flex: 1 },
+ ],
+ getMany: async ({ paginationModel, filterModel, sortModel }) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ let processedNotes = [...notesStore];
+
+ // Apply filters (demo only)
+ if (filterModel?.items?.length) {
+ filterModel.items.forEach(({ field, value, operator }) => {
+ if (!field || value == null) {
+ return;
+ }
+
+ processedNotes = processedNotes.filter((note) => {
+ const noteValue = note[field];
+
+ switch (operator) {
+ case 'contains':
+ return String(noteValue)
+ .toLowerCase()
+ .includes(String(value).toLowerCase());
+ case 'equals':
+ return noteValue === value;
+ case 'startsWith':
+ return String(noteValue)
+ .toLowerCase()
+ .startsWith(String(value).toLowerCase());
+ case 'endsWith':
+ return String(noteValue)
+ .toLowerCase()
+ .endsWith(String(value).toLowerCase());
+ case '>':
+ return noteValue > value;
+ case '<':
+ return noteValue < value;
+ default:
+ return true;
+ }
+ });
+ });
+ }
+
+ // Apply sorting
+ if (sortModel?.length) {
+ processedNotes.sort((a, b) => {
+ for (const { field, sort } of sortModel) {
+ if (a[field] < b[field]) {
+ return sort === 'asc' ? -1 : 1;
+ }
+ if (a[field] > b[field]) {
+ return sort === 'asc' ? 1 : -1;
+ }
+ }
+ return 0;
+ });
+ }
+
+ // Apply pagination
+ const start = paginationModel.page * paginationModel.pageSize;
+ const end = start + paginationModel.pageSize;
+ const paginatedNotes = processedNotes.slice(start, end);
+
+ return {
+ items: paginatedNotes,
+ itemCount: processedNotes.length,
+ };
+ },
+ getOne: async (noteId) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ const noteToShow = notesStore.find((note) => note.id === Number(noteId));
+
+ if (!noteToShow) {
+ throw new Error('Note not found');
+ }
+ return noteToShow;
+ },
+ createOne: async (data) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ const newNote = {
+ id: notesStore.reduce((max, note) => Math.max(max, note.id), 0) + 1,
+ ...data,
+ };
+
+ notesStore = [...notesStore, newNote];
+
+ return newNote;
+ },
+ updateOne: async (noteId, data) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ let updatedNote = null;
+
+ notesStore = notesStore.map((note) => {
+ if (note.id === Number(noteId)) {
+ updatedNote = { ...note, ...data };
+ return updatedNote;
+ }
+ return note;
+ });
+
+ if (!updatedNote) {
+ throw new Error('Note not found');
+ }
+ return updatedNote;
+ },
+ deleteOne: async (noteId) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ notesStore = notesStore.filter((note) => note.id !== Number(noteId));
+ },
+ validate: (formValues) => {
+ let issues = [];
+
+ if (!formValues.title) {
+ issues = [...issues, { message: 'Title is required', path: ['title'] }];
+ }
+
+ if (formValues.title && formValues.title.length < 3) {
+ issues = [
+ ...issues,
+ {
+ message: 'Title must be at least 3 characters long',
+ path: ['title'],
+ },
+ ];
+ }
+
+ if (!formValues.text) {
+ issues = [...issues, { message: 'Text is required', path: ['text'] }];
+ }
+
+ return { issues };
+ },
+};
+
+const notesCache = new DataSourceCache();
+
+function CrudCustomPageTitles(props) {
+ const { window } = props;
+
+ const router = useDemoRouter('/notes');
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+ // Remove this provider when copying and pasting into your project.
+
+
+
+ {/* preview-start */}
+
+ {/* preview-end */}
+
+
+
+ );
+}
+
+CrudCustomPageTitles.propTypes = {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window: PropTypes.func,
+};
+
+export default CrudCustomPageTitles;
diff --git a/docs/data/toolpad/core/components/crud/CrudCustomPageTitles.tsx b/docs/data/toolpad/core/components/crud/CrudCustomPageTitles.tsx
new file mode 100644
index 00000000000..e66e50fe423
--- /dev/null
+++ b/docs/data/toolpad/core/components/crud/CrudCustomPageTitles.tsx
@@ -0,0 +1,255 @@
+import * as React from 'react';
+import { createTheme } from '@mui/material/styles';
+import StickyNote2Icon from '@mui/icons-material/StickyNote2';
+import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import { Crud, DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud';
+import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
+
+const NAVIGATION: Navigation = [
+ {
+ segment: 'notes',
+ title: 'Notes',
+ icon: ,
+ pattern: 'notes{/:noteId}*',
+ },
+];
+
+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,
+ },
+ },
+});
+
+export interface Note extends DataModel {
+ id: number;
+ title: string;
+ text: string;
+}
+
+let notesStore: Note[] = [
+ { id: 1, title: 'Grocery List Item', text: 'Buy more coffee.' },
+ { id: 2, title: 'Personal Goal', text: 'Finish reading the book.' },
+];
+
+export const notesDataSource: DataSource = {
+ fields: [
+ { field: 'id', headerName: 'ID' },
+ { field: 'title', headerName: 'Title', flex: 1 },
+ { field: 'text', headerName: 'Text', flex: 1 },
+ ],
+
+ getMany: async ({ paginationModel, filterModel, sortModel }) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ let processedNotes = [...notesStore];
+
+ // Apply filters (demo only)
+ if (filterModel?.items?.length) {
+ filterModel.items.forEach(({ field, value, operator }) => {
+ if (!field || value == null) {
+ return;
+ }
+
+ processedNotes = processedNotes.filter((note) => {
+ const noteValue = note[field];
+
+ switch (operator) {
+ case 'contains':
+ return String(noteValue)
+ .toLowerCase()
+ .includes(String(value).toLowerCase());
+ case 'equals':
+ return noteValue === value;
+ case 'startsWith':
+ return String(noteValue)
+ .toLowerCase()
+ .startsWith(String(value).toLowerCase());
+ case 'endsWith':
+ return String(noteValue)
+ .toLowerCase()
+ .endsWith(String(value).toLowerCase());
+ case '>':
+ return (noteValue as number) > value;
+ case '<':
+ return (noteValue as number) < value;
+ default:
+ return true;
+ }
+ });
+ });
+ }
+
+ // Apply sorting
+ if (sortModel?.length) {
+ processedNotes.sort((a, b) => {
+ for (const { field, sort } of sortModel) {
+ if ((a[field] as number) < (b[field] as number)) {
+ return sort === 'asc' ? -1 : 1;
+ }
+ if ((a[field] as number) > (b[field] as number)) {
+ return sort === 'asc' ? 1 : -1;
+ }
+ }
+ return 0;
+ });
+ }
+
+ // Apply pagination
+ const start = paginationModel.page * paginationModel.pageSize;
+ const end = start + paginationModel.pageSize;
+ const paginatedNotes = processedNotes.slice(start, end);
+
+ return {
+ items: paginatedNotes,
+ itemCount: processedNotes.length,
+ };
+ },
+
+ getOne: async (noteId) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ const noteToShow = notesStore.find((note) => note.id === Number(noteId));
+
+ if (!noteToShow) {
+ throw new Error('Note not found');
+ }
+ return noteToShow;
+ },
+
+ createOne: async (data) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ const newNote = {
+ id: notesStore.reduce((max, note) => Math.max(max, note.id), 0) + 1,
+ ...data,
+ } as Note;
+
+ notesStore = [...notesStore, newNote];
+
+ return newNote;
+ },
+
+ updateOne: async (noteId, data) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ let updatedNote: Note | null = null;
+
+ notesStore = notesStore.map((note) => {
+ if (note.id === Number(noteId)) {
+ updatedNote = { ...note, ...data };
+ return updatedNote;
+ }
+ return note;
+ });
+
+ if (!updatedNote) {
+ throw new Error('Note not found');
+ }
+ return updatedNote;
+ },
+
+ deleteOne: async (noteId) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ notesStore = notesStore.filter((note) => note.id !== Number(noteId));
+ },
+
+ validate: (formValues) => {
+ let issues: { message: string; path: [keyof Note] }[] = [];
+
+ if (!formValues.title) {
+ issues = [...issues, { message: 'Title is required', path: ['title'] }];
+ }
+
+ if (formValues.title && formValues.title.length < 3) {
+ issues = [
+ ...issues,
+ {
+ message: 'Title must be at least 3 characters long',
+ path: ['title'],
+ },
+ ];
+ }
+
+ if (!formValues.text) {
+ issues = [...issues, { message: 'Text is required', path: ['text'] }];
+ }
+
+ return { issues };
+ },
+};
+
+const notesCache = new DataSourceCache();
+
+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 CrudCustomPageTitles(props: DemoProps) {
+ const { window } = props;
+
+ const router = useDemoRouter('/notes');
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+ // Remove this provider when copying and pasting into your project.
+
+
+
+ {/* preview-start */}
+
+ dataSource={notesDataSource}
+ dataSourceCache={notesCache}
+ rootPath="/notes"
+ initialPageSize={10}
+ defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ list: 'Custom Title for List',
+ create: 'Custom Title for Create',
+ edit: 'Custom Title for Edit',
+ show: 'Custom Title for Show',
+ }}
+ />
+ {/* preview-end */}
+
+
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/crud/CrudCustomPageTitles.tsx.preview b/docs/data/toolpad/core/components/crud/CrudCustomPageTitles.tsx.preview
new file mode 100644
index 00000000000..128005577bc
--- /dev/null
+++ b/docs/data/toolpad/core/components/crud/CrudCustomPageTitles.tsx.preview
@@ -0,0 +1,13 @@
+
+ dataSource={notesDataSource}
+ dataSourceCache={notesCache}
+ rootPath="/notes"
+ initialPageSize={10}
+ defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ list: 'Custom Title for List',
+ create: 'Custom Title for Create',
+ edit: 'Custom Title for Edit',
+ show: 'Custom Title for Show',
+ }}
+/>
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/crud/CrudDataGridSlots.js b/docs/data/toolpad/core/components/crud/CrudDataGridSlots.js
index 10e4fd5c657..50bc12239b4 100644
--- a/docs/data/toolpad/core/components/crud/CrudDataGridSlots.js
+++ b/docs/data/toolpad/core/components/crud/CrudDataGridSlots.js
@@ -5,7 +5,6 @@ import StickyNote2Icon from '@mui/icons-material/StickyNote2';
import { DataGridPro } from '@mui/x-data-grid-pro';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -212,21 +211,8 @@ function CrudDataGridSlots(props) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/notes/new') {
- return 'New Note';
- }
- const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath('/notes/:noteId', router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showNoteId = matchPath('/notes/:noteId', router.pathname);
+ const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -238,25 +224,28 @@ function CrudDataGridSlots(props) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- {/* preview-end */}
-
+ },
+ }}
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudDataGridSlots.tsx b/docs/data/toolpad/core/components/crud/CrudDataGridSlots.tsx
index d292769c795..ae618885f0d 100644
--- a/docs/data/toolpad/core/components/crud/CrudDataGridSlots.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudDataGridSlots.tsx
@@ -4,7 +4,6 @@ import StickyNote2Icon from '@mui/icons-material/StickyNote2';
import { DataGridPro } from '@mui/x-data-grid-pro';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -231,21 +230,8 @@ export default function CrudDataGridSlots(props: DemoProps) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/notes/new') {
- return 'New Note';
- }
- const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath('/notes/:noteId', router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showNoteId = matchPath('/notes/:noteId', router.pathname);
+ const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -257,25 +243,28 @@ export default function CrudDataGridSlots(props: DemoProps) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- dataSource={notesDataSource}
- dataSourceCache={notesCache}
- rootPath="/notes"
- initialPageSize={10}
- defaultValues={{ title: 'New note' }}
- slots={{ list: { dataGrid: DataGridPro } }}
- slotProps={{
- list: {
- dataGrid: {
- initialState: { pinnedColumns: { right: ['actions'] } },
- },
+ {/* preview-start */}
+
+ dataSource={notesDataSource}
+ dataSourceCache={notesCache}
+ rootPath="/notes"
+ initialPageSize={10}
+ defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ create: 'New Note',
+ edit: `Note ${editNoteId} - Edit`,
+ show: `Note ${showNoteId}`,
+ }}
+ slots={{ list: { dataGrid: DataGridPro } }}
+ slotProps={{
+ list: {
+ dataGrid: {
+ initialState: { pinnedColumns: { right: ['actions'] } },
},
- }}
- />
- {/* preview-end */}
-
+ },
+ }}
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudDataGridSlots.tsx.preview b/docs/data/toolpad/core/components/crud/CrudDataGridSlots.tsx.preview
index 5030bf5981b..9c5056a9f6c 100644
--- a/docs/data/toolpad/core/components/crud/CrudDataGridSlots.tsx.preview
+++ b/docs/data/toolpad/core/components/crud/CrudDataGridSlots.tsx.preview
@@ -4,6 +4,11 @@
rootPath="/notes"
initialPageSize={10}
defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ create: 'New Note',
+ edit: `Note ${editNoteId} - Edit`,
+ show: `Note ${showNoteId}`,
+ }}
slots={{ list: { dataGrid: DataGridPro } }}
slotProps={{
list: {
diff --git a/docs/data/toolpad/core/components/crud/CrudEdit.js b/docs/data/toolpad/core/components/crud/CrudEdit.js
index 6f99b53fe9a..fca8ceb5bdf 100644
--- a/docs/data/toolpad/core/components/crud/CrudEdit.js
+++ b/docs/data/toolpad/core/components/crud/CrudEdit.js
@@ -4,7 +4,6 @@ import { createTheme } from '@mui/material/styles';
import PersonIcon from '@mui/icons-material/Person';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { DataSourceCache, Edit } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -145,16 +144,15 @@ function CrudEdit(props) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudEdit.tsx b/docs/data/toolpad/core/components/crud/CrudEdit.tsx
index 752a326513e..2d132dc6782 100644
--- a/docs/data/toolpad/core/components/crud/CrudEdit.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudEdit.tsx
@@ -3,7 +3,6 @@ import { createTheme } from '@mui/material/styles';
import PersonIcon from '@mui/icons-material/Person';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { DataModel, DataSource, DataSourceCache, Edit } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -160,16 +159,15 @@ export default function CrudEdit(props: DemoProps) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- id={1}
- dataSource={peopleDataSource}
- dataSourceCache={peopleCache}
- onSubmitSuccess={handleSubmitSuccess}
- />
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ id={1}
+ dataSource={peopleDataSource}
+ dataSourceCache={peopleCache}
+ onSubmitSuccess={handleSubmitSuccess}
+ pageTitle="Person 1 - Edit"
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudEdit.tsx.preview b/docs/data/toolpad/core/components/crud/CrudEdit.tsx.preview
index 9aa1ab0805e..5dd43992fc0 100644
--- a/docs/data/toolpad/core/components/crud/CrudEdit.tsx.preview
+++ b/docs/data/toolpad/core/components/crud/CrudEdit.tsx.preview
@@ -3,4 +3,5 @@
dataSource={peopleDataSource}
dataSourceCache={peopleCache}
onSubmitSuccess={handleSubmitSuccess}
+ pageTitle="Person 1 - Edit"
/>
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/crud/CrudFormSlots.js b/docs/data/toolpad/core/components/crud/CrudFormSlots.js
index b7acdee8bfe..de892e77ad2 100644
--- a/docs/data/toolpad/core/components/crud/CrudFormSlots.js
+++ b/docs/data/toolpad/core/components/crud/CrudFormSlots.js
@@ -4,7 +4,6 @@ import { createTheme } from '@mui/material/styles';
import LocalActivityIcon from '@mui/icons-material/LocalActivity';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -216,21 +215,8 @@ function CrudFormSlots(props) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/things/new') {
- return 'New Thing';
- }
- const editThingId = matchPath('/things/:thingId/edit', router.pathname);
- if (editThingId) {
- return `Thing ${editThingId} - Edit`;
- }
- const showThingId = matchPath('/things/:thingId', router.pathname);
- if (showThingId) {
- return `Thing ${showThingId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showThingId = matchPath('/things/:thingId', router.pathname);
+ const editThingId = matchPath('/things/:thingId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -242,33 +228,36 @@ function CrudFormSlots(props) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- {/* preview-end */}
-
+ },
+ }}
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudFormSlots.tsx b/docs/data/toolpad/core/components/crud/CrudFormSlots.tsx
index 9995a4e4224..f665f249958 100644
--- a/docs/data/toolpad/core/components/crud/CrudFormSlots.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudFormSlots.tsx
@@ -3,7 +3,6 @@ import { createTheme } from '@mui/material/styles';
import LocalActivityIcon from '@mui/icons-material/LocalActivity';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -236,21 +235,8 @@ export default function CrudFormSlots(props: DemoProps) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/things/new') {
- return 'New Thing';
- }
- const editThingId = matchPath('/things/:thingId/edit', router.pathname);
- if (editThingId) {
- return `Thing ${editThingId} - Edit`;
- }
- const showThingId = matchPath('/things/:thingId', router.pathname);
- if (showThingId) {
- return `Thing ${showThingId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showThingId = matchPath('/things/:thingId', router.pathname);
+ const editThingId = matchPath('/things/:thingId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -262,33 +248,36 @@ export default function CrudFormSlots(props: DemoProps) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- dataSource={thingsDataSource}
- dataSourceCache={thingsCache}
- rootPath="/things"
- initialPageSize={10}
- defaultValues={{ title: 'New thing' }}
- slotProps={{
- form: {
- textField: {
- variant: 'filled',
- },
- checkbox: {
- color: 'secondary',
- },
- datePicker: {
- views: ['year', 'month'],
- },
- select: {
- variant: 'standard',
- },
+ {/* preview-start */}
+
+ dataSource={thingsDataSource}
+ dataSourceCache={thingsCache}
+ rootPath="/things"
+ initialPageSize={10}
+ defaultValues={{ title: 'New thing' }}
+ pageTitles={{
+ create: 'New Thing',
+ edit: `Thing ${editThingId} - Edit`,
+ show: `Thing ${showThingId}`,
+ }}
+ slotProps={{
+ form: {
+ textField: {
+ variant: 'filled',
+ },
+ checkbox: {
+ color: 'secondary',
+ },
+ datePicker: {
+ views: ['year', 'month'],
+ },
+ select: {
+ variant: 'standard',
},
- }}
- />
- {/* preview-end */}
-
+ },
+ }}
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudFormSlots.tsx.preview b/docs/data/toolpad/core/components/crud/CrudFormSlots.tsx.preview
index 3cc288520a9..2cf41dd3d3b 100644
--- a/docs/data/toolpad/core/components/crud/CrudFormSlots.tsx.preview
+++ b/docs/data/toolpad/core/components/crud/CrudFormSlots.tsx.preview
@@ -4,6 +4,11 @@
rootPath="/things"
initialPageSize={10}
defaultValues={{ title: 'New thing' }}
+ pageTitles={{
+ create: 'New Thing',
+ edit: `Thing ${editThingId} - Edit`,
+ show: `Thing ${showThingId}`,
+ }}
slotProps={{
form: {
textField: {
diff --git a/docs/data/toolpad/core/components/crud/CrudList.js b/docs/data/toolpad/core/components/crud/CrudList.js
index a0fea33c1e5..5a62563b24c 100644
--- a/docs/data/toolpad/core/components/crud/CrudList.js
+++ b/docs/data/toolpad/core/components/crud/CrudList.js
@@ -4,7 +4,6 @@ import { createTheme } from '@mui/material/styles';
import PersonIcon from '@mui/icons-material/Person';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { DataSourceCache, List } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -175,19 +174,17 @@ function CrudList(props) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudList.tsx b/docs/data/toolpad/core/components/crud/CrudList.tsx
index e751702611d..ab70f559f2f 100644
--- a/docs/data/toolpad/core/components/crud/CrudList.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudList.tsx
@@ -3,7 +3,6 @@ import { createTheme } from '@mui/material/styles';
import PersonIcon from '@mui/icons-material/Person';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { DataModel, DataSource, DataSourceCache, List } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -190,19 +189,17 @@ export default function CrudList(props: DemoProps) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- dataSource={peopleDataSource}
- dataSourceCache={peopleCache}
- initialPageSize={4}
- onRowClick={handleRowClick}
- onCreateClick={handleCreateClick}
- onEditClick={handleEditClick}
- onDelete={handleDelete}
- />
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ dataSource={peopleDataSource}
+ dataSourceCache={peopleCache}
+ initialPageSize={4}
+ onRowClick={handleRowClick}
+ onCreateClick={handleCreateClick}
+ onEditClick={handleEditClick}
+ onDelete={handleDelete}
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudNoCache.js b/docs/data/toolpad/core/components/crud/CrudNoCache.js
index a6b4b3764ff..fd0f4f53f72 100644
--- a/docs/data/toolpad/core/components/crud/CrudNoCache.js
+++ b/docs/data/toolpad/core/components/crud/CrudNoCache.js
@@ -4,7 +4,6 @@ import { createTheme } from '@mui/material/styles';
import StickyNote2Icon from '@mui/icons-material/StickyNote2';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -207,21 +206,8 @@ function CrudNoCache(props) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/notes/new') {
- return 'New Note';
- }
- const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath('/notes/:noteId', router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showNoteId = matchPath('/notes/:noteId', router.pathname);
+ const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -233,17 +219,20 @@ function CrudNoCache(props) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudNoCache.tsx b/docs/data/toolpad/core/components/crud/CrudNoCache.tsx
index f6737901ec3..bad5a7aad9e 100644
--- a/docs/data/toolpad/core/components/crud/CrudNoCache.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudNoCache.tsx
@@ -3,7 +3,6 @@ import { createTheme } from '@mui/material/styles';
import StickyNote2Icon from '@mui/icons-material/StickyNote2';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataModel, DataSource } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -220,21 +219,8 @@ export default function CrudNoCache(props: DemoProps) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/notes/new') {
- return 'New Note';
- }
- const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath('/notes/:noteId', router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showNoteId = matchPath('/notes/:noteId', router.pathname);
+ const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -246,17 +232,20 @@ export default function CrudNoCache(props: DemoProps) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- dataSource={notesDataSource}
- dataSourceCache={null}
- rootPath="/notes"
- initialPageSize={10}
- defaultValues={{ title: 'New note' }}
- />
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ dataSource={notesDataSource}
+ dataSourceCache={null}
+ rootPath="/notes"
+ initialPageSize={10}
+ defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ create: 'New Note',
+ edit: `Note ${editNoteId} - Edit`,
+ show: `Note ${showNoteId}`,
+ }}
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudNoCache.tsx.preview b/docs/data/toolpad/core/components/crud/CrudNoCache.tsx.preview
index ad1072142d5..9dd70c57053 100644
--- a/docs/data/toolpad/core/components/crud/CrudNoCache.tsx.preview
+++ b/docs/data/toolpad/core/components/crud/CrudNoCache.tsx.preview
@@ -4,4 +4,9 @@
rootPath="/notes"
initialPageSize={10}
defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ create: 'New Note',
+ edit: `Note ${editNoteId} - Edit`,
+ show: `Note ${showNoteId}`,
+ }}
/>
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/crud/CrudNonEditableFields.js b/docs/data/toolpad/core/components/crud/CrudNonEditableFields.js
index 3f31008bc1b..5917eead108 100644
--- a/docs/data/toolpad/core/components/crud/CrudNonEditableFields.js
+++ b/docs/data/toolpad/core/components/crud/CrudNonEditableFields.js
@@ -4,7 +4,6 @@ import { createTheme } from '@mui/material/styles';
import StickyNote2Icon from '@mui/icons-material/StickyNote2';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -231,21 +230,8 @@ function CrudNonEditableFields(props) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/notes/new') {
- return 'New Note';
- }
- const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath('/notes/:noteId', router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showNoteId = matchPath('/notes/:noteId', router.pathname);
+ const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -257,17 +243,20 @@ function CrudNonEditableFields(props) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudNonEditableFields.tsx b/docs/data/toolpad/core/components/crud/CrudNonEditableFields.tsx
index 980f0297bec..26328b10637 100644
--- a/docs/data/toolpad/core/components/crud/CrudNonEditableFields.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudNonEditableFields.tsx
@@ -3,7 +3,6 @@ import { createTheme } from '@mui/material/styles';
import StickyNote2Icon from '@mui/icons-material/StickyNote2';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { Crud, DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -250,21 +249,8 @@ export default function CrudNonEditableFields(props: DemoProps) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/notes/new') {
- return 'New Note';
- }
- const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
- if (editNoteId) {
- return `Note ${editNoteId} - Edit`;
- }
- const showNoteId = matchPath('/notes/:noteId', router.pathname);
- if (showNoteId) {
- return `Note ${showNoteId}`;
- }
-
- return undefined;
- }, [router.pathname]);
+ const showNoteId = matchPath('/notes/:noteId', router.pathname);
+ const editNoteId = matchPath('/notes/:noteId/edit', router.pathname);
return (
// Remove this provider when copying and pasting into your project.
@@ -276,17 +262,20 @@ export default function CrudNonEditableFields(props: DemoProps) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- dataSource={notesDataSource}
- dataSourceCache={notesCache}
- rootPath="/notes"
- initialPageSize={10}
- defaultValues={{ title: 'New note' }}
- />
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ dataSource={notesDataSource}
+ dataSourceCache={notesCache}
+ rootPath="/notes"
+ initialPageSize={10}
+ defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ create: 'New Note',
+ edit: `Note ${editNoteId} - Edit`,
+ show: `Note ${showNoteId}`,
+ }}
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudNonEditableFields.tsx.preview b/docs/data/toolpad/core/components/crud/CrudNonEditableFields.tsx.preview
index 2d2c3ac7f02..f054cdb9576 100644
--- a/docs/data/toolpad/core/components/crud/CrudNonEditableFields.tsx.preview
+++ b/docs/data/toolpad/core/components/crud/CrudNonEditableFields.tsx.preview
@@ -20,4 +20,9 @@ export const notesDataSource: DataSource = {
rootPath="/notes"
initialPageSize={10}
defaultValues={{ title: 'New note' }}
+ pageTitles={{
+ create: 'New Note',
+ edit: `Note ${editNoteId} - Edit`,
+ show: `Note ${showNoteId}`,
+ }}
/>
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/crud/CrudPageContainerSlots.js b/docs/data/toolpad/core/components/crud/CrudPageContainerSlots.js
new file mode 100644
index 00000000000..ca300c15154
--- /dev/null
+++ b/docs/data/toolpad/core/components/crud/CrudPageContainerSlots.js
@@ -0,0 +1,260 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { createTheme } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+import StickyNote2Icon from '@mui/icons-material/StickyNote2';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import { Crud, DataSourceCache } from '@toolpad/core/Crud';
+import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
+import { Typography } from '@mui/material';
+
+const NAVIGATION = [
+ {
+ segment: 'notes',
+ title: 'Notes',
+ icon: ,
+ pattern: 'notes{/:noteId}*',
+ },
+];
+
+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,
+ },
+ },
+});
+
+let notesStore = [
+ { id: 1, title: 'Grocery List Item', text: 'Buy more coffee.' },
+ { id: 2, title: 'Personal Goal', text: 'Finish reading the book.' },
+];
+
+export const notesDataSource = {
+ fields: [
+ { field: 'id', headerName: 'ID' },
+ { field: 'title', headerName: 'Title', flex: 1 },
+ { field: 'text', headerName: 'Text', flex: 1 },
+ ],
+ getMany: async ({ paginationModel, filterModel, sortModel }) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ let processedNotes = [...notesStore];
+
+ // Apply filters (demo only)
+ if (filterModel?.items?.length) {
+ filterModel.items.forEach(({ field, value, operator }) => {
+ if (!field || value == null) {
+ return;
+ }
+
+ processedNotes = processedNotes.filter((note) => {
+ const noteValue = note[field];
+
+ switch (operator) {
+ case 'contains':
+ return String(noteValue)
+ .toLowerCase()
+ .includes(String(value).toLowerCase());
+ case 'equals':
+ return noteValue === value;
+ case 'startsWith':
+ return String(noteValue)
+ .toLowerCase()
+ .startsWith(String(value).toLowerCase());
+ case 'endsWith':
+ return String(noteValue)
+ .toLowerCase()
+ .endsWith(String(value).toLowerCase());
+ case '>':
+ return noteValue > value;
+ case '<':
+ return noteValue < value;
+ default:
+ return true;
+ }
+ });
+ });
+ }
+
+ // Apply sorting
+ if (sortModel?.length) {
+ processedNotes.sort((a, b) => {
+ for (const { field, sort } of sortModel) {
+ if (a[field] < b[field]) {
+ return sort === 'asc' ? -1 : 1;
+ }
+ if (a[field] > b[field]) {
+ return sort === 'asc' ? 1 : -1;
+ }
+ }
+ return 0;
+ });
+ }
+
+ // Apply pagination
+ const start = paginationModel.page * paginationModel.pageSize;
+ const end = start + paginationModel.pageSize;
+ const paginatedNotes = processedNotes.slice(start, end);
+
+ return {
+ items: paginatedNotes,
+ itemCount: processedNotes.length,
+ };
+ },
+ getOne: async (noteId) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ const noteToShow = notesStore.find((note) => note.id === Number(noteId));
+
+ if (!noteToShow) {
+ throw new Error('Note not found');
+ }
+ return noteToShow;
+ },
+ createOne: async (data) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ const newNote = {
+ id: notesStore.reduce((max, note) => Math.max(max, note.id), 0) + 1,
+ ...data,
+ };
+
+ notesStore = [...notesStore, newNote];
+
+ return newNote;
+ },
+ updateOne: async (noteId, data) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ let updatedNote = null;
+
+ notesStore = notesStore.map((note) => {
+ if (note.id === Number(noteId)) {
+ updatedNote = { ...note, ...data };
+ return updatedNote;
+ }
+ return note;
+ });
+
+ if (!updatedNote) {
+ throw new Error('Note not found');
+ }
+ return updatedNote;
+ },
+ deleteOne: async (noteId) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ notesStore = notesStore.filter((note) => note.id !== Number(noteId));
+ },
+ validate: (formValues) => {
+ let issues = [];
+
+ if (!formValues.title) {
+ issues = [...issues, { message: 'Title is required', path: ['title'] }];
+ }
+
+ if (formValues.title && formValues.title.length < 3) {
+ issues = [
+ ...issues,
+ {
+ message: 'Title must be at least 3 characters long',
+ path: ['title'],
+ },
+ ];
+ }
+
+ if (!formValues.text) {
+ issues = [...issues, { message: 'Text is required', path: ['text'] }];
+ }
+
+ return { issues };
+ },
+};
+
+const notesCache = new DataSourceCache();
+
+function CustomPageContainer({ children }) {
+ return (
+
+
+ My External Title
+
+ {children}
+
+ );
+}
+
+CustomPageContainer.propTypes = {
+ children: PropTypes.node,
+};
+
+function CrudPageContainerSlots(props) {
+ const { window } = props;
+
+ const router = useDemoRouter('/notes');
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+ // Remove this provider when copying and pasting into your project.
+
+
+
+ {/* preview-start */}
+
+ {/* preview-end */}
+
+
+
+ );
+}
+
+CrudPageContainerSlots.propTypes = {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window: PropTypes.func,
+};
+
+export default CrudPageContainerSlots;
diff --git a/docs/data/toolpad/core/components/crud/CrudPageContainerSlots.tsx b/docs/data/toolpad/core/components/crud/CrudPageContainerSlots.tsx
new file mode 100644
index 00000000000..a2d2b40044a
--- /dev/null
+++ b/docs/data/toolpad/core/components/crud/CrudPageContainerSlots.tsx
@@ -0,0 +1,265 @@
+import * as React from 'react';
+import { createTheme } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+import StickyNote2Icon from '@mui/icons-material/StickyNote2';
+import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import { Crud, DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud';
+import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
+import { Typography } from '@mui/material';
+
+const NAVIGATION: Navigation = [
+ {
+ segment: 'notes',
+ title: 'Notes',
+ icon: ,
+ pattern: 'notes{/:noteId}*',
+ },
+];
+
+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,
+ },
+ },
+});
+
+export interface Note extends DataModel {
+ id: number;
+ title: string;
+ text: string;
+}
+
+let notesStore: Note[] = [
+ { id: 1, title: 'Grocery List Item', text: 'Buy more coffee.' },
+ { id: 2, title: 'Personal Goal', text: 'Finish reading the book.' },
+];
+
+export const notesDataSource: DataSource = {
+ fields: [
+ { field: 'id', headerName: 'ID' },
+ { field: 'title', headerName: 'Title', flex: 1 },
+ { field: 'text', headerName: 'Text', flex: 1 },
+ ],
+
+ getMany: async ({ paginationModel, filterModel, sortModel }) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ let processedNotes = [...notesStore];
+
+ // Apply filters (demo only)
+ if (filterModel?.items?.length) {
+ filterModel.items.forEach(({ field, value, operator }) => {
+ if (!field || value == null) {
+ return;
+ }
+
+ processedNotes = processedNotes.filter((note) => {
+ const noteValue = note[field];
+
+ switch (operator) {
+ case 'contains':
+ return String(noteValue)
+ .toLowerCase()
+ .includes(String(value).toLowerCase());
+ case 'equals':
+ return noteValue === value;
+ case 'startsWith':
+ return String(noteValue)
+ .toLowerCase()
+ .startsWith(String(value).toLowerCase());
+ case 'endsWith':
+ return String(noteValue)
+ .toLowerCase()
+ .endsWith(String(value).toLowerCase());
+ case '>':
+ return (noteValue as number) > value;
+ case '<':
+ return (noteValue as number) < value;
+ default:
+ return true;
+ }
+ });
+ });
+ }
+
+ // Apply sorting
+ if (sortModel?.length) {
+ processedNotes.sort((a, b) => {
+ for (const { field, sort } of sortModel) {
+ if ((a[field] as number) < (b[field] as number)) {
+ return sort === 'asc' ? -1 : 1;
+ }
+ if ((a[field] as number) > (b[field] as number)) {
+ return sort === 'asc' ? 1 : -1;
+ }
+ }
+ return 0;
+ });
+ }
+
+ // Apply pagination
+ const start = paginationModel.page * paginationModel.pageSize;
+ const end = start + paginationModel.pageSize;
+ const paginatedNotes = processedNotes.slice(start, end);
+
+ return {
+ items: paginatedNotes,
+ itemCount: processedNotes.length,
+ };
+ },
+
+ getOne: async (noteId) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ const noteToShow = notesStore.find((note) => note.id === Number(noteId));
+
+ if (!noteToShow) {
+ throw new Error('Note not found');
+ }
+ return noteToShow;
+ },
+
+ createOne: async (data) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ const newNote = {
+ id: notesStore.reduce((max, note) => Math.max(max, note.id), 0) + 1,
+ ...data,
+ } as Note;
+
+ notesStore = [...notesStore, newNote];
+
+ return newNote;
+ },
+
+ updateOne: async (noteId, data) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ let updatedNote: Note | null = null;
+
+ notesStore = notesStore.map((note) => {
+ if (note.id === Number(noteId)) {
+ updatedNote = { ...note, ...data };
+ return updatedNote;
+ }
+ return note;
+ });
+
+ if (!updatedNote) {
+ throw new Error('Note not found');
+ }
+ return updatedNote;
+ },
+
+ deleteOne: async (noteId) => {
+ // Simulate loading delay
+ await new Promise((resolve) => {
+ setTimeout(resolve, 750);
+ });
+
+ notesStore = notesStore.filter((note) => note.id !== Number(noteId));
+ },
+
+ validate: (formValues) => {
+ let issues: { message: string; path: [keyof Note] }[] = [];
+
+ if (!formValues.title) {
+ issues = [...issues, { message: 'Title is required', path: ['title'] }];
+ }
+
+ if (formValues.title && formValues.title.length < 3) {
+ issues = [
+ ...issues,
+ {
+ message: 'Title must be at least 3 characters long',
+ path: ['title'],
+ },
+ ];
+ }
+
+ if (!formValues.text) {
+ issues = [...issues, { message: 'Text is required', path: ['text'] }];
+ }
+
+ return { issues };
+ },
+};
+
+const notesCache = new DataSourceCache();
+
+interface DemoProps {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window?: () => Window;
+}
+
+function CustomPageContainer({ children }: { children?: React.ReactNode }) {
+ return (
+
+
+ My External Title
+
+ {children}
+
+ );
+}
+
+export default function CrudPageContainerSlots(props: DemoProps) {
+ const { window } = props;
+
+ const router = useDemoRouter('/notes');
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+ // Remove this provider when copying and pasting into your project.
+
+
+
+ {/* preview-start */}
+
+ dataSource={notesDataSource}
+ dataSourceCache={notesCache}
+ rootPath="/notes"
+ initialPageSize={10}
+ defaultValues={{ title: 'New note' }}
+ slots={{
+ pageContainer: CustomPageContainer,
+ }}
+ />
+ {/* preview-end */}
+
+
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/crud/CrudPageContainerSlots.tsx.preview b/docs/data/toolpad/core/components/crud/CrudPageContainerSlots.tsx.preview
new file mode 100644
index 00000000000..d88c534a403
--- /dev/null
+++ b/docs/data/toolpad/core/components/crud/CrudPageContainerSlots.tsx.preview
@@ -0,0 +1,10 @@
+
+ dataSource={notesDataSource}
+ dataSourceCache={notesCache}
+ rootPath="/notes"
+ initialPageSize={10}
+ defaultValues={{ title: 'New note' }}
+ slots={{
+ pageContainer: CustomPageContainer,
+ }}
+/>
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/crud/CrudShow.js b/docs/data/toolpad/core/components/crud/CrudShow.js
index 722ed76b57a..057ace46727 100644
--- a/docs/data/toolpad/core/components/crud/CrudShow.js
+++ b/docs/data/toolpad/core/components/crud/CrudShow.js
@@ -4,7 +4,6 @@ import { createTheme } from '@mui/material/styles';
import PersonIcon from '@mui/icons-material/Person';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { DataSourceCache, Show } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -113,17 +112,16 @@ function CrudShow(props) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudShow.tsx b/docs/data/toolpad/core/components/crud/CrudShow.tsx
index 5942051f23f..7e7eca1d413 100644
--- a/docs/data/toolpad/core/components/crud/CrudShow.tsx
+++ b/docs/data/toolpad/core/components/crud/CrudShow.tsx
@@ -3,7 +3,6 @@ import { createTheme } from '@mui/material/styles';
import PersonIcon from '@mui/icons-material/Person';
import { AppProvider, type Navigation } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { DataModel, DataSource, DataSourceCache, Show } from '@toolpad/core/Crud';
import { DemoProvider, useDemoRouter } from '@toolpad/core/internal';
@@ -128,17 +127,16 @@ export default function CrudShow(props: DemoProps) {
window={demoWindow}
>
-
- {/* preview-start */}
-
- id={1}
- dataSource={peopleDataSource}
- dataSourceCache={peopleCache}
- onEditClick={handleEditClick}
- onDelete={handleDelete}
- />
- {/* preview-end */}
-
+ {/* preview-start */}
+
+ id={1}
+ dataSource={peopleDataSource}
+ dataSourceCache={peopleCache}
+ onEditClick={handleEditClick}
+ onDelete={handleDelete}
+ pageTitle="Person 1"
+ />
+ {/* preview-end */}
diff --git a/docs/data/toolpad/core/components/crud/CrudShow.tsx.preview b/docs/data/toolpad/core/components/crud/CrudShow.tsx.preview
index 44a1ce745e3..d3605a74772 100644
--- a/docs/data/toolpad/core/components/crud/CrudShow.tsx.preview
+++ b/docs/data/toolpad/core/components/crud/CrudShow.tsx.preview
@@ -4,4 +4,5 @@
dataSourceCache={peopleCache}
onEditClick={handleEditClick}
onDelete={handleDelete}
+ pageTitle="Person 1"
/>
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/crud/crud.md b/docs/data/toolpad/core/components/crud/crud.md
index 7fb8c905a0e..4d8d9a9fc0f 100644
--- a/docs/data/toolpad/core/components/crud/crud.md
+++ b/docs/data/toolpad/core/components/crud/crud.md
@@ -332,6 +332,12 @@ This function receives `value`, `onChange` and `error` as arguments for the resp
{{"demo": "CrudCustomFormField.js", "height": 600, "iframe": true}}
+### Custom page titles
+
+The `pageTitles` prop can be used to set custom page titles for each CRUD page.
+
+{{"demo": "CrudCustomPageTitles.js", "height": 600, "iframe": true}}
+
### Slots
Certain parts of the CRUD UIs can be customized or replaced with custom components by using the `slots` and `slotProps` props.
@@ -348,6 +354,12 @@ The `form` slots and slot props can be used, for example, to customize component
{{"demo": "CrudFormSlots.js", "height": 600, "iframe": true}}
+#### Override default page container
+
+The `pageContainer` slot can be used to override the default page container and use a custom wrapper component instead:
+
+{{"demo": "CrudPageContainerSlots.js", "height": 500, "iframe": true}}
+
## Advanced configuration
For more flexibility of customization, and especially if you want full control over where to place the different CRUD pages, you can use the `List`, `Show`, `Create` and `Edit` subcomponents instead of the all-in-one `Crud` component.
@@ -357,6 +369,8 @@ For more flexibility of customization, and especially if you want full control o
The `CrudProvider` component is optional, but it can be used to easily pass a single `dataSource` and `dataSourceCache` to the CRUD subcomponents inside it as context.
Alternatively, each of those components can take its own `dataSource` and `dataSourceCache` as props.
+Each component has a `pageTitle` prop that can be used to set a page title.
+
### `List` component
Displays a [Data Grid](https://mui.com/x/react-data-grid/) listing items from a data source, with support for pagination, sorting and filtering, along with some useful controls such as refreshing data.
diff --git a/docs/data/toolpad/core/introduction/Introduction.js b/docs/data/toolpad/core/introduction/Introduction.js
index d212663c398..b37327e68a7 100644
--- a/docs/data/toolpad/core/introduction/Introduction.js
+++ b/docs/data/toolpad/core/introduction/Introduction.js
@@ -230,6 +230,9 @@ function matchPath(pattern, pathname) {
}
function DemoPageContent({ pathname }) {
+ const showPersonId = matchPath('/people/:personId', pathname);
+ const editPersonId = matchPath('/people/:personId/edit', pathname);
+
if (pathname.includes('/people')) {
return (
);
}
return (
-
- Dashboard content for {pathname}
-
+
+
+ Dashboard content for {pathname}
+
+
);
}
@@ -269,22 +279,6 @@ function Introduction(props) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/people/new') {
- return 'New Person';
- }
- const editPersonId = matchPath('/people/:peopleId/edit', router.pathname);
- if (editPersonId) {
- return `Person ${editPersonId} - Edit`;
- }
- const showPersonId = matchPath('/people/:peopleId', router.pathname);
- if (showPersonId) {
- return `Person ${showPersonId}`;
- }
-
- return undefined;
- }, [router.pathname]);
-
return (
// Remove this provider when copying and pasting into your project.
@@ -296,9 +290,7 @@ function Introduction(props) {
window={demoWindow}
>
-
-
-
+
{/* preview-end */}
diff --git a/docs/data/toolpad/core/introduction/Introduction.tsx b/docs/data/toolpad/core/introduction/Introduction.tsx
index 4fea4d6d34a..ac1fa3ed0be 100644
--- a/docs/data/toolpad/core/introduction/Introduction.tsx
+++ b/docs/data/toolpad/core/introduction/Introduction.tsx
@@ -236,6 +236,9 @@ function matchPath(pattern: string, pathname: string): string | null {
}
function DemoPageContent({ pathname }: { pathname: string }) {
+ const showPersonId = matchPath('/people/:personId', pathname);
+ const editPersonId = matchPath('/people/:personId/edit', pathname);
+
if (pathname.includes('/people')) {
return (
@@ -244,22 +247,29 @@ function DemoPageContent({ pathname }: { pathname: string }) {
rootPath="/people"
initialPageSize={4}
defaultValues={{ age: 18 }}
+ pageTitles={{
+ create: 'New Person',
+ edit: `Person ${editPersonId} - Edit`,
+ show: `Person ${showPersonId}`,
+ }}
/>
);
}
return (
-
- Dashboard content for {pathname}
-
+
+
+ Dashboard content for {pathname}
+
+
);
}
@@ -279,22 +289,6 @@ export default function Introduction(props: DemoProps) {
// Remove this const when copying and pasting into your project.
const demoWindow = window !== undefined ? window() : undefined;
- const title = React.useMemo(() => {
- if (router.pathname === '/people/new') {
- return 'New Person';
- }
- const editPersonId = matchPath('/people/:peopleId/edit', router.pathname);
- if (editPersonId) {
- return `Person ${editPersonId} - Edit`;
- }
- const showPersonId = matchPath('/people/:peopleId', router.pathname);
- if (showPersonId) {
- return `Person ${showPersonId}`;
- }
-
- return undefined;
- }, [router.pathname]);
-
return (
// Remove this provider when copying and pasting into your project.
@@ -306,9 +300,7 @@ export default function Introduction(props: DemoProps) {
window={demoWindow}
>
-
-
-
+
{/* preview-end */}
diff --git a/docs/data/toolpad/core/introduction/Introduction.tsx.preview b/docs/data/toolpad/core/introduction/Introduction.tsx.preview
index 74ad39c2cf7..a13c4901487 100644
--- a/docs/data/toolpad/core/introduction/Introduction.tsx.preview
+++ b/docs/data/toolpad/core/introduction/Introduction.tsx.preview
@@ -5,8 +5,6 @@
window={demoWindow}
>
-
-
-
+
\ No newline at end of file
diff --git a/docs/pages/toolpad/core/api/create.json b/docs/pages/toolpad/core/api/create.json
index 7af3f41b5c8..12b26e7a088 100644
--- a/docs/pages/toolpad/core/api/create.json
+++ b/docs/pages/toolpad/core/api/create.json
@@ -10,18 +10,19 @@
"initialValues": { "type": { "name": "object" }, "default": "{}" },
"localeText": { "type": { "name": "object" } },
"onSubmitSuccess": { "type": { "name": "func" } },
+ "pageTitle": { "type": { "name": "string" } },
"resetOnSubmit": { "type": { "name": "bool" }, "default": "false" },
"slotProps": {
"type": {
"name": "shape",
- "description": "{ form?: { checkbox?: object, datePicker?: object, dateTimePicker?: object, select?: object, textField?: object } }"
+ "description": "{ form?: { checkbox?: object, datePicker?: object, dateTimePicker?: object, select?: object, textField?: object }, pageContainer?: object }"
},
"default": "{}"
},
"slots": {
"type": {
"name": "shape",
- "description": "{ form?: { checkbox?: elementType, datePicker?: elementType, dateTimePicker?: elementType, select?: elementType, textField?: elementType } }"
+ "description": "{ form?: { checkbox?: elementType, datePicker?: elementType, dateTimePicker?: elementType, select?: elementType, textField?: elementType }, pageContainer?: elementType }"
},
"default": "{}",
"additionalInfo": { "slotsApi": true }
diff --git a/docs/pages/toolpad/core/api/crud.json b/docs/pages/toolpad/core/api/crud.json
index 7506ff1523a..4bc0759f7ee 100644
--- a/docs/pages/toolpad/core/api/crud.json
+++ b/docs/pages/toolpad/core/api/crud.json
@@ -10,17 +10,23 @@
},
"defaultValues": { "type": { "name": "object" }, "default": "{}" },
"initialPageSize": { "type": { "name": "number" }, "default": "100" },
+ "pageTitles": {
+ "type": {
+ "name": "shape",
+ "description": "{ create?: string, edit?: string, list?: string, show?: string }"
+ }
+ },
"slotProps": {
"type": {
"name": "shape",
- "description": "{ form?: { checkbox?: object, datePicker?: object, dateTimePicker?: object, select?: object, textField?: object }, list?: { dataGrid?: object } }"
+ "description": "{ form?: { checkbox?: object, datePicker?: object, dateTimePicker?: object, select?: object, textField?: object }, list?: { dataGrid?: object, pageContainer?: object }, pageContainer?: object }"
},
"default": "{}"
},
"slots": {
"type": {
"name": "shape",
- "description": "{ form?: { checkbox?: elementType, datePicker?: elementType, dateTimePicker?: elementType, select?: elementType, textField?: elementType }, list?: { dataGrid?: func } }"
+ "description": "{ form?: { checkbox?: elementType, datePicker?: elementType, dateTimePicker?: elementType, select?: elementType, textField?: elementType }, list?: { dataGrid?: func, pageContainer?: elementType }, pageContainer?: elementType }"
},
"default": "{}",
"additionalInfo": { "slotsApi": true }
diff --git a/docs/pages/toolpad/core/api/edit.json b/docs/pages/toolpad/core/api/edit.json
index 384fbbe3f6c..4d0aef7b9ca 100644
--- a/docs/pages/toolpad/core/api/edit.json
+++ b/docs/pages/toolpad/core/api/edit.json
@@ -9,17 +9,18 @@
},
"localeText": { "type": { "name": "object" } },
"onSubmitSuccess": { "type": { "name": "func" } },
+ "pageTitle": { "type": { "name": "string" } },
"slotProps": {
"type": {
"name": "shape",
- "description": "{ form?: { checkbox?: object, datePicker?: object, dateTimePicker?: object, select?: object, textField?: object } }"
+ "description": "{ form?: { checkbox?: object, datePicker?: object, dateTimePicker?: object, select?: object, textField?: object }, pageContainer?: object }"
},
"default": "{}"
},
"slots": {
"type": {
"name": "shape",
- "description": "{ form?: { checkbox?: elementType, datePicker?: elementType, dateTimePicker?: elementType, select?: elementType, textField?: elementType } }"
+ "description": "{ form?: { checkbox?: elementType, datePicker?: elementType, dateTimePicker?: elementType, select?: elementType, textField?: elementType }, pageContainer?: elementType }"
},
"default": "{}",
"additionalInfo": { "slotsApi": true }
diff --git a/docs/pages/toolpad/core/api/list.json b/docs/pages/toolpad/core/api/list.json
index ad7d0a629c4..68c5f4d2d0b 100644
--- a/docs/pages/toolpad/core/api/list.json
+++ b/docs/pages/toolpad/core/api/list.json
@@ -13,12 +13,16 @@
"onDelete": { "type": { "name": "func" } },
"onEditClick": { "type": { "name": "func" } },
"onRowClick": { "type": { "name": "func" } },
+ "pageTitle": { "type": { "name": "string" } },
"slotProps": {
- "type": { "name": "shape", "description": "{ dataGrid?: object }" },
+ "type": { "name": "shape", "description": "{ dataGrid?: object, pageContainer?: object }" },
"default": "{}"
},
"slots": {
- "type": { "name": "shape", "description": "{ dataGrid?: func }" },
+ "type": {
+ "name": "shape",
+ "description": "{ dataGrid?: func, pageContainer?: elementType }"
+ },
"default": "{}",
"additionalInfo": { "slotsApi": true }
}
@@ -31,7 +35,8 @@
"description": "The DataGrid component used to list the items.",
"default": "DataGrid",
"class": null
- }
+ },
+ { "name": "pageContainer", "description": "", "class": null }
],
"classes": [],
"muiName": "List",
diff --git a/docs/pages/toolpad/core/api/show.json b/docs/pages/toolpad/core/api/show.json
index 52ffbf4b9e6..85f330e8210 100644
--- a/docs/pages/toolpad/core/api/show.json
+++ b/docs/pages/toolpad/core/api/show.json
@@ -9,7 +9,17 @@
},
"localeText": { "type": { "name": "object" } },
"onDelete": { "type": { "name": "func" } },
- "onEditClick": { "type": { "name": "func" } }
+ "onEditClick": { "type": { "name": "func" } },
+ "pageTitle": { "type": { "name": "string" } },
+ "slotProps": {
+ "type": { "name": "shape", "description": "{ pageContainer?: object }" },
+ "default": "{}"
+ },
+ "slots": {
+ "type": { "name": "shape", "description": "{ pageContainer?: elementType }" },
+ "default": "{}",
+ "additionalInfo": { "slotsApi": true }
+ }
},
"name": "Show",
"imports": ["import { Show } from '@toolpad/core/Crud';"],
diff --git a/docs/translations/api-docs/create/create.json b/docs/translations/api-docs/create/create.json
index 2f8e7398aff..247acea2e90 100644
--- a/docs/translations/api-docs/create/create.json
+++ b/docs/translations/api-docs/create/create.json
@@ -10,6 +10,7 @@
"initialValues": { "description": "Initial form values." },
"localeText": { "description": "Locale text for the component." },
"onSubmitSuccess": { "description": "Callback fired when the form is successfully submitted." },
+ "pageTitle": { "description": "The title of the page." },
"resetOnSubmit": {
"description": "Whether the form fields should reset after the form is submitted."
},
diff --git a/docs/translations/api-docs/crud/crud.json b/docs/translations/api-docs/crud/crud.json
index 2267cb879bd..9ba444c0491 100644
--- a/docs/translations/api-docs/crud/crud.json
+++ b/docs/translations/api-docs/crud/crud.json
@@ -9,6 +9,7 @@
},
"defaultValues": { "description": "Default form values for a new item." },
"initialPageSize": { "description": "Initial number of rows to show per page." },
+ "pageTitles": { "description": "The title of each CRUD page." },
"rootPath": { "description": "Root path to CRUD pages." },
"slotProps": { "description": "The props used for each slot inside." },
"slots": { "description": "The components used for each slot inside." }
diff --git a/docs/translations/api-docs/edit/edit.json b/docs/translations/api-docs/edit/edit.json
index bff383090f5..587dfd97c01 100644
--- a/docs/translations/api-docs/edit/edit.json
+++ b/docs/translations/api-docs/edit/edit.json
@@ -9,6 +9,7 @@
},
"localeText": { "description": "Locale text for the component." },
"onSubmitSuccess": { "description": "Callback fired when the form is successfully submitted." },
+ "pageTitle": { "description": "The title of the page." },
"slotProps": { "description": "The props used for each slot inside." },
"slots": { "description": "The components used for each slot inside." }
},
diff --git a/docs/translations/api-docs/list/list.json b/docs/translations/api-docs/list/list.json
index 17628533bd7..ea698151c54 100644
--- a/docs/translations/api-docs/list/list.json
+++ b/docs/translations/api-docs/list/list.json
@@ -17,9 +17,13 @@
"onRowClick": {
"description": "Callback fired when a row is clicked. Not called if the target clicked is an interactive element added by the built-in columns."
},
+ "pageTitle": { "description": "The title of the page." },
"slotProps": { "description": "The props used for each slot inside." },
"slots": { "description": "The components used for each slot inside." }
},
"classDescriptions": {},
- "slotDescriptions": { "dataGrid": "The DataGrid component used to list the items." }
+ "slotDescriptions": {
+ "dataGrid": "The DataGrid component used to list the items.",
+ "pageContainer": ""
+ }
}
diff --git a/docs/translations/api-docs/show/show.json b/docs/translations/api-docs/show/show.json
index 142ce4e8c39..7d74903eb65 100644
--- a/docs/translations/api-docs/show/show.json
+++ b/docs/translations/api-docs/show/show.json
@@ -9,7 +9,10 @@
},
"localeText": { "description": "Locale text for the component." },
"onDelete": { "description": "Callback fired when the item is successfully deleted." },
- "onEditClick": { "description": "Callback fired when the "Edit" button is clicked." }
+ "onEditClick": { "description": "Callback fired when the "Edit" button is clicked." },
+ "pageTitle": { "description": "The title of the page." },
+ "slotProps": { "description": "The props used for each slot inside." },
+ "slots": { "description": "The components used for each slot inside." }
},
"classDescriptions": {}
}
diff --git a/packages/create-toolpad-app/src/templates/employeesPage.ts b/packages/create-toolpad-app/src/templates/employeesPage.ts
index 5df60beee90..e6d13635d7e 100644
--- a/packages/create-toolpad-app/src/templates/employeesPage.ts
+++ b/packages/create-toolpad-app/src/templates/employeesPage.ts
@@ -3,9 +3,12 @@ import { Template } from '../types';
const ordersPage: Template = (options) => {
const authEnabled = options.auth;
const routerType = options.router;
+ const framework = options.framework;
- let imports = `import * as React from 'react';
-${routerType === 'nextjs-pages' ? `import { useRouter } from 'next/router';` : ``}
+ let imports = `${routerType === 'nextjs-app' ? 'use client;' : ''}import * as React from 'react';
+${routerType === 'nextjs-pages' ? `import { useRouter } from 'next/router';` : ''}
+${routerType === 'nextjs-app' ? `import { useParams } from 'next/navigation';` : ''}
+${framework === 'vite' ? `import { useParams } from 'react-router';` : ''}
import { Crud } from '@toolpad/core/Crud';
import { employeesDataSource, Employee, employeesCache } from '${routerType === 'nextjs-app' ? `../../../` : ``}${routerType === 'nextjs-pages' ? `../` : ``}../data/employees';`;
@@ -40,17 +43,32 @@ import { employeesDataSource, Employee, employeesCache } from '${routerType ===
export default ${isAsync}function EmployeesCrudPage() {
- ${routerType === 'nextjs-pages' ? `const router = useRouter();\n` : ``}${sessionHandling}
+ ${
+ routerType === 'nextjs-pages'
+ ? `const router = useRouter();
+ const { segments = [] } = router.query;
+ const [employeeId] = segments;`
+ : ''
+ }
+ ${routerType === 'nextjs-app' ? 'const { employeeId } = useParams();' : ''}
+ ${framework === 'vite' ? 'const { employeeId } = useParams();' : ''}
+
+ ${sessionHandling}
- return ${routerType === 'nextjs-pages' ? `router.isReady ? ` : ``}(
+ return ${routerType === 'nextjs-pages' ? `router.isReady ? ` : ''}(
dataSource={employeesDataSource}
dataSourceCache={employeesCache}
rootPath="/employees"
initialPageSize={25}
defaultValues={{ itemCount: 1 }}
+ pageTitles={{
+ show: \`Employee \${employeeId}\`,
+ create: 'New Employee',
+ edit: \`Employee \${employeeId} - Edit\`,
+ }}
/>
- )${routerType === 'nextjs-pages' ? ` : null` : ``};
+ )${routerType === 'nextjs-pages' ? ` : null` : ''};
}${requireAuth}
`;
};
diff --git a/packages/create-toolpad-app/src/templates/indexPage.ts b/packages/create-toolpad-app/src/templates/indexPage.ts
index 5576feda920..7ca83ecce4b 100644
--- a/packages/create-toolpad-app/src/templates/indexPage.ts
+++ b/packages/create-toolpad-app/src/templates/indexPage.ts
@@ -5,7 +5,8 @@ const indexPage: Template = (options) => {
const routerType = options.router;
let imports = `import * as React from 'react';
-import Typography from '@mui/material/Typography';`;
+import Typography from '@mui/material/Typography';
+import { PageContainer } from '@toolpad/core/PageContainer';`;
let sessionHandling = '';
@@ -51,7 +52,9 @@ export default ${isAsync}function HomePage() {
return (
- ${welcomeMessage}
+
+ ${welcomeMessage}
+
);
}${requireAuth}
diff --git a/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/dashboardLayout.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/dashboardLayout.ts
index 30685b2ee10..aba8a288229 100644
--- a/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/dashboardLayout.ts
+++ b/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/dashboardLayout.ts
@@ -1,30 +1,10 @@
-const dashboardLayout = `'use client';
-import * as React from 'react';
-import { usePathname, useParams } from 'next/navigation';
+const dashboardLayout = `import * as React from 'react';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
export default function Layout(props: { children: React.ReactNode }) {
-const pathname = usePathname();
- const params = useParams();
- const [employeeId] = params.segments ?? [];
-
- const title = React.useMemo(() => {
- if (pathname === '/employees/new') {
- return 'New Employee';
- }
- if (employeeId && pathname.includes('/edit')) {
- return \`Employee \${employeeId} - Edit\`;
- }
- if (employeeId) {
- return \`Employee \${employeeId}\`;
- }
- return undefined;
- }, [employeeId, pathname]);
-
return (
- {props.children}
+ {props.children}
);
}
diff --git a/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts
index 37b1313d72e..42821667c31 100644
--- a/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts
+++ b/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts
@@ -8,7 +8,6 @@ import { NextAppProvider } from '@toolpad/core/nextjs';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
import { PageContainer } from '@toolpad/core/PageContainer';
import Head from 'next/head';
-import { useRouter } from 'next/router';
import { AppCacheProvider } from '@mui/material-nextjs/v14-pagesRouter';
import DashboardIcon from '@mui/icons-material/Dashboard';
import PersonIcon from '@mui/icons-material/Person';
@@ -92,26 +91,9 @@ function RequireAuth({ children }: { children: React.ReactNode }) {
}
function DefaultLayout({ page }: { page: React.ReactElement }) {
- const router = useRouter();
- const { segments = [] } = router.query;
- const [employeeId] = segments;
-
- const title = React.useMemo(() => {
- if (router.asPath.split('?')[0] === '/employees/new') {
- return 'New Employee';
- }
- if (employeeId && router.asPath.includes('/edit')) {
- return \`Employee \${employeeId} - Edit\`;
- }
- if (employeeId) {
- return \`Employee \${employeeId}\`;
- }
- return undefined;
- }, [employeeId, router.asPath]);
-
return (
- {page}
+ {page}
);
}
diff --git a/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts b/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts
index e726dddec55..0f34b3b4d6d 100644
--- a/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts
+++ b/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts
@@ -9,7 +9,7 @@ const dashboardTemplate: Template = (options) => {
`
: ``
}
-import { Outlet, useLocation, useParams, matchPath } from 'react-router';
+import { Outlet } from 'react-router';
import { DashboardLayout${auth ? ', ThemeSwitcher' : ''} } from '@toolpad/core/DashboardLayout';
import { PageContainer } from '@toolpad/core/PageContainer';
${
@@ -34,22 +34,6 @@ function CustomActions() {
}
export default function Layout() {
- const location = useLocation();
- const { employeeId } = useParams();
-
- const title = React.useMemo(() => {
- if (location.pathname === '/employees/new') {
- return 'New Employee';
- }
- if (matchPath('/employees/:employeeId/edit', location.pathname)) {
- return \`Employee \${employeeId} - Edit\`;
- }
- if (employeeId) {
- return \`Employee \${employeeId}\`;
- }
- return undefined;
- }, [location.pathname, employeeId]);
-
${
auth
? `const { session, loading } = useSession();
@@ -72,9 +56,7 @@ export default function Layout() {
return (
-
-
-
+
);
}`;
diff --git a/packages/toolpad-core/src/Crud/Create.tsx b/packages/toolpad-core/src/Crud/Create.tsx
index 439568d124a..8516b3d9961 100644
--- a/packages/toolpad-core/src/Crud/Create.tsx
+++ b/packages/toolpad-core/src/Crud/Create.tsx
@@ -10,6 +10,8 @@ import { DataSourceCache } from './cache';
import { useCachedDataSource } from './useCachedDataSource';
import { CRUD_DEFAULT_LOCALE_TEXT, type CRUDLocaleText } from './localeText';
import type { DataFieldFormValue, DataModel, DataSource, OmitId } from './types';
+import { PageContainer, type PageContainerProps } from '../PageContainer';
+import { useActivePage } from '../useActivePage';
export interface CreateProps {
/**
@@ -34,6 +36,10 @@ export interface CreateProps {
* [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source.
*/
dataSourceCache?: DataSourceCache | null;
+ /**
+ * The title of the page.
+ */
+ pageTitle?: string;
/**
* Locale text for the component.
*/
@@ -44,6 +50,7 @@ export interface CreateProps {
*/
slots?: {
form?: CrudFormSlots;
+ pageContainer?: React.JSXElementConstructor;
};
/**
* The props used for each slot inside.
@@ -51,6 +58,7 @@ export interface CreateProps {
*/
slotProps?: {
form?: CrudFormSlotProps;
+ pageContainer?: PageContainerProps;
};
}
@@ -70,6 +78,7 @@ function Create(props: CreateProps) {
onSubmitSuccess,
resetOnSubmit = false,
dataSourceCache,
+ pageTitle,
localeText: propsLocaleText,
slots,
slotProps,
@@ -97,6 +106,8 @@ function Create(props: CreateProps) {
const { fields, createOne, validate } = cachedDataSource;
+ const activePage = useActivePage();
+
const [formState, setFormState] = React.useState<{
values: Partial>;
errors: Partial>;
@@ -199,17 +210,34 @@ function Create(props: CreateProps) {
validate,
]);
+ const PageContainerSlot = slots?.pageContainer ?? PageContainer;
+
return (
-
+
+
+
);
}
@@ -245,6 +273,10 @@ Create.propTypes /* remove-proptypes */ = {
* Callback fired when the form is successfully submitted.
*/
onSubmitSuccess: PropTypes.func,
+ /**
+ * The title of the page.
+ */
+ pageTitle: PropTypes.string,
/**
* Whether the form fields should reset after the form is submitted.
* @default false
@@ -262,6 +294,7 @@ Create.propTypes /* remove-proptypes */ = {
select: PropTypes.object,
textField: PropTypes.object,
}),
+ pageContainer: PropTypes.object,
}),
/**
* The components used for each slot inside.
@@ -275,6 +308,7 @@ Create.propTypes /* remove-proptypes */ = {
select: PropTypes.elementType,
textField: PropTypes.elementType,
}),
+ pageContainer: PropTypes.elementType,
}),
} as any;
diff --git a/packages/toolpad-core/src/Crud/Crud.tsx b/packages/toolpad-core/src/Crud/Crud.tsx
index 9d4a79907d4..4b562993451 100644
--- a/packages/toolpad-core/src/Crud/Crud.tsx
+++ b/packages/toolpad-core/src/Crud/Crud.tsx
@@ -11,7 +11,8 @@ import { Create } from './Create';
import { Edit } from './Edit';
import { DataSourceCache } from './cache';
import type { DataModel, DataModelId, DataSource, OmitId } from './types';
-import { CrudFormSlotProps, CrudFormSlots } from './CrudForm';
+import type { CrudFormSlotProps, CrudFormSlots } from './CrudForm';
+import { type PageContainerProps } from '../PageContainer';
export interface CrudProps {
/**
@@ -36,6 +37,15 @@ export interface CrudProps {
* [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source.
*/
dataSourceCache?: DataSourceCache | null;
+ /**
+ * The title of each CRUD page.
+ */
+ pageTitles?: {
+ list?: string;
+ show?: string;
+ create?: string;
+ edit?: string;
+ };
/**
* The components used for each slot inside.
* @default {}
@@ -43,6 +53,7 @@ export interface CrudProps {
slots?: {
list?: ListSlots;
form?: CrudFormSlots;
+ pageContainer?: React.JSXElementConstructor;
};
/**
* The props used for each slot inside.
@@ -51,6 +62,7 @@ export interface CrudProps {
slotProps?: {
list?: ListSlotProps;
form?: CrudFormSlotProps;
+ pageContainer?: PageContainerProps;
};
}
/**
@@ -70,6 +82,7 @@ function Crud(props: CrudProps) {
initialPageSize,
defaultValues,
dataSourceCache,
+ pageTitles,
slots,
slotProps,
} = props;
@@ -121,8 +134,23 @@ function Crud(props: CrudProps) {
onRowClick={handleRowClick}
onCreateClick={handleCreateClick}
onEditClick={handleEditClick}
- slots={slots?.list}
- slotProps={slotProps?.list}
+ pageTitle={pageTitles?.list}
+ slots={{
+ ...(slots?.pageContainer
+ ? {
+ pageContainer: slots?.pageContainer,
+ }
+ : {}),
+ ...slots?.list,
+ }}
+ slotProps={{
+ ...(slotProps?.pageContainer
+ ? {
+ pageContainer: slotProps?.pageContainer,
+ }
+ : {}),
+ ...slotProps?.list,
+ }}
/>
);
}
@@ -132,16 +160,31 @@ function Crud(props: CrudProps) {
initialValues={defaultValues}
onSubmitSuccess={handleCreate}
resetOnSubmit={false}
- slots={
- slots?.form && {
- form: slots?.form,
- }
- }
- slotProps={
- slotProps?.form && {
- form: slotProps?.form,
- }
- }
+ pageTitle={pageTitles?.create}
+ slots={{
+ ...(slots?.form
+ ? {
+ form: slots?.form,
+ }
+ : {}),
+ ...(slots?.pageContainer
+ ? {
+ pageContainer: slots?.pageContainer,
+ }
+ : {}),
+ }}
+ slotProps={{
+ ...(slotProps?.form
+ ? {
+ form: slotProps?.form,
+ }
+ : {}),
+ ...(slotProps?.pageContainer
+ ? {
+ pageContainer: slotProps?.pageContainer,
+ }
+ : {}),
+ }}
/>
);
}
@@ -149,7 +192,28 @@ function Crud(props: CrudProps) {
if (showMatch) {
const resourceId = showMatch.params.id;
invariant(resourceId, 'No resource ID present in URL.');
- return id={resourceId} onEditClick={handleEditClick} onDelete={handleDelete} />;
+ return (
+
+ id={resourceId}
+ onEditClick={handleEditClick}
+ onDelete={handleDelete}
+ pageTitle={pageTitles?.show}
+ slots={{
+ ...(slots?.pageContainer
+ ? {
+ pageContainer: slots?.pageContainer,
+ }
+ : {}),
+ }}
+ slotProps={{
+ ...(slotProps?.pageContainer
+ ? {
+ pageContainer: slotProps?.pageContainer,
+ }
+ : {}),
+ }}
+ />
+ );
}
const editMatch = match<{ id: DataModelId }>(editPath)(pathname);
if (editMatch) {
@@ -159,16 +223,31 @@ function Crud(props: CrudProps) {
id={resourceId}
onSubmitSuccess={handleEdit}
- slots={
- slots?.form && {
- form: slots?.form,
- }
- }
- slotProps={
- slotProps?.form && {
- form: slotProps?.form,
- }
- }
+ pageTitle={pageTitles?.edit}
+ slots={{
+ ...(slots?.form
+ ? {
+ form: slots?.form,
+ }
+ : {}),
+ ...(slots?.pageContainer
+ ? {
+ pageContainer: slots?.pageContainer,
+ }
+ : {}),
+ }}
+ slotProps={{
+ ...(slotProps?.form
+ ? {
+ form: slotProps?.form,
+ }
+ : {}),
+ ...(slotProps?.pageContainer
+ ? {
+ pageContainer: slotProps?.pageContainer,
+ }
+ : {}),
+ }}
/>
);
}
@@ -185,6 +264,7 @@ function Crud(props: CrudProps) {
handleRowClick,
initialPageSize,
listPath,
+ pageTitles,
routerContext?.pathname,
showPath,
slotProps,
@@ -227,6 +307,15 @@ Crud.propTypes /* remove-proptypes */ = {
* @default 100
*/
initialPageSize: PropTypes.number,
+ /**
+ * The title of each CRUD page.
+ */
+ pageTitles: PropTypes.shape({
+ create: PropTypes.string,
+ edit: PropTypes.string,
+ list: PropTypes.string,
+ show: PropTypes.string,
+ }),
/**
* Root path to CRUD pages.
*/
@@ -245,7 +334,9 @@ Crud.propTypes /* remove-proptypes */ = {
}),
list: PropTypes.shape({
dataGrid: PropTypes.object,
+ pageContainer: PropTypes.object,
}),
+ pageContainer: PropTypes.object,
}),
/**
* The components used for each slot inside.
@@ -261,7 +352,9 @@ Crud.propTypes /* remove-proptypes */ = {
}),
list: PropTypes.shape({
dataGrid: PropTypes.func,
+ pageContainer: PropTypes.elementType,
}),
+ pageContainer: PropTypes.elementType,
}),
} as any;
diff --git a/packages/toolpad-core/src/Crud/Edit.tsx b/packages/toolpad-core/src/Crud/Edit.tsx
index 7d16f689aba..7287340c42d 100644
--- a/packages/toolpad-core/src/Crud/Edit.tsx
+++ b/packages/toolpad-core/src/Crud/Edit.tsx
@@ -13,6 +13,8 @@ import { DataSourceCache } from './cache';
import { useCachedDataSource } from './useCachedDataSource';
import { CRUD_DEFAULT_LOCALE_TEXT, type CRUDLocaleText } from './localeText';
import type { DataFieldFormValue, DataModel, DataModelId, DataSource, OmitId } from './types';
+import { PageContainer, type PageContainerProps } from '../PageContainer';
+import { useActivePage } from '../useActivePage';
interface EditFormProps {
dataSource: DataSource & Required, 'getOne' | 'updateOne'>>;
@@ -189,6 +191,10 @@ export interface EditProps {
* [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source.
*/
dataSourceCache?: DataSourceCache | null;
+ /**
+ * The title of the page.
+ */
+ pageTitle?: string;
/**
* Locale text for the component.
*/
@@ -199,6 +205,7 @@ export interface EditProps {
*/
slots?: {
form?: CrudFormSlots;
+ pageContainer?: React.JSXElementConstructor;
};
/**
* The props used for each slot inside.
@@ -206,6 +213,7 @@ export interface EditProps {
*/
slotProps?: {
form?: CrudFormSlotProps;
+ pageContainer?: PageContainerProps;
};
}
@@ -224,6 +232,7 @@ function Edit(props: EditProps) {
id,
onSubmitSuccess,
dataSourceCache,
+ pageTitle,
localeText: propsLocaleText,
slots,
slotProps,
@@ -249,6 +258,8 @@ function Edit(props: EditProps) {
const { fields, validate, ...methods } = cachedDataSource;
const { getOne, updateOne } = methods;
+ const activePage = useActivePage();
+
const [data, setData] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState(null);
@@ -329,7 +340,26 @@ function Edit(props: EditProps) {
slots,
]);
- return {renderEdit};
+ const PageContainerSlot = slots?.pageContainer ?? PageContainer;
+
+ return (
+
+ {renderEdit}
+
+ );
}
Edit.propTypes /* remove-proptypes */ = {
@@ -363,6 +393,10 @@ Edit.propTypes /* remove-proptypes */ = {
* Callback fired when the form is successfully submitted.
*/
onSubmitSuccess: PropTypes.func,
+ /**
+ * The title of the page.
+ */
+ pageTitle: PropTypes.string,
/**
* The props used for each slot inside.
* @default {}
@@ -375,6 +409,7 @@ Edit.propTypes /* remove-proptypes */ = {
select: PropTypes.object,
textField: PropTypes.object,
}),
+ pageContainer: PropTypes.object,
}),
/**
* The components used for each slot inside.
@@ -388,6 +423,7 @@ Edit.propTypes /* remove-proptypes */ = {
select: PropTypes.elementType,
textField: PropTypes.elementType,
}),
+ pageContainer: PropTypes.elementType,
}),
} as any;
diff --git a/packages/toolpad-core/src/Crud/List.tsx b/packages/toolpad-core/src/Crud/List.tsx
index eac1a42c6d3..f3838e55b18 100644
--- a/packages/toolpad-core/src/Crud/List.tsx
+++ b/packages/toolpad-core/src/Crud/List.tsx
@@ -36,6 +36,8 @@ import { DataSourceCache } from './cache';
import { useCachedDataSource } from './useCachedDataSource';
import type { DataModel, DataModelId, DataSource } from './types';
import { CRUD_DEFAULT_LOCALE_TEXT, type CRUDLocaleText } from './localeText';
+import { PageContainer, type PageContainerProps } from '../PageContainer';
+import { useActivePage } from '../useActivePage';
const ErrorOverlay = styled('div')(({ theme }) => ({
position: 'absolute',
@@ -54,6 +56,7 @@ const ErrorOverlay = styled('div')(({ theme }) => ({
export interface ListSlotProps {
dataGrid?: Partial;
+ pageContainer?: PageContainerProps;
}
export interface ListSlots {
@@ -65,6 +68,7 @@ export interface ListSlots {
| React.JSXElementConstructor
| React.JSXElementConstructor
| React.JSXElementConstructor;
+ pageContainer?: React.JSXElementConstructor;
}
export interface ListProps {
@@ -97,6 +101,10 @@ export interface ListProps {
* [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source.
*/
dataSourceCache?: DataSourceCache | null;
+ /**
+ * The title of the page.
+ */
+ pageTitle?: string;
/**
* The components used for each slot inside.
* @default {}
@@ -131,6 +139,7 @@ function List(props: ListProps) {
onEditClick,
onDelete,
dataSourceCache,
+ pageTitle,
slots,
slotProps,
localeText: propsLocaleText,
@@ -159,6 +168,8 @@ function List(props: ListProps) {
const routerContext = React.useContext(RouterContext);
+ const activePage = useActivePage();
+
const dialogs = useDialogs();
const notifications = useNotifications();
@@ -357,6 +368,7 @@ function List(props: ListProps) {
);
const DataGridSlot = slots?.dataGrid ?? DataGrid;
+ const PageContainerSlot = slots?.pageContainer ?? PageContainer;
const initialState = React.useMemo(
() => ({
@@ -366,6 +378,12 @@ function List(props: ListProps) {
);
const columns = React.useMemo(() => {
+ const pinnedColumnsOverride = (slotProps?.dataGrid as DataGridProProps | DataGridPremiumProps)
+ ?.initialState?.pinnedColumns;
+ const isActionsColumnPinned =
+ pinnedColumnsOverride?.left?.includes('actions') ||
+ pinnedColumnsOverride?.right?.includes('actions');
+
return [
...fields.map((field) => ({
...field,
@@ -374,7 +392,7 @@ function List(props: ListProps) {
{
field: 'actions',
type: 'actions',
- flex: 1,
+ flex: isActionsColumnPinned ? undefined : 1,
align: 'right',
getActions: ({ id }) => [
...(onEditClick
@@ -408,74 +426,90 @@ function List(props: ListProps) {
localeText.deleteLabel,
localeText.editLabel,
onEditClick,
+ slotProps?.dataGrid,
]);
return (
-
-
-
-
-
-
-
-
-
- {onCreateClick ? (
- }>
- {localeText.createNewButtonLabel}
-
- ) : null}
-
-
- {/* Use NoSsr to prevent issue https://github.com/mui/mui-x/issues/17077 as DataGrid has no SSR support */}
-
- )}
- sx={{
- [`& .${gridClasses.columnHeader}, & .${gridClasses.cell}`]: {
- outline: 'transparent',
+
+
+
+
+
+
+
+
+
+
+ {onCreateClick ? (
+ }>
+ {localeText.createNewButtonLabel}
+
+ ) : null}
+
+
+ {/* Use NoSsr to prevent issue https://github.com/mui/mui-x/issues/17077 as DataGrid has no SSR support */}
+
+ )}
+ sx={{
+ [`& .${gridClasses.columnHeader}, & .${gridClasses.cell}`]: {
+ outline: 'transparent',
},
- ...(onRowClick
- ? {
- [`& .${gridClasses.row}:hover`]: {
- cursor: 'pointer',
- },
- }
- : {}),
- ...slotProps?.dataGrid?.sx,
- }}
- />
-
- {error && (
-
- {error.message}
-
- )}
-
-
+ [`& .${gridClasses.columnHeader}:focus-within, & .${gridClasses.cell}:focus-within`]:
+ {
+ outline: 'none',
+ },
+ ...(onRowClick
+ ? {
+ [`& .${gridClasses.row}:hover`]: {
+ cursor: 'pointer',
+ },
+ }
+ : {}),
+ ...slotProps?.dataGrid?.sx,
+ }}
+ />
+
+ {error && (
+
+ {error.message}
+
+ )}
+
+
+
);
}
@@ -523,12 +557,17 @@ List.propTypes /* remove-proptypes */ = {
* Callback fired when a row is clicked. Not called if the target clicked is an interactive element added by the built-in columns.
*/
onRowClick: PropTypes.func,
+ /**
+ * The title of the page.
+ */
+ pageTitle: PropTypes.string,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
dataGrid: PropTypes.object,
+ pageContainer: PropTypes.object,
}),
/**
* The components used for each slot inside.
@@ -536,6 +575,7 @@ List.propTypes /* remove-proptypes */ = {
*/
slots: PropTypes.shape({
dataGrid: PropTypes.func,
+ pageContainer: PropTypes.elementType,
}),
} as any;
diff --git a/packages/toolpad-core/src/Crud/Show.tsx b/packages/toolpad-core/src/Crud/Show.tsx
index 0e8cae05917..c8bd33b84ad 100644
--- a/packages/toolpad-core/src/Crud/Show.tsx
+++ b/packages/toolpad-core/src/Crud/Show.tsx
@@ -23,6 +23,8 @@ import { DataSourceCache } from './cache';
import { useCachedDataSource } from './useCachedDataSource';
import type { DataField, DataModel, DataModelId, DataSource } from './types';
import { CRUD_DEFAULT_LOCALE_TEXT, type CRUDLocaleText } from './localeText';
+import { PageContainer, type PageContainerProps } from '../PageContainer';
+import { useActivePage } from '../useActivePage';
export interface ShowProps {
id: DataModelId;
@@ -42,10 +44,28 @@ export interface ShowProps {
* [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source.
*/
dataSourceCache?: DataSourceCache | null;
+ /**
+ * The title of the page.
+ */
+ pageTitle?: string;
/**
* Locale text for the component.
*/
localeText?: CRUDLocaleText;
+ /**
+ * The components used for each slot inside.
+ * @default {}
+ */
+ slots?: {
+ pageContainer?: React.JSXElementConstructor;
+ };
+ /**
+ * The props used for each slot inside.
+ * @default {}
+ */
+ slotProps?: {
+ pageContainer?: PageContainerProps;
+ };
}
/**
@@ -59,7 +79,16 @@ export interface ShowProps {
* - [Show API](https://mui.com/toolpad/core/api/show)
*/
function Show(props: ShowProps) {
- const { id, onEditClick, onDelete, dataSourceCache, localeText: propsLocaleText } = props;
+ const {
+ id,
+ onEditClick,
+ onDelete,
+ dataSourceCache,
+ pageTitle,
+ localeText: propsLocaleText,
+ slots,
+ slotProps,
+ } = props;
const globalLocaleText = useLocaleText();
const localeText = { ...CRUD_DEFAULT_LOCALE_TEXT, ...globalLocaleText, ...propsLocaleText };
@@ -82,6 +111,8 @@ function Show(props: ShowProps) {
const { fields, validate, ...methods } = cachedDataSource;
const { getOne, deleteOne } = methods;
+ const activePage = useActivePage();
+
const dialogs = useDialogs();
const notifications = useNotifications();
@@ -309,7 +340,26 @@ function Show(props: ShowProps) {
renderField,
]);
- return {renderShow};
+ const PageContainerSlot = slots?.pageContainer ?? PageContainer;
+
+ return (
+
+ {renderShow}
+
+ );
}
Show.propTypes /* remove-proptypes */ = {
@@ -347,6 +397,24 @@ Show.propTypes /* remove-proptypes */ = {
* Callback fired when the "Edit" button is clicked.
*/
onEditClick: PropTypes.func,
+ /**
+ * The title of the page.
+ */
+ pageTitle: PropTypes.string,
+ /**
+ * The props used for each slot inside.
+ * @default {}
+ */
+ slotProps: PropTypes.shape({
+ pageContainer: PropTypes.object,
+ }),
+ /**
+ * The components used for each slot inside.
+ * @default {}
+ */
+ slots: PropTypes.shape({
+ pageContainer: PropTypes.elementType,
+ }),
} as any;
export { Show };
diff --git a/playground/nextjs-pages/src/pages/_app.tsx b/playground/nextjs-pages/src/pages/_app.tsx
index 6ee56f6040b..7678e70294c 100644
--- a/playground/nextjs-pages/src/pages/_app.tsx
+++ b/playground/nextjs-pages/src/pages/_app.tsx
@@ -1,7 +1,5 @@
import * as React from 'react';
-import { useRouter } from 'next/router';
import { NextAppProvider } from '@toolpad/core/nextjs';
-import { PageContainer } from '@toolpad/core/PageContainer';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
import Head from 'next/head';
import { AppCacheProvider } from '@mui/material-nextjs/v14-pagesRouter';
@@ -49,28 +47,7 @@ const AUTHENTICATION = {
};
function DefaultLayout({ page }: { page: React.ReactElement }) {
- const router = useRouter();
- const { segments = [] } = router.query;
- const [orderId] = segments;
-
- const title = React.useMemo(() => {
- if (router.asPath.split('?')[0] === '/orders/new') {
- return 'New Order';
- }
- if (orderId && router.asPath.includes('/edit')) {
- return `Order ${orderId} - Edit`;
- }
- if (orderId) {
- return `Order ${orderId}`;
- }
- return undefined;
- }, [orderId, router.asPath]);
-
- return (
-
- {page}
-
- );
+ return {page};
}
function getDefaultLayout(page: React.ReactElement) {
diff --git a/playground/nextjs-pages/src/pages/index.tsx b/playground/nextjs-pages/src/pages/index.tsx
index a23c1d4f688..7c0817c0f6e 100644
--- a/playground/nextjs-pages/src/pages/index.tsx
+++ b/playground/nextjs-pages/src/pages/index.tsx
@@ -1,11 +1,16 @@
import * as React from 'react';
import Typography from '@mui/material/Typography';
+import { PageContainer } from '@toolpad/core/PageContainer';
import { useSession } from 'next-auth/react';
export default function HomePage() {
const { data: session } = useSession();
- return Welcome to Toolpad, {session?.user?.name || 'User'}!;
+ return (
+
+ Welcome to Toolpad, {session?.user?.name || 'User'}!
+
+ );
}
HomePage.requireAuth = true;
diff --git a/playground/nextjs-pages/src/pages/orders/[[...segments]].tsx b/playground/nextjs-pages/src/pages/orders/[[...segments]].tsx
index fa63ee9d965..d5090b4542e 100644
--- a/playground/nextjs-pages/src/pages/orders/[[...segments]].tsx
+++ b/playground/nextjs-pages/src/pages/orders/[[...segments]].tsx
@@ -5,6 +5,8 @@ import { ordersDataSource, Order, ordersCache } from '../../data/orders';
export default function OrdersCrudPage() {
const router = useRouter();
+ const { segments = [] } = router.query;
+ const [orderId] = segments;
return router.isReady ? (
@@ -13,6 +15,11 @@ export default function OrdersCrudPage() {
rootPath="/orders"
initialPageSize={25}
defaultValues={{ itemCount: 1 }}
+ pageTitles={{
+ show: `Order ${orderId}`,
+ create: 'New Order',
+ edit: `Order ${orderId} - Edit`,
+ }}
/>
) : null;
}
diff --git a/playground/nextjs/src/app/(dashboard)/layout.tsx b/playground/nextjs/src/app/(dashboard)/layout.tsx
index 493cff95f43..be011cf6ea0 100644
--- a/playground/nextjs/src/app/(dashboard)/layout.tsx
+++ b/playground/nextjs/src/app/(dashboard)/layout.tsx
@@ -1,6 +1,5 @@
'use client';
import * as React from 'react';
-import { usePathname, useParams } from 'next/navigation';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import MenuList from '@mui/material/MenuList';
@@ -17,7 +16,6 @@ import {
AccountPreviewProps,
} from '@toolpad/core/Account';
import { DashboardLayout, SidebarFooterProps, ThemeSwitcher } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
function ToolbarActions() {
return (
@@ -166,23 +164,6 @@ function SidebarFooterAccount({ mini }: SidebarFooterProps) {
}
export default function DashboardPagesLayout(props: { children: React.ReactNode }) {
- const pathname = usePathname();
- const params = useParams();
- const [orderId] = params.segments ?? [];
-
- const title = React.useMemo(() => {
- if (pathname === '/orders/new') {
- return 'New Order';
- }
- if (orderId && pathname.includes('/edit')) {
- return `Order ${orderId} - Edit`;
- }
- if (orderId) {
- return `Order ${orderId}`;
- }
- return undefined;
- }, [orderId, pathname]);
-
return (
- {props.children}
+ {props.children}
);
}
diff --git a/playground/nextjs/src/app/(dashboard)/orders/[[...segments]]/page.tsx b/playground/nextjs/src/app/(dashboard)/orders/[[...segments]]/page.tsx
index feaf48f212e..473395d562a 100644
--- a/playground/nextjs/src/app/(dashboard)/orders/[[...segments]]/page.tsx
+++ b/playground/nextjs/src/app/(dashboard)/orders/[[...segments]]/page.tsx
@@ -1,8 +1,13 @@
+'use client';
import * as React from 'react';
import { Crud } from '@toolpad/core/Crud';
+import { useParams } from 'next/navigation';
import { ordersDataSource, Order, ordersCache } from '../../../../data/orders';
export default function OrdersCrudPage() {
+ const params = useParams();
+ const [orderId] = params.segments ?? [];
+
return (
dataSource={ordersDataSource}
@@ -10,6 +15,11 @@ export default function OrdersCrudPage() {
rootPath="/orders"
initialPageSize={25}
defaultValues={{ itemCount: 1 }}
+ pageTitles={{
+ show: `Order ${orderId}`,
+ create: 'New Order',
+ edit: `Order ${orderId} - Edit`,
+ }}
/>
);
}
diff --git a/playground/nextjs/src/app/(dashboard)/page.tsx b/playground/nextjs/src/app/(dashboard)/page.tsx
index 9fc6ddc433b..f03a822f494 100644
--- a/playground/nextjs/src/app/(dashboard)/page.tsx
+++ b/playground/nextjs/src/app/(dashboard)/page.tsx
@@ -1,9 +1,14 @@
import * as React from 'react';
import Typography from '@mui/material/Typography';
+import { PageContainer } from '@toolpad/core/PageContainer';
import { auth } from '../../auth';
export default async function HomePage() {
const session = await auth();
- return Welcome to Toolpad, {session?.user?.name || 'User'}!;
+ return (
+
+ Welcome to Toolpad, {session?.user?.name || 'User'}!
+
+ );
}
diff --git a/playground/vite-react-router/src/layouts/dashboard.tsx b/playground/vite-react-router/src/layouts/dashboard.tsx
index 4ae22979981..50247224592 100644
--- a/playground/vite-react-router/src/layouts/dashboard.tsx
+++ b/playground/vite-react-router/src/layouts/dashboard.tsx
@@ -1,30 +1,11 @@
import * as React from 'react';
-import { Outlet, useLocation, useParams, matchPath } from 'react-router';
+import { Outlet } from 'react-router';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
export default function Layout() {
- const location = useLocation();
- const { orderId } = useParams();
-
- const title = React.useMemo(() => {
- if (location.pathname === '/orders/new') {
- return 'New Order';
- }
- if (matchPath('/orders/:orderId/edit', location.pathname)) {
- return `Order ${orderId} - Edit`;
- }
- if (orderId) {
- return `Order ${orderId}`;
- }
- return undefined;
- }, [location.pathname, orderId]);
-
return (
-
-
-
+
);
}
diff --git a/playground/vite-react-router/src/pages/index.tsx b/playground/vite-react-router/src/pages/index.tsx
index e4581fc26bf..b18c7f9b95e 100644
--- a/playground/vite-react-router/src/pages/index.tsx
+++ b/playground/vite-react-router/src/pages/index.tsx
@@ -1,6 +1,11 @@
import * as React from 'react';
import Typography from '@mui/material/Typography';
+import { PageContainer } from '@toolpad/core/PageContainer';
export default function DashboardPage() {
- return Welcome to Toolpad!;
+ return (
+
+ Welcome to Toolpad!
+
+ );
}
diff --git a/playground/vite-react-router/src/pages/orders.tsx b/playground/vite-react-router/src/pages/orders.tsx
index 40b45f0d306..babf5bd7efd 100644
--- a/playground/vite-react-router/src/pages/orders.tsx
+++ b/playground/vite-react-router/src/pages/orders.tsx
@@ -1,8 +1,12 @@
import * as React from 'react';
+
import { Crud } from '@toolpad/core/Crud';
+import { useParams } from 'react-router';
import { ordersDataSource, Order, ordersCache } from '../data/orders';
export default function OrdersCrudPage() {
+ const { orderId } = useParams();
+
return (
dataSource={ordersDataSource}
@@ -10,6 +14,11 @@ export default function OrdersCrudPage() {
rootPath="/orders"
initialPageSize={25}
defaultValues={{ itemCount: 1 }}
+ pageTitles={{
+ show: `Order ${orderId}`,
+ create: 'New Order',
+ edit: `Order ${orderId} - Edit`,
+ }}
/>
);
}
diff --git a/playground/vite-tanstack-router/src/routes/_layout/index.tsx b/playground/vite-tanstack-router/src/routes/_layout/index.tsx
index 25980eecfbf..f1f5e155955 100644
--- a/playground/vite-tanstack-router/src/routes/_layout/index.tsx
+++ b/playground/vite-tanstack-router/src/routes/_layout/index.tsx
@@ -1,9 +1,14 @@
import * as React from 'react';
import { createFileRoute } from '@tanstack/react-router';
import Typography from '@mui/material/Typography';
+import { PageContainer } from '@toolpad/core/PageContainer';
function DashboardPage() {
- return Welcome to Toolpad!;
+ return (
+
+ Welcome to Toolpad!
+
+ );
}
export const Route = createFileRoute('/_layout/')({
diff --git a/playground/vite-tanstack-router/src/routes/_layout/orders.$.tsx b/playground/vite-tanstack-router/src/routes/_layout/orders.$.tsx
index a27b51871c9..6be271a8dc3 100644
--- a/playground/vite-tanstack-router/src/routes/_layout/orders.$.tsx
+++ b/playground/vite-tanstack-router/src/routes/_layout/orders.$.tsx
@@ -1,9 +1,13 @@
import * as React from 'react';
-import { createFileRoute } from '@tanstack/react-router';
+import { createFileRoute, useParams } from '@tanstack/react-router';
import { Crud } from '@toolpad/core/Crud';
import { ordersDataSource, Order, ordersCache } from '../../data/orders';
export default function OrdersCrudPage() {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const { _splat } = useParams({ strict: false });
+ const orderId = _splat?.split('/')[0];
+
return (
dataSource={ordersDataSource}
@@ -11,6 +15,11 @@ export default function OrdersCrudPage() {
rootPath="/orders"
initialPageSize={25}
defaultValues={{ itemCount: 1 }}
+ pageTitles={{
+ show: `Order ${orderId}`,
+ create: 'New Order',
+ edit: `Order ${orderId} - Edit`,
+ }}
/>
);
}
diff --git a/playground/vite-tanstack-router/src/routes/_layout/route.tsx b/playground/vite-tanstack-router/src/routes/_layout/route.tsx
index 0e63f37e216..de01d0a89d2 100644
--- a/playground/vite-tanstack-router/src/routes/_layout/route.tsx
+++ b/playground/vite-tanstack-router/src/routes/_layout/route.tsx
@@ -1,34 +1,11 @@
import * as React from 'react';
-import { Outlet, createFileRoute, useLocation, useParams } from '@tanstack/react-router';
+import { Outlet, createFileRoute } from '@tanstack/react-router';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import { PageContainer } from '@toolpad/core/PageContainer';
function Layout() {
- const { pathname } = useLocation();
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const { _splat } = useParams({ strict: false });
-
- const title = React.useMemo(() => {
- if (pathname.endsWith('/orders/new')) {
- return 'New Order';
- }
-
- const orderId = _splat?.split('/')[0];
- if (orderId && pathname.endsWith('/edit')) {
- return `Order ${orderId} - Edit`;
- }
- if (orderId) {
- return `Order ${orderId}`;
- }
-
- return undefined;
- }, [_splat, pathname]);
-
return (
-
-
-
+
);
}