Skip to content

Commit

Permalink
All-in-one CRUD ohyeah
Browse files Browse the repository at this point in the history
  • Loading branch information
apedroferreira committed Feb 3, 2025
1 parent 734d716 commit 3784767
Show file tree
Hide file tree
Showing 15 changed files with 194 additions and 136 deletions.
118 changes: 113 additions & 5 deletions packages/toolpad-core/src/CRUD/CRUD.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,128 @@
'use client';
import * as React from 'react';
import { DataModel, DataSource } from './shared';
import { CRUDContext } from '../shared/context';
import { match } from 'path-to-regexp';
import invariant from 'invariant';
import { DataModel, DataModelId, DataSource } from './shared';
import { Provider as CRUDProvider } from './Provider';
import { RouterContext } from '../shared/context';
import { List } from './List';
import { Show } from './Show';
import { Create } from './Create';
import { Edit } from './Edit';

export interface CRUDProps<D extends DataModel> {
/**
* Server-side data source.
*/
dataSource: DataSource<D>;
children?: React.ReactNode;
/**
* Path to resource list page.
*/
list: string;
/**
* Path to resource show page.
*/
show: string;
/**
* Path to resource create page.
*/
create: string;
/**
* Path to resource edit page.
*/
edit: string;
/**
* Initial number of rows to show per page.
* @default 100
*/
initialPageSize?: number;
/**
* Initial form values.
*/
initialValues?: Omit<D, 'id'>;
}

function CRUD<D extends DataModel>(props: CRUDProps<D>) {
const { dataSource, children } = props;
const { dataSource, list, show, create, edit, initialPageSize, initialValues } = props;

const routerContext = React.useContext(RouterContext);

const handleRowClick = React.useCallback(
(id: string | number) => {
routerContext?.navigate(`/orders/${String(id)}`);
},
[routerContext],
);

const handleCreateClick = React.useCallback(() => {
routerContext?.navigate('/orders/new');
}, [routerContext]);

const handleEditClick = React.useCallback(
(id: string | number) => {
routerContext?.navigate(`/orders/${String(id)}/edit`);
},
[routerContext],
);

const handleCreate = React.useCallback(() => {
routerContext?.navigate('/orders');
}, [routerContext]);

const handleEdit = React.useCallback(() => {
routerContext?.navigate('/orders');
}, [routerContext]);

const handleDelete = React.useCallback(() => {
routerContext?.navigate('/orders');
}, [routerContext]);

const renderedRoute = React.useMemo(() => {
const pathname = routerContext?.pathname ?? '';

if (match(list)(pathname)) {
return (
<List<D>
initialPageSize={initialPageSize}
onRowClick={handleRowClick}
onCreateClick={handleCreateClick}
onEditClick={handleEditClick}
/>
);
}
const showMatch = match<{ id: DataModelId }>(show)(pathname);
if (showMatch) {
const resourceId = showMatch.params.id;
invariant(resourceId, 'No resource ID present in URL.');
return <Show<D> id={resourceId} onEditClick={handleEditClick} onDelete={handleDelete} />;
}
if (match(create)(pathname)) {
return <Create<D> initialValues={initialValues} onSubmitSuccess={handleCreate} />;
}
const editMatch = match<{ id: DataModelId }>(edit)(pathname);
if (editMatch) {
const resourceId = editMatch.params.id;
invariant(resourceId, 'No resource ID present in URL.');
return <Edit<D> id={resourceId} onSubmitSuccess={handleEdit} />;
}
return null;
}, [
routerContext?.pathname,
list,
show,
create,
edit,
initialPageSize,
handleRowClick,
handleCreateClick,
handleEditClick,
handleDelete,
initialValues,
handleCreate,
handleEdit,
]);

return <CRUDContext value={{ dataSource } as CRUDProps<DataModel>}>{children}</CRUDContext>;
return <CRUDProvider<D> dataSource={dataSource}>{renderedRoute}</CRUDProvider>;
}

export { CRUD };
9 changes: 7 additions & 2 deletions packages/toolpad-core/src/CRUD/Create.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import invariant from 'invariant';
import { FormPage } from './FormPage';
import { DataModel, DataSource } from './shared';
import { CRUDContext } from '../shared/context';
Expand All @@ -13,7 +14,7 @@ export interface CreateProps<D extends DataModel> {
/**
* Initial form values.
*/
initialValues: Omit<D, 'id'>;
initialValues?: Omit<D, 'id'>;
/**
* Callback fired when the form is successfully submitted.
*/
Expand All @@ -24,7 +25,11 @@ function Create<D extends DataModel>(props: CreateProps<D>) {
const { initialValues, onSubmitSuccess } = props;

const crudContext = React.useContext(CRUDContext);
const dataSource = props.dataSource ?? crudContext.dataSource;
const dataSource = (props.dataSource ?? crudContext.dataSource) as Exclude<
typeof props.dataSource,
undefined
>;
invariant(dataSource, 'No data source found.');

return (
<FormPage
Expand Down
7 changes: 6 additions & 1 deletion packages/toolpad-core/src/CRUD/Edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import invariant from 'invariant';
import { FormPage } from './FormPage';
import { DataModel, DataModelId, DataSource } from './shared';
import { CRUDContext } from '../shared/context';
Expand All @@ -24,7 +25,11 @@ function Edit<D extends DataModel>(props: EditProps<D>) {
const { id, onSubmitSuccess } = props;

const crudContext = React.useContext(CRUDContext);
const dataSource = props.dataSource ?? crudContext.dataSource;
const dataSource = (props.dataSource ?? crudContext.dataSource) as Exclude<
typeof props.dataSource,
undefined
>;
invariant(dataSource, 'No data source found.');

const { fields, ...methods } = dataSource;
const { getOne } = methods;
Expand Down
10 changes: 8 additions & 2 deletions packages/toolpad-core/src/CRUD/FormPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface FormPageProps<D extends DataModel> {
dataSource:
| (DataSource<D> & Required<Pick<DataSource<D>, 'createOne'>>)
| (DataSource<D> & Required<Pick<DataSource<D>, 'updateOne'>>);
initialValues: Omit<D, 'id'>;
initialValues?: Omit<D, 'id'>;
submitMethodName: 'createOne' | 'updateOne';
onSubmitSuccess?: () => void;
localeText: FormPageLocaleText;
Expand All @@ -42,7 +42,13 @@ export interface FormPageProps<D extends DataModel> {
* @ignore - internal component.
*/
function FormPage<D extends DataModel>(props: FormPageProps<D>) {
const { dataSource, initialValues, submitMethodName, onSubmitSuccess, localeText } = props;
const {
dataSource,
initialValues = {} as Omit<D, 'id'>,
submitMethodName,
onSubmitSuccess,
localeText,
} = props;
const { fields, validate, ...methods } = dataSource;

const submitMethod = methods[submitMethodName] as (data: Omit<D, 'id'>) => Promise<D>;
Expand Down
7 changes: 6 additions & 1 deletion packages/toolpad-core/src/CRUD/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import invariant from 'invariant';
import { useDialogs } from '../useDialogs';
import { useNotifications } from '../useNotifications';
import { DataModel, DataModelId, DataSource } from './shared';
Expand Down Expand Up @@ -93,7 +94,11 @@ function List<D extends DataModel>(props: ListProps<D>) {
const { initialPageSize = 100, onRowClick, onCreateClick, onEditClick, slots, slotProps } = props;

const crudContext = React.useContext(CRUDContext);
const dataSource = props.dataSource ?? crudContext.dataSource;
const dataSource = (props.dataSource ?? crudContext.dataSource) as Exclude<
typeof props.dataSource,
undefined
>;
invariant(dataSource, 'No data source found.');

const { fields, ...methods } = dataSource;
const { getMany, deleteOne } = methods;
Expand Down
20 changes: 20 additions & 0 deletions packages/toolpad-core/src/CRUD/Provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';
import * as React from 'react';
import { DataModel, DataSource } from './shared';
import { CRUDContext } from '../shared/context';

export interface ProviderProps<D extends DataModel> {
/**
* Server-side data source.
*/
dataSource: DataSource<D>;
children?: React.ReactNode;
}

function Provider<D extends DataModel>(props: ProviderProps<D>) {
const { dataSource, children } = props;

return <CRUDContext value={{ dataSource } as ProviderProps<DataModel>}>{children}</CRUDContext>;
}

export { Provider };
7 changes: 6 additions & 1 deletion packages/toolpad-core/src/CRUD/Show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import invariant from 'invariant';
import { useDialogs } from '../useDialogs';
import { useNotifications } from '../useNotifications';
import { DataModel, DataModelId, DataSource } from './shared';
Expand All @@ -37,7 +38,11 @@ function Show<D extends DataModel>(props: ShowProps<D>) {
const { id, onEditClick, onDelete } = props;

const crudContext = React.useContext(CRUDContext);
const dataSource = props.dataSource ?? crudContext.dataSource;
const dataSource = (props.dataSource ?? crudContext.dataSource) as Exclude<
typeof props.dataSource,
undefined
>;
invariant(dataSource, 'No data source found.');

const { fields, ...methods } = dataSource;
const { getOne, deleteOne } = methods;
Expand Down
4 changes: 4 additions & 0 deletions packages/toolpad-core/src/CRUD/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ export * from './Show';
export * from './Create';
export * from './Edit';

export * from './Provider';

export * from './CRUD';

export * from './shared';
4 changes: 2 additions & 2 deletions packages/toolpad-core/src/shared/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import type { PaletteMode } from '@mui/material';
import type { Branding, Navigation, Router } from '../AppProvider';
import type { DataModel } from '../CRUD';
import type { CRUDProps } from '../CRUD/CRUD';
import type { ProviderProps } from '../CRUD/Provider';

export const BrandingContext = React.createContext<Branding | null>(null);

Expand All @@ -23,7 +23,7 @@ export const RouterContext = React.createContext<Router | null>(null);
export const WindowContext = React.createContext<Window | undefined>(undefined);

export const CRUDContext = React.createContext<{
dataSource: CRUDProps<DataModel>['dataSource'] | null;
dataSource: ProviderProps<DataModel>['dataSource'] | null;
}>({
dataSource: null,
});
17 changes: 1 addition & 16 deletions playground/vite/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import App from './App';
import Layout from './layouts/dashboard';
import DashboardPage from './pages';
import OrdersPage from './pages/orders';
import OrderPage from './pages/order';
import NewOrderPage from './pages/new-order';
import EditOrderPage from './pages/edit-order';

const router = createBrowserRouter([
{
Expand All @@ -22,21 +19,9 @@ const router = createBrowserRouter([
Component: DashboardPage,
},
{
path: 'orders',
path: 'orders/*',
Component: OrdersPage,
},
{
path: 'orders/:orderId',
Component: OrderPage,
},
{
path: 'orders/new',
Component: NewOrderPage,
},
{
path: 'orders/:orderId/edit',
Component: EditOrderPage,
},
],
},
],
Expand Down
17 changes: 0 additions & 17 deletions playground/vite/src/pages/edit-order.tsx

This file was deleted.

20 changes: 0 additions & 20 deletions playground/vite/src/pages/new-order.tsx

This file was deleted.

Loading

0 comments on commit 3784767

Please sign in to comment.