diff --git a/packages/toolpad-core/src/CRUD/CRUD.tsx b/packages/toolpad-core/src/CRUD/CRUD.tsx deleted file mode 100644 index 4310fc25093..00000000000 --- a/packages/toolpad-core/src/CRUD/CRUD.tsx +++ /dev/null @@ -1,163 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { match } from 'path-to-regexp'; -import invariant from 'invariant'; -import { DataModel, DataModelId, DataSource, OmitId } from './shared'; -import { CrudProvider } from './CrudProvider'; -import { RouterContext } from '../shared/context'; -import { List } from './List'; -import { Show } from './Show'; -import { Create } from './Create'; -import { Edit } from './Edit'; - -export interface CrudProps { - /** - * Server-side data source. - */ - dataSource: DataSource; - /** - * Root path to CRUD pages. - */ - rootPath: string; - /** - * Initial number of rows to show per page. - * @default 100 - */ - initialPageSize?: number; - /** - * Default form values for a new item. - * @default {} - */ - defaultValues?: Partial>; -} -/** - * - * Demos: - * - * - [Crud](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [Crud API](https://mui.com/toolpad/core/api/crud) - */ -function Crud(props: CrudProps) { - const { dataSource, rootPath, initialPageSize, defaultValues } = props; - - const listPath = rootPath; - const showPath = `${rootPath}/:id`; - const createPath = `${rootPath}/new`; - const editPath = `${rootPath}/:id/edit`; - - const routerContext = React.useContext(RouterContext); - - const handleRowClick = React.useCallback( - (id: string | number) => { - routerContext?.navigate(`${rootPath}/${String(id)}`); - }, - [rootPath, routerContext], - ); - - const handleCreateClick = React.useCallback(() => { - routerContext?.navigate(createPath); - }, [createPath, routerContext]); - - const handleEditClick = React.useCallback( - (id: string | number) => { - routerContext?.navigate(`${rootPath}/${String(id)}/edit`); - }, - [rootPath, routerContext], - ); - - const handleCreate = React.useCallback(() => { - routerContext?.navigate(listPath); - }, [listPath, routerContext]); - - const handleEdit = React.useCallback(() => { - routerContext?.navigate(listPath); - }, [listPath, routerContext]); - - const handleDelete = React.useCallback(() => { - routerContext?.navigate(listPath); - }, [listPath, routerContext]); - - const renderedRoute = React.useMemo(() => { - const pathname = routerContext?.pathname ?? ''; - - if (match(listPath)(pathname)) { - return ( - - initialPageSize={initialPageSize} - onRowClick={handleRowClick} - onCreateClick={handleCreateClick} - onEditClick={handleEditClick} - /> - ); - } - if (match(createPath)(pathname)) { - return ( - - initialValues={defaultValues} - onSubmitSuccess={handleCreate} - resetOnSubmit={false} - /> - ); - } - const showMatch = match<{ id: DataModelId }>(showPath)(pathname); - if (showMatch) { - const resourceId = showMatch.params.id; - invariant(resourceId, 'No resource ID present in URL.'); - return id={resourceId} onEditClick={handleEditClick} onDelete={handleDelete} />; - } - const editMatch = match<{ id: DataModelId }>(editPath)(pathname); - if (editMatch) { - const resourceId = editMatch.params.id; - invariant(resourceId, 'No resource ID present in URL.'); - return id={resourceId} onSubmitSuccess={handleEdit} resetOnSubmit={false} />; - } - return null; - }, [ - createPath, - editPath, - handleCreate, - handleCreateClick, - handleDelete, - handleEdit, - handleEditClick, - handleRowClick, - initialPageSize, - defaultValues, - listPath, - routerContext?.pathname, - showPath, - ]); - - return dataSource={dataSource}>{renderedRoute}; -} - -Crud.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side data source. - */ - dataSource: PropTypes.object.isRequired, - /** - * Default form values for a new item. - * @default {} - */ - defaultValues: PropTypes.object, - /** - * Initial number of rows to show per page. - * @default 100 - */ - initialPageSize: PropTypes.number, - /** - * Root path to CRUD pages. - */ - rootPath: PropTypes.string.isRequired, -} as any; - -export { Crud }; diff --git a/packages/toolpad-core/src/CRUD/CRUDProvider.tsx b/packages/toolpad-core/src/CRUD/CRUDProvider.tsx deleted file mode 100644 index 18f91ced6ed..00000000000 --- a/packages/toolpad-core/src/CRUD/CRUDProvider.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { DataModel, DataSource } from './shared'; -import { CrudContext } from '../shared/context'; - -export interface CrudProviderProps { - /** - * Server-side data source. - */ - dataSource: DataSource; - children?: React.ReactNode; -} -/** - * - * Demos: - * - * - [Crud](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [CrudProvider API](https://mui.com/toolpad/core/api/crud-provider) - */ -function CrudProvider(props: CrudProviderProps) { - const { dataSource, children } = props; - - return ( - }>{children} - ); -} - -CrudProvider.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * @ignore - */ - children: PropTypes.node, - /** - * Server-side data source. - */ - dataSource: PropTypes.object.isRequired, -} as any; - -export { CrudProvider }; diff --git a/packages/toolpad-core/src/CRUD/Create.tsx b/packages/toolpad-core/src/CRUD/Create.tsx deleted file mode 100644 index b8dc510d235..00000000000 --- a/packages/toolpad-core/src/CRUD/Create.tsx +++ /dev/null @@ -1,98 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import invariant from 'invariant'; -import { FormPage } from './FormPage'; -import { DataModel, DataSource, OmitId } from './shared'; -import { CrudContext } from '../shared/context'; - -export interface CreateProps { - /** - * Server-side data source. - */ - dataSource?: DataSource & Required, 'createOne'>>; - /** - * Initial form values. - * @default {} - */ - initialValues?: Partial>; - /** - * Callback fired when the form is successfully submitted. - */ - onSubmitSuccess?: () => void; - /** - * Whether the form fields should reset after the form is submitted. - */ - resetOnSubmit?: boolean; -} -/** - * - * Demos: - * - * - [Crud](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [Create API](https://mui.com/toolpad/core/api/create) - */ -function Create(props: CreateProps) { - const { initialValues, onSubmitSuccess, resetOnSubmit } = props; - - const crudContext = React.useContext(CrudContext); - const dataSource = (props.dataSource ?? crudContext.dataSource) as Exclude< - typeof props.dataSource, - undefined - >; - - invariant(dataSource, 'No data source found.'); - - const { createOne } = dataSource; - - const handleCreate = React.useCallback( - async (formValues: Partial>) => { - await createOne(formValues); - }, - [createOne], - ); - - return ( - - ); -} - -Create.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side data source. - */ - dataSource: PropTypes.object, - /** - * Initial form values. - * @default {} - */ - initialValues: PropTypes.object, - /** - * Callback fired when the form is successfully submitted. - */ - onSubmitSuccess: PropTypes.func, - /** - * Whether the form fields should reset after the form is submitted. - */ - resetOnSubmit: PropTypes.bool, -} as any; - -export { Create }; diff --git a/packages/toolpad-core/src/CRUD/Edit.tsx b/packages/toolpad-core/src/CRUD/Edit.tsx deleted file mode 100644 index 54cc0c01d32..00000000000 --- a/packages/toolpad-core/src/CRUD/Edit.tsx +++ /dev/null @@ -1,146 +0,0 @@ -'use client'; -import * as React from 'react'; -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, OmitId } from './shared'; -import { CrudContext } from '../shared/context'; - -export interface EditProps { - id: DataModelId; - /** - * Server-side data source. - */ - dataSource?: DataSource & Required, 'getOne' | 'updateOne'>>; - /** - * Callback fired when the form is successfully submitted. - */ - onSubmitSuccess?: () => void; - /** - * Whether the form fields should reset after the form is submitted. - */ - resetOnSubmit?: boolean; -} -/** - * - * Demos: - * - * - [Crud](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [Edit API](https://mui.com/toolpad/core/api/edit) - */ -function Edit(props: EditProps) { - const { id, onSubmitSuccess, resetOnSubmit } = props; - - const crudContext = React.useContext(CrudContext); - const dataSource = (props.dataSource ?? crudContext.dataSource) as Exclude< - typeof props.dataSource, - undefined - >; - - invariant(dataSource, 'No data source found.'); - - const { fields, ...methods } = dataSource; - const { getOne, updateOne } = methods; - - const handleEdit = React.useCallback( - async (formValues: Partial>) => { - await updateOne(id, formValues); - }, - [id, updateOne], - ); - - const [data, setData] = React.useState(null); - const [isLoading, setIsLoading] = React.useState(false); - const [error, setError] = React.useState(null); - - const loadData = React.useCallback(async () => { - setError(null); - setIsLoading(true); - try { - const showData = await getOne(id); - setData(showData); - } catch (showDataError) { - setError(showDataError as Error); - } - setIsLoading(false); - }, [getOne, id]); - - React.useEffect(() => { - loadData(); - }, [loadData]); - - const renderEdit = React.useMemo(() => { - if (isLoading) { - return ( - - - - ); - } - if (error) { - return ( - - {error.message} - - ); - } - - return data ? ( - - ) : null; - }, [data, dataSource, error, handleEdit, isLoading, onSubmitSuccess, resetOnSubmit]); - - return {renderEdit}; -} - -Edit.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side data source. - */ - dataSource: PropTypes.object, - /** - * @ignore - */ - id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - /** - * Callback fired when the form is successfully submitted. - */ - onSubmitSuccess: PropTypes.func, - /** - * Whether the form fields should reset after the form is submitted. - */ - resetOnSubmit: PropTypes.bool, -} as any; - -export { Edit }; diff --git a/packages/toolpad-core/src/CRUD/FormPage.tsx b/packages/toolpad-core/src/CRUD/FormPage.tsx deleted file mode 100644 index 1d5e924a4e9..00000000000 --- a/packages/toolpad-core/src/CRUD/FormPage.tsx +++ /dev/null @@ -1,376 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Checkbox from '@mui/material/Checkbox'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormGroup from '@mui/material/FormGroup'; -import FormHelperText from '@mui/material/FormHelperText'; -import Grid from '@mui/material/Grid2'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; -import TextField from '@mui/material/TextField'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import type { GridSingleSelectColDef } from '@mui/x-data-grid'; -import dayjs, { Dayjs } from 'dayjs'; -import { useNotifications } from '../useNotifications'; -import { DataField, DataModel, DataSource, OmitId } from './shared'; - -interface FormPageLocaleText { - submitButtonLabel: string; - submitSuccessMessage: string; - submitErrorMessage: string; -} - -export interface FormPageProps { - dataSource: - | (DataSource & Required, 'createOne'>>) - | (DataSource & Required, 'updateOne'>>); - initialValues?: Partial>; - onSubmit: (formValues: Partial>) => void | Promise; - onSubmitSuccess?: () => void; - resetOnSubmit?: boolean; - localeText: FormPageLocaleText; -} - -/** - * @ignore - internal component. - */ -function FormPage(props: FormPageProps) { - const { - dataSource, - initialValues = {} as Partial>, - onSubmit, - onSubmitSuccess, - resetOnSubmit = true, - localeText, - } = props; - const { fields, validate } = dataSource; - - const notifications = useNotifications(); - - const [formState, setFormState] = React.useState<{ - values: Partial>; - errors: Partial>; - }>({ - values: { - ...Object.fromEntries( - fields - .filter(({ field }) => field !== 'id') - .map(({ field, type }) => [ - field, - type === 'boolean' ? (initialValues[field] ?? false) : initialValues[field], - ]), - ), - ...initialValues, - }, - errors: {}, - }); - const formValues = formState.values; - const formErrors = formState.errors; - - const setFormValues = React.useCallback((newFormValues: Partial>) => { - setFormState((previousState) => ({ - ...previousState, - values: newFormValues, - })); - }, []); - - const setFormErrors = React.useCallback((newFormErrors: Partial>) => { - setFormState((previousState) => ({ - ...previousState, - errors: newFormErrors, - })); - }, []); - - const handleFormFieldChange = React.useCallback( - (name: keyof D, value: string | number | boolean | File | null) => { - const validateField = async (values: Partial>) => { - if (validate) { - const errors = await validate(values); - setFormErrors({ ...formErrors, [name]: errors[name] }); - } - }; - - const newFormValues = { ...formValues, [name]: value }; - - setFormValues(newFormValues); - validateField(newFormValues); - }, - [formErrors, formValues, setFormErrors, setFormValues, validate], - ); - - const handleFormReset = React.useCallback(() => { - setFormValues(initialValues); - }, [initialValues, setFormValues]); - - const [hasSubmittedSuccessfully, setHasSubmittedSuccessfully] = React.useState(false); - - const [, submitAction, isSubmitting] = React.useActionState(async () => { - if (validate) { - const errors = await validate(formValues); - if (Object.keys(errors).length > 0) { - setFormErrors(errors); - return new Error('Form validation failed'); - } - } - setFormErrors({}); - - try { - await onSubmit(formValues); - notifications.show(localeText.submitSuccessMessage, { - severity: 'success', - autoHideDuration: 3000, - }); - - if (onSubmitSuccess) { - onSubmitSuccess(); - } - - if (resetOnSubmit) { - handleFormReset(); - } - - setHasSubmittedSuccessfully(true); - } catch (createError) { - notifications.show(`${localeText.submitErrorMessage}\n${(createError as Error).message}`, { - severity: 'error', - autoHideDuration: 3000, - }); - return createError as Error; - } - - return null; - }, null); - - const handleTextFieldChange = React.useCallback( - (event: React.ChangeEvent) => { - handleFormFieldChange(event.target.name, event.target.value); - }, - [handleFormFieldChange], - ); - - const handleNumberFieldChange = React.useCallback( - (event: React.ChangeEvent) => { - handleFormFieldChange(event.target.name, Number(event.target.value)); - }, - [handleFormFieldChange], - ); - - const handleCheckboxFieldChange = React.useCallback( - (event: React.ChangeEvent, checked: boolean) => { - handleFormFieldChange(event.target.name, checked); - }, - [handleFormFieldChange], - ); - - const handleDateFieldChange = React.useCallback( - (name: string) => (value: Dayjs | null) => { - if (value?.isValid()) { - handleFormFieldChange(name, value.toISOString() ?? null); - } else if (formValues[name]) { - handleFormFieldChange(name, null); - } - }, - [formValues, handleFormFieldChange], - ); - - const handleSelectFieldChange = React.useCallback( - (event: SelectChangeEvent) => { - handleFormFieldChange(event.target.name, event.target.value); - }, - [handleFormFieldChange], - ); - - const renderField = React.useCallback( - (formField: DataField) => { - const { field, type, headerName } = formField; - - const fieldValue = formValues[field]; - const fieldError = formErrors[field]; - - let fieldElement: React.ReactNode = null; - if (!type || type === 'string' || type === 'longString') { - fieldElement = ( - - ); - } - if (type === 'number') { - fieldElement = ( - - ); - } - if (type === 'boolean') { - fieldElement = ( - - - } - label={headerName} - /> - {fieldError ?? ' '} - - ); - } - if (type === 'date') { - fieldElement = ( - - - - ); - } - if (type === 'dateTime') { - fieldElement = ( - - - - ); - } - if (type === 'singleSelect') { - const { getOptionValue, getOptionLabel, valueOptions } = - formField as GridSingleSelectColDef; - - if (valueOptions && Array.isArray(valueOptions)) { - const labelId = `${field}-label`; - - fieldElement = ( - - {headerName} - - {fieldError ?? ' '} - - ); - } - } - - return ( - - {fieldElement} - - ); - }, - [ - formErrors, - formValues, - handleCheckboxFieldChange, - handleDateFieldChange, - handleNumberFieldChange, - handleSelectFieldChange, - handleTextFieldChange, - ], - ); - - return ( - - - - {fields.filter(({ field }) => field !== 'id').map(renderField)} - - - - - - - ); -} - -FormPage.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side data source. - */ - dataSource: PropTypes.object.isRequired, -} as any; - -export { FormPage }; diff --git a/packages/toolpad-core/src/CRUD/List.tsx b/packages/toolpad-core/src/CRUD/List.tsx deleted file mode 100644 index d24b93b8e4d..00000000000 --- a/packages/toolpad-core/src/CRUD/List.tsx +++ /dev/null @@ -1,450 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { styled } from '@mui/material'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import IconButton from '@mui/material/IconButton'; -import Stack from '@mui/material/Stack'; -import Tooltip from '@mui/material/Tooltip'; -import Typography from '@mui/material/Typography'; -import { - DataGrid, - GridToolbar, - GridActionsCellItem, - DataGridProps, - GridColDef, - GridFilterModel, - GridPaginationModel, - GridSortModel, - GridEventListener, - gridClasses, -} from '@mui/x-data-grid'; -import type { DataGridProProps } from '@mui/x-data-grid-pro'; -import type { DataGridPremiumProps } from '@mui/x-data-grid-premium'; -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'; -import { CrudContext, RouterContext, WindowContext } from '../shared/context'; - -const ErrorOverlay = styled('div')(({ theme }) => ({ - position: 'absolute', - backgroundColor: theme.palette.error.light, - borderRadius: '4px', - top: 0, - height: '100%', - width: '100%', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - textAlign: 'center', - p: 1, - zIndex: 10, -})); - -export interface ListSlotProps { - dataGrid?: Partial; -} - -export interface ListSlots { - /** - * The DataGrid component used to list the items. - * @default DataGrid - */ - dataGrid?: - | React.JSXElementConstructor - | React.JSXElementConstructor - | React.JSXElementConstructor; -} - -export interface ListProps { - /** - * Server-side data source. - */ - dataSource?: DataSource & Required, 'getMany'>>; - /** - * Initial number of rows to show per page. - * @default 100 - */ - initialPageSize?: number; - /** - * Callback fired when a row is clicked. Not called if the target clicked is an interactive element added by the built-in columns. - */ - onRowClick?: (id: DataModelId) => void; - /** - * Callback fired when the "Create" button is clicked. - */ - onCreateClick?: () => void; - /** - * Callback fired when the "Edit" button is clicked. - */ - onEditClick?: (id: DataModelId) => void; - /** - * Callback fired when the item is successfully deleted. - */ - onDelete?: (id: DataModelId) => void; - /** - * The components used for each slot inside. - * @default {} - */ - slots?: ListSlots; - /** - * The props used for each slot inside. - * @default {} - */ - slotProps?: ListSlotProps; -} -/** - * - * Demos: - * - * - [Crud](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [List API](https://mui.com/toolpad/core/api/list) - */ -function List(props: ListProps) { - const { - initialPageSize = 100, - onRowClick, - onCreateClick, - onEditClick, - onDelete, - slots, - slotProps, - } = props; - - const crudContext = React.useContext(CrudContext); - 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; - - const routerContext = React.useContext(RouterContext); - const appWindowContext = React.useContext(WindowContext); - - const appWindow = appWindowContext ?? (typeof window !== 'undefined' ? window : null); - - const dialogs = useDialogs(); - const notifications = useNotifications(); - - const [rowsState, setRowsState] = React.useState<{ rows: D[]; rowCount: number }>({ - rows: [], - rowCount: 0, - }); - - const [paginationModel, setPaginationModel] = React.useState({ - page: routerContext?.searchParams.get('page') - ? Number(routerContext?.searchParams.get('page')) - : 0, - pageSize: routerContext?.searchParams.get('pageSize') - ? Number(routerContext?.searchParams.get('pageSize')) - : initialPageSize, - }); - const [filterModel, setFilterModel] = React.useState( - routerContext?.searchParams.get('filter') - ? JSON.parse(routerContext?.searchParams.get('filter') ?? '') - : { items: [] }, - ); - const [sortModel, setSortModel] = React.useState( - routerContext?.searchParams.get('sort') - ? JSON.parse(routerContext?.searchParams.get('sort') ?? '') - : [], - ); - - const [isLoading, setIsLoading] = React.useState(true); - const [error, setError] = React.useState(null); - - React.useEffect(() => { - if (appWindow) { - const url = new URL(appWindow.location.href); - - url.searchParams.set('page', String(paginationModel.page)); - url.searchParams.set('pageSize', String(paginationModel.pageSize)); - - if (!appWindow.frameElement) { - appWindow.history.pushState({}, '', url); - } - } - }, [appWindow, paginationModel.page, paginationModel.pageSize]); - - React.useEffect(() => { - if (appWindow) { - const url = new URL(appWindow.location.href); - - if ( - filterModel.items.length > 0 || - (filterModel.quickFilterValues && filterModel.quickFilterValues.length > 0) - ) { - url.searchParams.set('filter', JSON.stringify(filterModel)); - } else { - url.searchParams.delete('filter'); - } - - if (!appWindow.frameElement) { - appWindow.history.pushState({}, '', url); - } - } - }, [appWindow, filterModel]); - - React.useEffect(() => { - if (appWindow) { - const url = new URL(appWindow.location.href); - - if (sortModel.length > 0) { - url.searchParams.set('sort', JSON.stringify(sortModel)); - } else { - url.searchParams.delete('sort'); - } - - if (!appWindow.frameElement) { - appWindow.history.pushState({}, '', url); - } - } - }, [appWindow, sortModel]); - - const loadData = React.useCallback(async () => { - setError(null); - setIsLoading(true); - try { - const listData = await getMany({ - paginationModel, - sortModel, - filterModel, - }); - setRowsState({ - rows: listData.items, - rowCount: listData.itemCount, - }); - } catch (listDataError) { - setError(listDataError as Error); - } - setIsLoading(false); - }, [filterModel, getMany, paginationModel, sortModel]); - - React.useEffect(() => { - loadData(); - }, [filterModel, getMany, loadData, paginationModel, sortModel]); - - const handleRefresh = React.useCallback(() => { - if (!isLoading) { - loadData(); - } - }, [isLoading, loadData]); - - const handleRowClick = React.useCallback>( - ({ row }) => { - if (onRowClick) { - onRowClick(row.id); - } - }, - [onRowClick], - ); - - const handleItemEdit = React.useCallback( - (itemId: DataModelId) => () => { - if (onEditClick) { - onEditClick(itemId); - } - }, - [onEditClick], - ); - - const handleItemDelete = React.useCallback( - (itemId: DataModelId) => async () => { - const confirmed = await dialogs.confirm(`Do you wish to delete item "${itemId}"?`, { - title: 'Delete item?', - severity: 'error', - okText: 'Delete', - cancelText: 'Cancel', - }); - - if (confirmed) { - setIsLoading(true); - try { - await deleteOne?.(itemId); - - if (onDelete) { - onDelete(itemId); - } - - notifications.show('Item deleted successfully.', { - severity: 'success', - autoHideDuration: 3000, - }); - loadData(); - } catch (deleteError) { - notifications.show(`Failed to delete item. Reason: ${(deleteError as Error).message}`, { - severity: 'error', - autoHideDuration: 3000, - }); - } - setIsLoading(false); - } - }, - [deleteOne, dialogs, loadData, notifications, onDelete], - ); - - const DataGridSlot = slots?.dataGrid ?? DataGrid; - - const initialState = React.useMemo( - () => ({ - pagination: { paginationModel: { pageSize: initialPageSize } }, - }), - [initialPageSize], - ); - - const columns = React.useMemo(() => { - return [ - ...(fields.map((field) => - field.type === 'longString' ? { ...field, type: 'string' } : field, - ) as GridColDef[]), - { - field: 'actions', - type: 'actions', - flex: 1, - align: 'right', - getActions: ({ id }) => [ - ...(onEditClick - ? [ - } - label="Edit" - onClick={handleItemEdit(id)} - />, - ] - : []), - ...(deleteOne - ? [ - } - label="Delete" - onClick={handleItemDelete(id)} - />, - ] - : []), - ], - }, - ]; - }, [deleteOne, fields, handleItemDelete, handleItemEdit, onEditClick]); - - return ( - - - -
- - - -
-
- {onCreateClick ? ( - - ) : null} -
- - )} - sx={{ - [`& .${gridClasses.columnHeader}, & .${gridClasses.cell}`]: { - outline: 'transparent', - }, - [`& .${gridClasses.columnHeader}:focus-within, & .${gridClasses.cell}:focus-within`]: { - outline: 'none', - }, - [`& .${gridClasses.row}:hover`]: { - cursor: 'pointer', - }, - ...slotProps?.dataGrid?.sx, - }} - /> - {error && ( - - {error.message} - - )} - -
- ); -} - -List.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side data source. - */ - dataSource: PropTypes.object, - /** - * Initial number of rows to show per page. - * @default 100 - */ - initialPageSize: PropTypes.number, - /** - * Callback fired when the "Create" button is clicked. - */ - onCreateClick: PropTypes.func, - /** - * Callback fired when the item is successfully deleted. - */ - onDelete: PropTypes.func, - /** - * Callback fired when the "Edit" button is clicked. - */ - onEditClick: PropTypes.func, - /** - * 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 props used for each slot inside. - * @default {} - */ - slotProps: PropTypes.shape({ - dataGrid: PropTypes.object, - }), - /** - * The components used for each slot inside. - * @default {} - */ - slots: PropTypes.shape({ - dataGrid: PropTypes.func, - }), -} as any; - -export { List }; diff --git a/packages/toolpad-core/src/CRUD/Show.tsx b/packages/toolpad-core/src/CRUD/Show.tsx deleted file mode 100644 index adc0def957a..00000000000 --- a/packages/toolpad-core/src/CRUD/Show.tsx +++ /dev/null @@ -1,262 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Alert from '@mui/material/Alert'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import CircularProgress from '@mui/material/CircularProgress'; -import Divider from '@mui/material/Divider'; -import Grid from '@mui/material/Grid2'; -import Paper from '@mui/material/Paper'; -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 dayjs from 'dayjs'; -import { useDialogs } from '../useDialogs'; -import { useNotifications } from '../useNotifications'; -import { DataField, DataModel, DataModelId, DataSource } from './shared'; -import { CrudContext } from '../shared/context'; - -export interface ShowProps { - id: DataModelId; - /** - * Server-side data source. - */ - dataSource?: DataSource & Required, 'getOne'>>; - /** - * Callback fired when the "Edit" button is clicked. - */ - onEditClick?: (id: DataModelId) => void; - /** - * Callback fired when the item is successfully deleted. - */ - onDelete?: (id: DataModelId) => void; -} -/** - * - * Demos: - * - * - [Crud](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [Show API](https://mui.com/toolpad/core/api/show) - */ -function Show(props: ShowProps) { - const { id, onEditClick, onDelete } = props; - - const crudContext = React.useContext(CrudContext); - 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; - - const dialogs = useDialogs(); - const notifications = useNotifications(); - - const [data, setData] = React.useState(null); - const [isLoading, setIsLoading] = React.useState(false); - const [error, setError] = React.useState(null); - - const [hasDeleted, setHasDeleted] = React.useState(false); - - const loadData = React.useCallback(async () => { - setError(null); - setIsLoading(true); - try { - const showData = await getOne(id); - setData(showData); - } catch (showDataError) { - setError(showDataError as Error); - } - setIsLoading(false); - }, [getOne, id]); - - React.useEffect(() => { - loadData(); - }, [loadData]); - - const handleItemEdit = React.useCallback(() => { - if (onEditClick) { - onEditClick(id); - } - }, [id, onEditClick]); - - const handleItemDelete = React.useCallback(async () => { - const confirmed = await dialogs.confirm(`Do you wish to delete item "${id}"?`, { - title: 'Delete item?', - severity: 'error', - okText: 'Delete', - cancelText: 'Cancel', - }); - - if (confirmed) { - setIsLoading(true); - try { - await deleteOne?.(id); - - if (onDelete) { - onDelete(id); - } - - notifications.show('Item deleted successfully.', { - severity: 'success', - autoHideDuration: 3000, - }); - - setHasDeleted(true); - } catch (deleteError) { - notifications.show(`Failed to delete item. Reason: ${(deleteError as Error).message}`, { - severity: 'error', - autoHideDuration: 3000, - }); - } - setIsLoading(false); - } - }, [deleteOne, dialogs, id, notifications, onDelete]); - - const renderField = React.useCallback( - (showField: DataField) => { - if (!data) { - return '…'; - } - - const { field, type } = showField; - const fieldValue = data[field]; - - if (type === 'boolean') { - return fieldValue ? 'Yes' : 'No'; - } - if (type === 'date') { - return fieldValue ? dayjs(fieldValue as string).format('MMMM D, YYYY') : '-'; - } - if (type === 'dateTime') { - return fieldValue ? dayjs(fieldValue as string).format('MMMM D, YYYY h:mm A') : '-'; - } - - return fieldValue ? String(fieldValue) : '-'; - }, - [data], - ); - - const renderShow = React.useMemo(() => { - if (isLoading) { - return ( - - - - ); - } - if (error) { - return ( - - {error.message} - - ); - } - - if (hasDeleted) { - return ( - - This item has been deleted. - - ); - } - - return data ? ( - - - {fields - .filter(({ type }) => type !== 'actions' && type !== 'custom') - .map((showField) => { - const { field, headerName } = showField; - - return ( - - - {headerName} - - {renderField(showField)} - - - - ); - })} - - - - {onEditClick ? ( - - ) : null} - {deleteOne ? ( - - ) : null} - - - ) : null; - }, [ - data, - deleteOne, - error, - fields, - handleItemDelete, - handleItemEdit, - hasDeleted, - isLoading, - onEditClick, - renderField, - ]); - - return {renderShow}; -} - -Show.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side data source. - */ - dataSource: PropTypes.object, - /** - * @ignore - */ - id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - /** - * Callback fired when the item is successfully deleted. - */ - onDelete: PropTypes.func, - /** - * Callback fired when the "Edit" button is clicked. - */ - onEditClick: PropTypes.func, -} as any; - -export { Show }; diff --git a/packages/toolpad-core/src/CRUD/index.ts b/packages/toolpad-core/src/CRUD/index.ts deleted file mode 100644 index a3cce1a8c68..00000000000 --- a/packages/toolpad-core/src/CRUD/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './List'; -export * from './Show'; -export * from './Create'; -export * from './Edit'; - -export * from './CrudProvider'; - -export * from './Crud'; - -export * from './shared'; diff --git a/packages/toolpad-core/src/CRUD/shared.ts b/packages/toolpad-core/src/CRUD/shared.ts deleted file mode 100644 index b6209b50774..00000000000 --- a/packages/toolpad-core/src/CRUD/shared.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - GridColDef, - GridColType, - 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; -} - -type RemappedOmit = { [P in keyof T as P extends K ? never : P]: T[P] }; - -export type OmitId = RemappedOmit; - -export type DataField = RemappedOmit & { type?: GridColType | 'longString' }; - -export interface DataSource { - fields: DataField[]; - getMany?: (params: GetManyParams) => Promise<{ items: D[]; itemCount: number }>; - getOne?: (id: DataModelId) => Promise; - createOne?: (data: Partial>) => Promise; - updateOne?: (id: DataModelId, data: Partial>) => Promise; - deleteOne?: (id: DataModelId) => Promise; - /** - * Function to validate form values. Returns object with error strings for each field. - */ - validate?: ( - formValues: Partial>, - ) => Partial> | Promise>>; -}