diff --git a/packages/toolpad-core/src/CRUD/Create.tsx b/packages/toolpad-core/src/CRUD/Create.tsx index 0942aa3015e..655e1edba00 100644 --- a/packages/toolpad-core/src/CRUD/Create.tsx +++ b/packages/toolpad-core/src/CRUD/Create.tsx @@ -4,36 +4,55 @@ import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Grid from '@mui/material/Grid2'; import TextField from '@mui/material/TextField'; -import { CRUDFields, DataModel } from './shared'; +import { useNotifications } from '../useNotifications'; +import { DataModel, DataSource } from './shared'; export interface CreateProps { /** - * Fields to show. + * Server-side data source. */ - fields: CRUDFields; - /** - * Methods to interact with server-side data. - */ - methods: { - createOne: (data: Omit) => Promise; - }; + dataSource: DataSource & Required, 'createOne'>>; } function Create(props: CreateProps) { - const { fields, methods } = props; + const { dataSource } = props; + const { fields, ...methods } = dataSource; const { createOne } = methods; + const notifications = useNotifications(); + + const [, submitAction, isSubmitting] = React.useActionState( + async (previousState, formData) => { + try { + await createOne( + Object.fromEntries(fields.map(({ field }) => [field, formData.get(field)])) as D, + ); + notifications.show('Item created successfully.', { + severity: 'success', + }); + } catch (createError) { + notifications.show(`Failed to create item. Reason: ${(createError as Error).message}`, { + severity: 'error', + }); + return createError as Error; + } + + return null; + }, + null, + ); + return ( - + - {fields.map((field) => ( - - + {fields.map(({ field, headerName }) => ( + + ))} - ); diff --git a/packages/toolpad-core/src/CRUD/List.tsx b/packages/toolpad-core/src/CRUD/List.tsx index 4e662bdfdcd..3e5cca43e24 100644 --- a/packages/toolpad-core/src/CRUD/List.tsx +++ b/packages/toolpad-core/src/CRUD/List.tsx @@ -25,7 +25,7 @@ import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import { useDialogs } from '../useDialogs'; import { useNotifications } from '../useNotifications'; -import { CRUDFields, DataModel, DataModelId } from './shared'; +import { DataModel, DataModelId, DataSource } from './shared'; const ErrorOverlay = styled('div')(({ theme }) => ({ position: 'absolute', @@ -42,12 +42,6 @@ const ErrorOverlay = styled('div')(({ theme }) => ({ zIndex: 10, })); -interface GetManyParams { - paginationModel: GridPaginationModel; - sortModel: GridSortModel; - filterModel: GridFilterModel; -} - export interface ListSlotProps { dataGrid?: DataGridProps; } @@ -62,16 +56,9 @@ export interface ListSlots { export interface ListProps { /** - * Fields to show. - */ - fields: CRUDFields; - /** - * Methods to interact with server-side data. + * Server-side data source. */ - methods: { - getMany: (params: GetManyParams) => Promise<{ items: D[]; itemCount: number }>; - deleteOne?: (id: DataModelId) => Promise; - }; + dataSource: DataSource & Required, 'getMany'>>; /** * Initial number of rows to show per page. * @default 100 @@ -103,8 +90,7 @@ export interface ListProps { function List(props: ListProps) { const { - fields, - methods, + dataSource, initialPageSize = 100, onRowClick, onCreateClick, @@ -112,6 +98,7 @@ function List(props: ListProps) { slots, slotProps, } = props; + const { fields, ...methods } = dataSource; const { getMany, deleteOne } = methods; const dialogs = useDialogs(); diff --git a/packages/toolpad-core/src/CRUD/Show.tsx b/packages/toolpad-core/src/CRUD/Show.tsx index c1a2263a282..9f5e7e0aa6c 100644 --- a/packages/toolpad-core/src/CRUD/Show.tsx +++ b/packages/toolpad-core/src/CRUD/Show.tsx @@ -13,21 +13,14 @@ import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import { useDialogs } from '../useDialogs'; import { useNotifications } from '../useNotifications'; -import { CRUDFields, DataModel, DataModelId } from './shared'; +import { DataModel, DataModelId, DataSource } from './shared'; export interface ShowProps { id: DataModelId; /** - * Fields to show. + * Server-side data source. */ - fields: CRUDFields; - /** - * Methods to interact with server-side data. - */ - methods: { - getOne: (id: DataModelId) => Promise; - deleteOne?: (id: DataModelId) => Promise; - }; + dataSource: DataSource & Required, 'getOne'>>; /** * Callback fired when the "Edit" button is clicked. */ @@ -39,7 +32,8 @@ export interface ShowProps { } function Show(props: ShowProps) { - const { id, fields, methods, onEditClick, onDelete } = props; + const { id, dataSource, onEditClick, onDelete } = props; + const { fields, ...methods } = dataSource; const { getOne, deleteOne } = methods; const dialogs = useDialogs(); @@ -122,12 +116,12 @@ function Show(props: ShowProps) { return data ? ( - {fields.map((field) => ( - + {fields.map(({ field, headerName }) => ( + - {field.headerName} + {headerName} - {String(data[field.field])} + {String(data[field])} diff --git a/packages/toolpad-core/src/CRUD/index.ts b/packages/toolpad-core/src/CRUD/index.ts index ea0c0d4556b..a352e7d0275 100644 --- a/packages/toolpad-core/src/CRUD/index.ts +++ b/packages/toolpad-core/src/CRUD/index.ts @@ -1,3 +1,5 @@ export * from './List'; export * from './Show'; export * from './Create'; + +export * from './shared'; diff --git a/packages/toolpad-core/src/CRUD/shared.ts b/packages/toolpad-core/src/CRUD/shared.ts index 8c22a05fcaa..1ce5b205a09 100644 --- a/packages/toolpad-core/src/CRUD/shared.ts +++ b/packages/toolpad-core/src/CRUD/shared.ts @@ -1,6 +1,4 @@ -import { GridColDef } from '@mui/x-data-grid'; - -export type CRUDFields = GridColDef[]; +import { GridColDef, GridFilterModel, GridPaginationModel, GridSortModel } from '@mui/x-data-grid'; export type DataModelId = string | number; @@ -8,3 +6,17 @@ export interface DataModel { id: DataModelId; [key: string]: unknown; } + +export interface GetManyParams { + paginationModel: GridPaginationModel; + sortModel: GridSortModel; + filterModel: GridFilterModel; +} + +export interface DataSource { + fields: GridColDef[]; + getMany?: (params: GetManyParams) => Promise<{ items: D[]; itemCount: number }>; + getOne?: (id: DataModelId) => Promise; + createOne?: (data: Omit) => Promise; + deleteOne?: (id: DataModelId) => Promise; +} diff --git a/playground/vite/src/data/orders.ts b/playground/vite/src/data/orders.ts new file mode 100644 index 00000000000..26b821108f9 --- /dev/null +++ b/playground/vite/src/data/orders.ts @@ -0,0 +1,62 @@ +import { DataSource } from '@toolpad/core/CRUD'; + +export interface Order extends Record { + id: number; + name: string; + status: string; +} + +const ordersDataSource: Required> = { + fields: [ + { field: 'id', headerName: 'ID' }, + { field: 'name', headerName: 'Name' }, + { field: 'status', headerName: 'Status' }, + ], + getMany: async ({ paginationModel }) => { + return new Promise<{ items: Order[]; itemCount: number }>((resolve) => { + setTimeout(() => { + resolve({ + items: Array.from({ length: paginationModel.pageSize }, (_, i) => ({ + id: paginationModel.page * paginationModel.pageSize + i + 1, + name: `Order ${paginationModel.page * paginationModel.pageSize + i + 1}`, + status: 'pending', + })).slice(0, paginationModel.pageSize), + itemCount: 300, + }); + }, 1500); + }); + }, + getOne: (orderId) => { + return new Promise((resolve) => { + setTimeout(() => { + return orderId + ? resolve({ + id: Number(orderId), + name: `Order ${orderId}`, + status: 'pending', + }) + : null; + }, 1500); + }); + }, + createOne: () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + id: 69, + name: 'Order 69', + status: 'pending', + }); + }, 1500); + }); + }, + deleteOne: () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 1500); + }); + }, +}; + +export default ordersDataSource; diff --git a/playground/vite/src/pages/new-order.tsx b/playground/vite/src/pages/new-order.tsx index d35c2967342..ba64a9f7a98 100644 --- a/playground/vite/src/pages/new-order.tsx +++ b/playground/vite/src/pages/new-order.tsx @@ -1,35 +1,7 @@ import * as React from 'react'; import { Create } from '@toolpad/core/CRUD'; - -interface Order extends Record { - id: number; - name: string; - status: string; -} - -const orderFields = [ - { field: 'id', headerName: 'ID' }, - { field: 'name', headerName: 'Name' }, - { field: 'status', headerName: 'Status' }, -]; +import ordersDataSource, { Order } from '../data/orders'; export default function OrderPage() { - return ( - - fields={orderFields} - methods={{ - createOne: () => { - return new Promise((resolve) => { - setTimeout(() => { - resolve({ - id: 69, - name: 'Order 69', - status: 'pending', - }); - }, 1500); - }); - }, - }} - /> - ); + return dataSource={ordersDataSource} />; } diff --git a/playground/vite/src/pages/order.tsx b/playground/vite/src/pages/order.tsx index a39d39dcca2..fc6bf176c93 100644 --- a/playground/vite/src/pages/order.tsx +++ b/playground/vite/src/pages/order.tsx @@ -1,18 +1,7 @@ import * as React from 'react'; import { Show } from '@toolpad/core/CRUD'; import { useNavigate, useParams } from 'react-router-dom'; - -interface Order extends Record { - id: number; - name: string; - status: string; -} - -const orderFields = [ - { field: 'id', headerName: 'ID' }, - { field: 'name', headerName: 'Name' }, - { field: 'status', headerName: 'Status' }, -]; +import ordersDataSource, { Order } from '../data/orders'; export default function OrderPage() { const navigate = useNavigate(); @@ -25,29 +14,7 @@ export default function OrderPage() { return orderId ? ( id={orderId} - fields={orderFields} - methods={{ - getOne: () => { - return new Promise((resolve) => { - setTimeout(() => { - return orderId - ? resolve({ - id: Number(orderId), - name: `Order ${orderId}`, - status: 'pending', - }) - : null; - }, 1500); - }); - }, - deleteOne: () => { - return new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 1500); - }); - }, - }} + dataSource={ordersDataSource} onEditClick={() => {}} onDelete={handleDelete} /> diff --git a/playground/vite/src/pages/orders.tsx b/playground/vite/src/pages/orders.tsx index 2168f592d9d..69119e16944 100644 --- a/playground/vite/src/pages/orders.tsx +++ b/playground/vite/src/pages/orders.tsx @@ -1,18 +1,7 @@ import * as React from 'react'; import { useNavigate } from 'react-router-dom'; import { List } from '@toolpad/core/CRUD'; - -interface Order extends Record { - id: number; - name: string; - status: string; -} - -const orderFields = [ - { field: 'id', headerName: 'ID' }, - { field: 'name', headerName: 'Name' }, - { field: 'status', headerName: 'Status' }, -]; +import ordersDataSource, { Order } from '../data/orders'; export default function OrdersPage() { const navigate = useNavigate(); @@ -30,30 +19,7 @@ export default function OrdersPage() { return ( - fields={orderFields} - methods={{ - getMany: async ({ paginationModel }) => { - return new Promise<{ items: Order[]; itemCount: number }>((resolve) => { - setTimeout(() => { - resolve({ - items: Array.from({ length: paginationModel.pageSize }, (_, i) => ({ - id: paginationModel.page * paginationModel.pageSize + i + 1, - name: `Order ${paginationModel.page * paginationModel.pageSize + i + 1}`, - status: 'pending', - })).slice(0, paginationModel.pageSize), - itemCount: 300, - }); - }, 1500); - }); - }, - deleteOne: () => { - return new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 1500); - }); - }, - }} + dataSource={ordersDataSource} initialPageSize={25} onRowClick={handleRowClick} onCreateClick={handleCreateClick} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57627856428..7f62673f17d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,7 +195,7 @@ importers: version: 7.37.2(eslint@8.57.1) eslint-plugin-react-compiler: specifier: latest - version: 19.0.0-beta-63e3235-20250105(eslint@8.57.1) + version: 19.0.0-beta-decd7b8-20250118(eslint@8.57.1) eslint-plugin-react-hooks: specifier: 4.6.2 version: 4.6.2(eslint@8.57.1) @@ -1362,7 +1362,7 @@ importers: version: 18.3.1(react@18.3.1) recharts: specifier: alpha - version: 3.0.0-alpha.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@4.2.1) + version: 3.0.0-alpha.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@4.2.1) packages: @@ -6059,8 +6059,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@19.0.0-beta-63e3235-20250105: - resolution: {integrity: sha512-Smts5x+u+rRopr0926jCXFPkS8D8hFJexDvTW41V0Xu/xHgd4pnGWiJQRBsvTEARzOdJ6NdlmYs4n+O4Thn2iA==} + eslint-plugin-react-compiler@19.0.0-beta-decd7b8-20250118: + resolution: {integrity: sha512-qfs+Xo+VcYPbbVLI2tCP+KBGwm0oksAhjFJO1GwOvP+4b18LLcPZu7xopRhUTOaNd+nn1vOp9EQLZC1wMNxSrQ==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -9056,8 +9056,8 @@ packages: resolution: {integrity: sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==} engines: {node: '>= 4'} - recharts@3.0.0-alpha.2: - resolution: {integrity: sha512-pEbg/WqTMvxCfsMnhcGzD7t68U5RFzYg2VBPlNB8dLBdoodnm27HffPf2RR0si2Tsjcp1KLaWEOAMpr84hlxSA==} + recharts@3.0.0-alpha.4: + resolution: {integrity: sha512-vRRHmy678cOjzW4ODdl7BEqMXRWaRYC3qf6yYwLY6ECV0Xe0AJvLMU3ITBhP3huQ9Hh1ECK2mT3kVEmVbcjvXw==} engines: {node: '>=18'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -15911,7 +15911,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@19.0.0-beta-63e3235-20250105(eslint@8.57.1): + eslint-plugin-react-compiler@19.0.0-beta-decd7b8-20250118(eslint@8.57.1): dependencies: '@babel/core': 7.26.0 '@babel/parser': 7.26.2 @@ -19408,7 +19408,7 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 - recharts@3.0.0-alpha.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@4.2.1): + recharts@3.0.0-alpha.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@4.2.1): dependencies: '@reduxjs/toolkit': 1.9.7(react-redux@8.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1))(react@18.3.1) clsx: 2.1.1