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 ? ( - - ) : 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 ? ( + + ) : 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 ( - - - + ); }