Skip to content

Commit

Permalink
Merge pull request #4858 from marmelab/delete-button-hooks
Browse files Browse the repository at this point in the history
Add ability to create custom DeleteButton views without rewriting the logic
  • Loading branch information
djhi authored May 27, 2020
2 parents 060e264 + bded30b commit 48c3a66
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 92 deletions.
4 changes: 4 additions & 0 deletions packages/ra-core/src/controller/button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import useDeleteWithUndoController from './useDeleteWithUndoController';
import useDeleteWithConfirmController from './useDeleteWithConfirmController';

export { useDeleteWithUndoController, useDeleteWithConfirmController };
Original file line number Diff line number Diff line change
@@ -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 (
* <Fragment>
* <Button
* onClick={handleDialogOpen}
* label="ra.action.delete"
* {...rest}
* >
* {icon}
* </Button>
* <Confirm
* isOpen={open}
* loading={loading}
* title="ra.message.delete_title"
* content="ra.message.delete_content"
* translateOptions={{
* name: resource,
* id: record.id,
* }}
* onConfirm={handleDelete}
* onClose={handleDialogClose}
* />
* </Fragment>
* );
* };
*/
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<any>;
}

export interface UseDeleteWithConfirmControllerReturn {
open: boolean;
loading: boolean;
handleDialogOpen: (e: SyntheticEvent) => void;
handleDialogClose: (e: SyntheticEvent) => void;
handleDelete: ReactEventHandler<any>;
}

export default useDeleteWithConfirmController;
105 changes: 105 additions & 0 deletions packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
Original file line number Diff line number Diff line change
@@ -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 (
* <Button
* onClick={handleDelete}
* disabled={loading}
* label="ra.action.delete"
* {...rest}
* >
* <ActionDelete />
* </Button>
* );
* };
*/
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<any>;
}

export interface UseDeleteWithUndoControllerReturn {
loading: boolean;
handleDelete: ReactEventHandler<any>;
}

export default useDeleteWithUndoController;
1 change: 1 addition & 0 deletions packages/ra-core/src/controller/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ export {

export * from './field';
export * from './input';
export * from './button';
1 change: 0 additions & 1 deletion packages/ra-ui-materialui/src/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 17 additions & 52 deletions packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, {
Fragment,
useState,
useCallback,
ReactEventHandler,
FC,
ReactElement,
SyntheticEvent,
Expand All @@ -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';
Expand All @@ -38,59 +33,29 @@ const DeleteWithConfirmButton: FC<DeleteWithConfirmButtonProps> = 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 (
<Fragment>
<Button
onClick={handleClick}
onClick={handleDialogOpen}
label={label}
className={classnames(
'ra-delete-button',
Expand Down Expand Up @@ -150,7 +115,7 @@ interface Props {
confirmContent?: string;
icon?: ReactElement;
label?: string;
onClick?: (e: MouseEvent) => void;
onClick?: ReactEventHandler<any>;
record?: Record;
redirect?: RedirectionSideEffect;
resource?: string;
Expand Down
Loading

0 comments on commit 48c3a66

Please sign in to comment.