Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to create custom DeleteButton views without rewriting the logic #4858

Merged
merged 1 commit into from
May 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -46,3 +46,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