Skip to content

Commit

Permalink
Let's pop a save
Browse files Browse the repository at this point in the history
  • Loading branch information
apedroferreira committed Jan 22, 2025
1 parent dccbb8e commit 6b47e14
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 161 deletions.
51 changes: 35 additions & 16 deletions packages/toolpad-core/src/CRUD/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<D extends DataModel> {
/**
* Fields to show.
* Server-side data source.
*/
fields: CRUDFields;
/**
* Methods to interact with server-side data.
*/
methods: {
createOne: (data: Omit<D, 'id'>) => Promise<D>;
};
dataSource: DataSource<D> & Required<Pick<DataSource<D>, 'createOne'>>;
}

function Create<D extends DataModel>(props: CreateProps<D>) {
const { fields, methods } = props;
const { dataSource } = props;
const { fields, ...methods } = dataSource;
const { createOne } = methods;

const notifications = useNotifications();

const [, submitAction, isSubmitting] = React.useActionState<null | Error, FormData>(
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 (
<Box component="form" noValidate autoComplete="off">
<Box component="form" action={submitAction} noValidate autoComplete="off">
<Grid container spacing={2} sx={{ mb: 2 }}>
{fields.map((field) => (
<Grid key={field.field} size={6}>
<TextField id={field.field} label={field.headerName} sx={{ width: '100%' }} />
{fields.map(({ field, headerName }) => (
<Grid key={field} size={6}>
<TextField id={field} label={headerName} sx={{ width: '100%' }} />
</Grid>
))}
</Grid>
<Button type="submit" variant="contained">
Contained
<Button type="submit" variant="contained" loading={isSubmitting}>
Create
</Button>
</Box>
);
Expand Down
23 changes: 5 additions & 18 deletions packages/toolpad-core/src/CRUD/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -42,12 +42,6 @@ const ErrorOverlay = styled('div')(({ theme }) => ({
zIndex: 10,
}));

interface GetManyParams {
paginationModel: GridPaginationModel;
sortModel: GridSortModel;
filterModel: GridFilterModel;
}

export interface ListSlotProps {
dataGrid?: DataGridProps;
}
Expand All @@ -62,16 +56,9 @@ export interface ListSlots {

export interface ListProps<D extends DataModel> {
/**
* 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<void>;
};
dataSource: DataSource<D> & Required<Pick<DataSource<D>, 'getMany'>>;
/**
* Initial number of rows to show per page.
* @default 100
Expand Down Expand Up @@ -103,15 +90,15 @@ export interface ListProps<D extends DataModel> {

function List<D extends DataModel>(props: ListProps<D>) {
const {
fields,
methods,
dataSource,
initialPageSize = 100,
onRowClick,
onCreateClick,
onEditClick,
slots,
slotProps,
} = props;
const { fields, ...methods } = dataSource;
const { getMany, deleteOne } = methods;

const dialogs = useDialogs();
Expand Down
24 changes: 9 additions & 15 deletions packages/toolpad-core/src/CRUD/Show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<D extends DataModel> {
id: DataModelId;
/**
* Fields to show.
* Server-side data source.
*/
fields: CRUDFields;
/**
* Methods to interact with server-side data.
*/
methods: {
getOne: (id: DataModelId) => Promise<D>;
deleteOne?: (id: DataModelId) => Promise<void>;
};
dataSource: DataSource<D> & Required<Pick<DataSource<D>, 'getOne'>>;
/**
* Callback fired when the "Edit" button is clicked.
*/
Expand All @@ -39,7 +32,8 @@ export interface ShowProps<D extends DataModel> {
}

function Show<D extends DataModel>(props: ShowProps<D>) {
const { id, fields, methods, onEditClick, onDelete } = props;
const { id, dataSource, onEditClick, onDelete } = props;
const { fields, ...methods } = dataSource;
const { getOne, deleteOne } = methods;

const dialogs = useDialogs();
Expand Down Expand Up @@ -122,12 +116,12 @@ function Show<D extends DataModel>(props: ShowProps<D>) {
return data ? (
<Box sx={{ flexGrow: 1 }}>
<Grid container spacing={2}>
{fields.map((field) => (
<Grid key={field.field} size={6}>
{fields.map(({ field, headerName }) => (
<Grid key={field} size={6}>
<Paper sx={{ px: 2, py: 1 }}>
<Typography variant="overline">{field.headerName}</Typography>
<Typography variant="overline">{headerName}</Typography>
<Typography variant="body1" sx={{ mb: 1 }}>
{String(data[field.field])}
{String(data[field])}
</Typography>
</Paper>
</Grid>
Expand Down
2 changes: 2 additions & 0 deletions packages/toolpad-core/src/CRUD/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './List';
export * from './Show';
export * from './Create';

export * from './shared';
18 changes: 15 additions & 3 deletions packages/toolpad-core/src/CRUD/shared.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
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;

export interface DataModel {
id: DataModelId;
[key: string]: unknown;
}

export interface GetManyParams {
paginationModel: GridPaginationModel;
sortModel: GridSortModel;
filterModel: GridFilterModel;
}

export interface DataSource<D extends DataModel> {
fields: GridColDef[];
getMany?: (params: GetManyParams) => Promise<{ items: D[]; itemCount: number }>;
getOne?: (id: DataModelId) => Promise<D>;
createOne?: (data: Omit<D, 'id'>) => Promise<D>;
deleteOne?: (id: DataModelId) => Promise<void>;
}
62 changes: 62 additions & 0 deletions playground/vite/src/data/orders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { DataSource } from '@toolpad/core/CRUD';

export interface Order extends Record<string, unknown> {
id: number;
name: string;
status: string;
}

const ordersDataSource: Required<DataSource<Order>> = {
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<Order>((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<void>((resolve) => {
setTimeout(() => {
resolve();
}, 1500);
});
},
};

export default ordersDataSource;
32 changes: 2 additions & 30 deletions playground/vite/src/pages/new-order.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,7 @@
import * as React from 'react';
import { Create } from '@toolpad/core/CRUD';

interface Order extends Record<string, unknown> {
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 (
<Create<Order>
fields={orderFields}
methods={{
createOne: () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 69,
name: 'Order 69',
status: 'pending',
});
}, 1500);
});
},
}}
/>
);
return <Create<Order> dataSource={ordersDataSource} />;
}
37 changes: 2 additions & 35 deletions playground/vite/src/pages/order.tsx
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
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();
Expand All @@ -25,29 +14,7 @@ export default function OrderPage() {
return orderId ? (
<Show<Order>
id={orderId}
fields={orderFields}
methods={{
getOne: () => {
return new Promise<Order>((resolve) => {
setTimeout(() => {
return orderId
? resolve({
id: Number(orderId),
name: `Order ${orderId}`,
status: 'pending',
})
: null;
}, 1500);
});
},
deleteOne: () => {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 1500);
});
},
}}
dataSource={ordersDataSource}
onEditClick={() => {}}
onDelete={handleDelete}
/>
Expand Down
Loading

0 comments on commit 6b47e14

Please sign in to comment.