From bded30b4a218e3f29235365790a33e4f16391c8a Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Tue, 26 May 2020 12:10:49 +0200 Subject: [PATCH] Add ability to create custom DeleteButton views without rewriting the logic --- .../ra-core/src/controller/button/index.ts | 4 + .../button/useDeleteWithConfirmController.tsx | 140 ++++++++++++++++++ .../button/useDeleteWithUndoController.tsx | 105 +++++++++++++ packages/ra-core/src/controller/index.ts | 1 + .../ra-ui-materialui/src/button/Button.tsx | 1 - .../src/button/DeleteWithConfirmButton.tsx | 69 +++------ .../src/button/DeleteWithUndoButton.tsx | 54 ++----- 7 files changed, 282 insertions(+), 92 deletions(-) create mode 100644 packages/ra-core/src/controller/button/index.ts create mode 100644 packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx create mode 100644 packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx diff --git a/packages/ra-core/src/controller/button/index.ts b/packages/ra-core/src/controller/button/index.ts new file mode 100644 index 00000000000..313e91771cd --- /dev/null +++ b/packages/ra-core/src/controller/button/index.ts @@ -0,0 +1,4 @@ +import useDeleteWithUndoController from './useDeleteWithUndoController'; +import useDeleteWithConfirmController from './useDeleteWithConfirmController'; + +export { useDeleteWithUndoController, useDeleteWithConfirmController }; diff --git a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx new file mode 100644 index 00000000000..197c542f7a4 --- /dev/null +++ b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx @@ -0,0 +1,140 @@ +import { + useState, + useCallback, + ReactEventHandler, + SyntheticEvent, +} from 'react'; +import { useDelete } from '../../dataProvider'; +import { CRUD_DELETE } from '../../actions'; +import { + useRefresh, + useNotify, + useRedirect, + RedirectionSideEffect, +} from '../../sideEffect'; +import { Record } from '../../types'; + +/** + * Prepare a set of callbacks for a delete button guarded by confirmation dialog + * + * @example + * + * const DeleteButton = ({ + * resource, + * record, + * basePath, + * redirect, + * onClick, + * ...rest + * }) => { + * const { + * open, + * loading, + * handleDialogOpen, + * handleDialogClose, + * handleDelete, + * } = useDeleteWithConfirmController({ + * resource, + * record, + * redirect, + * basePath, + * onClick, + * }); + * + * return ( + * + * + * + * + * ); + * }; + */ +const useDeleteWithConfirmController = ({ + resource, + record, + redirect: redirectTo, + basePath, + onClick, +}: UseDeleteWithConfirmControllerParams): UseDeleteWithConfirmControllerReturn => { + const [open, setOpen] = useState(false); + const notify = useNotify(); + const redirect = useRedirect(); + const refresh = useRefresh(); + const [deleteOne, { loading }] = useDelete(resource, null, null, { + action: CRUD_DELETE, + onSuccess: () => { + notify('ra.notification.deleted', 'info', { smart_count: 1 }); + redirect(redirectTo, basePath); + refresh(); + }, + onFailure: error => { + notify( + typeof error === 'string' + ? error + : error.message || 'ra.notification.http_error', + 'warning' + ); + setOpen(false); + }, + undoable: false, + }); + + const handleDialogOpen = e => { + setOpen(true); + e.stopPropagation(); + }; + + const handleDialogClose = e => { + setOpen(false); + e.stopPropagation(); + }; + + const handleDelete = useCallback( + event => { + deleteOne({ + payload: { id: record.id, previousData: record }, + }); + if (typeof onClick === 'function') { + onClick(event); + } + }, + [deleteOne, onClick, record] + ); + + return { open, loading, handleDialogOpen, handleDialogClose, handleDelete }; +}; + +export interface UseDeleteWithConfirmControllerParams { + basePath?: string; + record?: Record; + redirect?: RedirectionSideEffect; + resource: string; + onClick?: ReactEventHandler; +} + +export interface UseDeleteWithConfirmControllerReturn { + open: boolean; + loading: boolean; + handleDialogOpen: (e: SyntheticEvent) => void; + handleDialogClose: (e: SyntheticEvent) => void; + handleDelete: ReactEventHandler; +} + +export default useDeleteWithConfirmController; diff --git a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx new file mode 100644 index 00000000000..e027cc0a99f --- /dev/null +++ b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx @@ -0,0 +1,105 @@ +import { useCallback, ReactEventHandler } from 'react'; +import { useDelete } from '../../dataProvider'; +import { CRUD_DELETE } from '../../actions'; +import { + useRefresh, + useNotify, + useRedirect, + RedirectionSideEffect, +} from '../../sideEffect'; +import { Record } from '../../types'; + +/** + * Prepare callback for a Delete button with undo support + * + * @example + * + * import React from 'react'; + * import ActionDelete from '@material-ui/icons/Delete'; + * import { Button, useDeleteWithUndoController } from 'react-admin'; + * + * const DeleteButton = ({ + * resource, + * record, + * basePath, + * redirect, + * onClick, + * ...rest + * }) => { + * const { loading, handleDelete } = useDeleteWithUndoController({ + * resource, + * record, + * basePath, + * redirect, + * onClick, + * }); + * + * return ( + * + * ); + * }; + */ +const useDeleteWithUndoController = ({ + resource, + record, + basePath, + redirect: redirectTo = 'list', + onClick, +}: UseDeleteWithUndoControllerParams): UseDeleteWithUndoControllerReturn => { + const notify = useNotify(); + const redirect = useRedirect(); + const refresh = useRefresh(); + + const [deleteOne, { loading }] = useDelete(resource, null, null, { + action: CRUD_DELETE, + onSuccess: () => { + notify('ra.notification.deleted', 'info', { smart_count: 1 }, true); + redirect(redirectTo, basePath); + refresh(); + }, + onFailure: error => + notify( + typeof error === 'string' + ? error + : error.message || 'ra.notification.http_error', + 'warning' + ), + undoable: true, + }); + const handleDelete = useCallback( + event => { + event.stopPropagation(); + deleteOne({ + payload: { id: record.id, previousData: record }, + }); + if (typeof onClick === 'function') { + onClick(event); + } + }, + [deleteOne, onClick, record] + ); + + return { loading, handleDelete }; +}; + +export interface UseDeleteWithUndoControllerParams { + basePath?: string; + record?: Record; + redirect?: RedirectionSideEffect; + resource: string; + onClick?: ReactEventHandler; +} + +export interface UseDeleteWithUndoControllerReturn { + loading: boolean; + handleDelete: ReactEventHandler; +} + +export default useDeleteWithUndoController; diff --git a/packages/ra-core/src/controller/index.ts b/packages/ra-core/src/controller/index.ts index 192538241b1..43353287dac 100644 --- a/packages/ra-core/src/controller/index.ts +++ b/packages/ra-core/src/controller/index.ts @@ -46,3 +46,4 @@ export { export * from './field'; export * from './input'; +export * from './button'; diff --git a/packages/ra-ui-materialui/src/button/Button.tsx b/packages/ra-ui-materialui/src/button/Button.tsx index 40356ae7699..5376e9d608d 100644 --- a/packages/ra-ui-materialui/src/button/Button.tsx +++ b/packages/ra-ui-materialui/src/button/Button.tsx @@ -137,7 +137,6 @@ interface Props { label?: string; size?: 'small' | 'medium' | 'large'; icon?: ReactElement; - onClick?: (e: MouseEvent) => void; redirect?: RedirectionSideEffect; variant?: string; // May be injected by Toolbar diff --git a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx index 9083c38f7fd..a7f0c506944 100644 --- a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx +++ b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx @@ -1,7 +1,6 @@ import React, { Fragment, - useState, - useCallback, + ReactEventHandler, FC, ReactElement, SyntheticEvent, @@ -14,13 +13,9 @@ import classnames from 'classnames'; import inflection from 'inflection'; import { useTranslate, - useDelete, - useRefresh, - useNotify, - useRedirect, - CRUD_DELETE, Record, RedirectionSideEffect, + useDeleteWithConfirmController, } from 'ra-core'; import Confirm from '../layout/Confirm'; @@ -38,59 +33,29 @@ const DeleteWithConfirmButton: FC = props => { onClick, record, resource, - redirect: redirectTo = 'list', + redirect = 'list', ...rest } = props; - const [open, setOpen] = useState(false); const translate = useTranslate(); - const notify = useNotify(); - const redirect = useRedirect(); - const refresh = useRefresh(); const classes = useStyles(props); - - const [deleteOne, { loading }] = useDelete(resource, record.id, record, { - action: CRUD_DELETE, - onSuccess: () => { - notify('ra.notification.deleted', 'info', { smart_count: 1 }); - redirect(redirectTo, basePath); - refresh(); - }, - onFailure: error => { - notify( - typeof error === 'string' - ? error - : error.message || 'ra.notification.http_error', - 'warning' - ); - setOpen(false); - }, - undoable: false, + const { + open, + loading, + handleDialogOpen, + handleDialogClose, + handleDelete, + } = useDeleteWithConfirmController({ + resource, + record, + redirect, + basePath, + onClick, }); - const handleClick = e => { - setOpen(true); - e.stopPropagation(); - }; - - const handleDialogClose = e => { - setOpen(false); - e.stopPropagation(); - }; - - const handleDelete = useCallback( - event => { - deleteOne(); - if (typeof onClick === 'function') { - onClick(event); - } - }, - [deleteOne, onClick] - ); - return (