From 357b557811370969faee91fed6b1667e163fd7b3 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 1 Aug 2025 11:40:24 +0200
Subject: [PATCH 1/6] Introduce `useDeleteController`
---
.../ra-core/src/controller/button/index.ts | 1 +
.../controller/button/useDeleteController.tsx | 184 ++++++++++++++++++
.../button/useDeleteWithConfirmController.tsx | 184 +++++++-----------
.../button/useDeleteWithUndoController.tsx | 135 +++----------
.../src/button/DeleteWithConfirmButton.tsx | 102 +++++++---
.../src/button/DeleteWithUndoButton.tsx | 31 ++-
6 files changed, 371 insertions(+), 266 deletions(-)
create mode 100644 packages/ra-core/src/controller/button/useDeleteController.tsx
diff --git a/packages/ra-core/src/controller/button/index.ts b/packages/ra-core/src/controller/button/index.ts
index 313e91771cd..926e94e5424 100644
--- a/packages/ra-core/src/controller/button/index.ts
+++ b/packages/ra-core/src/controller/button/index.ts
@@ -2,3 +2,4 @@ import useDeleteWithUndoController from './useDeleteWithUndoController';
import useDeleteWithConfirmController from './useDeleteWithConfirmController';
export { useDeleteWithUndoController, useDeleteWithConfirmController };
+export * from './useDeleteController';
diff --git a/packages/ra-core/src/controller/button/useDeleteController.tsx b/packages/ra-core/src/controller/button/useDeleteController.tsx
new file mode 100644
index 00000000000..325694adc6b
--- /dev/null
+++ b/packages/ra-core/src/controller/button/useDeleteController.tsx
@@ -0,0 +1,184 @@
+import { useCallback } from 'react';
+import { UseMutationOptions } from '@tanstack/react-query';
+
+import { useDelete } from '../../dataProvider';
+import { useUnselect } from '../';
+import { useRedirect, RedirectionSideEffect } from '../../routing';
+import { useNotify } from '../../notification';
+import { RaRecord, MutationMode, DeleteParams } from '../../types';
+import { useResourceContext } from '../../core';
+import { useTranslate } from '../../i18n';
+
+/**
+ * Prepare a set of callbacks for a delete button guarded by confirmation dialog
+ *
+ * @example
+ *
+ * const DeleteButton = ({
+ * resource,
+ * record,
+ * redirect,
+ * onClick,
+ * ...rest
+ * }) => {
+ * const {
+ * open,
+ * isPending,
+ * handleDialogOpen,
+ * handleDialogClose,
+ * handleDelete,
+ * } = useDeleteWithConfirmController({
+ * resource,
+ * record,
+ * redirect,
+ * onClick,
+ * });
+ *
+ * return (
+ *
+ *
+ *
+ *
+ * );
+ * };
+ */
+export const useDeleteController = <
+ RecordType extends RaRecord = any,
+ ErrorType = Error,
+>(
+ props: UseDeleteControllerParams
+): UseDeleteControllerReturn => {
+ const { mutationMode } = props;
+ const {
+ record,
+ redirect: redirectTo = 'list',
+ mutationOptions = {},
+ successMessage,
+ } = props as UseDeleteControllerParams;
+ const { meta: mutationMeta, ...otherMutationOptions } = mutationOptions;
+ const resource = useResourceContext(props);
+ const notify = useNotify();
+ const unselect = useUnselect(resource);
+ const redirect = useRedirect();
+ const translate = useTranslate();
+
+ const [deleteOne, { isPending }] = useDelete(
+ resource,
+ undefined,
+ {
+ onSuccess: () => {
+ notify(
+ successMessage ??
+ `resources.${resource}.notifications.deleted`,
+ {
+ type: 'info',
+ messageArgs: {
+ smart_count: 1,
+ _: translate('ra.notification.deleted', {
+ smart_count: 1,
+ }),
+ },
+ undoable: mutationMode === 'undoable',
+ }
+ );
+ record && unselect([record.id]);
+ redirect(redirectTo, resource);
+ },
+ onError: error => {
+ notify(
+ typeof error === 'string'
+ ? error
+ : (error as Error)?.message ||
+ 'ra.notification.http_error',
+ {
+ type: 'error',
+ messageArgs: {
+ _:
+ typeof error === 'string'
+ ? error
+ : (error as Error)?.message
+ ? (error as Error).message
+ : undefined,
+ },
+ }
+ );
+ },
+ }
+ );
+
+ const handleDelete = useCallback(() => {
+ if (!record) {
+ throw new Error(
+ 'The record cannot be deleted because no record has been passed'
+ );
+ }
+ deleteOne(
+ resource,
+ {
+ id: record.id,
+ previousData: record,
+ meta: mutationMeta,
+ },
+ {
+ mutationMode,
+ ...otherMutationOptions,
+ }
+ );
+ }, [
+ deleteOne,
+ mutationMeta,
+ mutationMode,
+ otherMutationOptions,
+ record,
+ resource,
+ ]);
+
+ return {
+ isPending,
+ isLoading: isPending,
+ handleDelete,
+ };
+};
+
+export interface UseDeleteControllerParams<
+ RecordType extends RaRecord = any,
+ MutationOptionsError = unknown,
+> {
+ mutationMode?: MutationMode;
+ mutationOptions?: UseMutationOptions<
+ RecordType,
+ MutationOptionsError,
+ DeleteParams
+ >;
+ record?: RecordType;
+ redirect?: RedirectionSideEffect;
+ resource?: string;
+ successMessage?: string;
+}
+
+export interface UseDeleteControllerReturn {
+ isLoading: boolean;
+ isPending: boolean;
+ handleDelete: () => void;
+}
diff --git a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
index 15b72f8f8d4..492727c4636 100644
--- a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
+++ b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
@@ -1,18 +1,16 @@
+import { useState, ReactEventHandler, SyntheticEvent } from 'react';
import {
- useState,
- useCallback,
- ReactEventHandler,
- SyntheticEvent,
-} from 'react';
-import { UseMutationOptions } from '@tanstack/react-query';
-
-import { useDelete } from '../../dataProvider';
-import { useUnselect } from '../../controller';
-import { useRedirect, RedirectionSideEffect } from '../../routing';
+ useDeleteController,
+ UseDeleteControllerParams,
+ UseDeleteControllerReturn,
+ useUnselect,
+} from '../';
+import { useRedirect } from '../../routing';
import { useNotify } from '../../notification';
-import { RaRecord, MutationMode, DeleteParams } from '../../types';
+import { RaRecord } from '../../types';
import { useResourceContext } from '../../core';
import { useTranslate } from '../../i18n';
+import { useEvent } from '../../util';
/**
* Prepare a set of callbacks for a delete button guarded by confirmation dialog
@@ -75,112 +73,85 @@ const useDeleteWithConfirmController = <
props: UseDeleteWithConfirmControllerParams
): UseDeleteWithConfirmControllerReturn => {
const {
- record,
- redirect: redirectTo = 'list',
mutationMode,
onClick,
- mutationOptions = {},
+ record,
+ redirect: redirectTo = 'list',
successMessage,
} = props;
- const { meta: mutationMeta, ...otherMutationOptions } = mutationOptions;
- const resource = useResourceContext(props);
const [open, setOpen] = useState(false);
+ const resource = useResourceContext(props);
const notify = useNotify();
const unselect = useUnselect(resource);
const redirect = useRedirect();
const translate = useTranslate();
- const [deleteOne, { isPending }] = useDelete(
- resource,
- undefined,
- {
- onSuccess: () => {
- setOpen(false);
- notify(
- successMessage ??
- `resources.${resource}.notifications.deleted`,
- {
- type: 'info',
- messageArgs: {
- smart_count: 1,
- _: translate('ra.notification.deleted', {
+ const { isPending, handleDelete: controllerHandleDelete } =
+ useDeleteController({
+ mutationOptions: {
+ onSuccess: () => {
+ setOpen(false);
+ notify(
+ successMessage ??
+ `resources.${resource}.notifications.deleted`,
+ {
+ type: 'info',
+ messageArgs: {
smart_count: 1,
- }),
- },
- undoable: mutationMode === 'undoable',
- }
- );
- record && unselect([record.id]);
- redirect(redirectTo, resource);
- },
- onError: error => {
- setOpen(false);
+ _: translate('ra.notification.deleted', {
+ smart_count: 1,
+ }),
+ },
+ undoable: mutationMode === 'undoable',
+ }
+ );
+ record && unselect([record.id]);
+ redirect(redirectTo, resource);
+ },
+ onError: error => {
+ setOpen(false);
- notify(
- typeof error === 'string'
- ? error
- : (error as Error)?.message ||
- 'ra.notification.http_error',
- {
- type: 'error',
- messageArgs: {
- _:
- typeof error === 'string'
- ? error
- : (error as Error)?.message
- ? (error as Error).message
- : undefined,
- },
- }
- );
+ notify(
+ typeof error === 'string'
+ ? error
+ : (error as Error)?.message ||
+ 'ra.notification.http_error',
+ {
+ type: 'error',
+ messageArgs: {
+ _:
+ typeof error === 'string'
+ ? error
+ : (error as Error)?.message
+ ? (error as Error).message
+ : undefined,
+ },
+ }
+ );
+ },
},
- }
- );
+ ...props,
+ });
- const handleDialogOpen = e => {
- setOpen(true);
+ const handleDialogOpen = useEvent((e: any) => {
e.stopPropagation();
- };
+ setOpen(true);
+ });
- const handleDialogClose = e => {
- setOpen(false);
+ const handleDialogClose = useEvent((e: any) => {
e.stopPropagation();
- };
+ setOpen(false);
+ });
- const handleDelete = useCallback(
- event => {
+ const handleDelete = useEvent((event: any) => {
+ if (event && event.stopPropagation) {
event.stopPropagation();
- if (!record) {
- throw new Error(
- 'The record cannot be deleted because no record has been passed'
- );
- }
- deleteOne(
- resource,
- {
- id: record.id,
- previousData: record,
- meta: mutationMeta,
- },
- {
- mutationMode,
- ...otherMutationOptions,
- }
- );
- if (typeof onClick === 'function') {
- onClick(event);
- }
- },
- [
- deleteOne,
- mutationMeta,
- mutationMode,
- otherMutationOptions,
- onClick,
- record,
- resource,
- ]
- );
+ }
+ controllerHandleDelete();
+ if (typeof onClick === 'function') {
+ onClick(event);
+ }
+ });
return {
open,
@@ -195,24 +166,13 @@ const useDeleteWithConfirmController = <
export interface UseDeleteWithConfirmControllerParams<
RecordType extends RaRecord = any,
MutationOptionsError = unknown,
-> {
- mutationMode?: MutationMode;
- record?: RecordType;
- redirect?: RedirectionSideEffect;
- resource?: string;
+> extends UseDeleteControllerParams {
onClick?: ReactEventHandler;
- mutationOptions?: UseMutationOptions<
- RecordType,
- MutationOptionsError,
- DeleteParams
- >;
- successMessage?: string;
}
-export interface UseDeleteWithConfirmControllerReturn {
+export interface UseDeleteWithConfirmControllerReturn
+ extends Omit {
open: boolean;
- isLoading: boolean;
- isPending: boolean;
handleDialogOpen: (e: SyntheticEvent) => void;
handleDialogClose: (e: SyntheticEvent) => void;
handleDelete: ReactEventHandler;
diff --git a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
index f04445c4b86..ff6e6cb3a3b 100644
--- a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
+++ b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
@@ -1,13 +1,12 @@
-import { useCallback, ReactEventHandler } from 'react';
-import { UseMutationOptions } from '@tanstack/react-query';
+import type { ReactEventHandler } from 'react';
-import { useDelete } from '../../dataProvider';
-import { useUnselect } from '../../controller';
-import { useRedirect, RedirectionSideEffect } from '../../routing';
-import { useNotify } from '../../notification';
-import { RaRecord, DeleteParams } from '../../types';
-import { useResourceContext } from '../../core';
-import { useTranslate } from '../../i18n';
+import {
+ useDeleteController,
+ type UseDeleteControllerParams,
+ type UseDeleteControllerReturn,
+} from './useDeleteController';
+import type { RaRecord } from '../../types';
+import { useEvent } from '../../util';
/**
* Prepare callback for a Delete button with undo support
@@ -50,96 +49,19 @@ const useDeleteWithUndoController = <
>(
props: UseDeleteWithUndoControllerParams
): UseDeleteWithUndoControllerReturn => {
- const {
- record,
- redirect: redirectTo = 'list',
- onClick,
- mutationOptions = {},
- successMessage,
- } = props;
- const { meta: mutationMeta, ...otherMutationOptions } = mutationOptions;
- const resource = useResourceContext(props);
- const notify = useNotify();
- const unselect = useUnselect(resource);
- const redirect = useRedirect();
- const translate = useTranslate();
- const [deleteOne, { isPending }] = useDelete(
- resource,
- undefined,
- {
- onSuccess: () => {
- notify(
- successMessage ??
- `resources.${resource}.notifications.deleted`,
- {
- type: 'info',
- messageArgs: {
- smart_count: 1,
- _: translate('ra.notification.deleted', {
- smart_count: 1,
- }),
- },
- undoable: true,
- }
- );
- record && unselect([record.id]);
- redirect(redirectTo, resource);
- },
- onError: error => {
- notify(
- typeof error === 'string'
- ? error
- : (error as Error)?.message ||
- 'ra.notification.http_error',
- {
- type: 'error',
- messageArgs: {
- _:
- typeof error === 'string'
- ? error
- : (error as Error)?.message
- ? (error as Error).message
- : undefined,
- },
- }
- );
- },
- }
- );
+ const { onClick } = props;
+ const { isPending, handleDelete: controllerHandleDelete } =
+ useDeleteController({ ...props, mutationMode: 'undoable' });
- const handleDelete = useCallback(
- event => {
+ const handleDelete = useEvent((event: any) => {
+ if (event && event.stopPropagation) {
event.stopPropagation();
- if (!record) {
- throw new Error(
- 'The record cannot be deleted because no record has been passed'
- );
- }
- deleteOne(
- resource,
- {
- id: record.id,
- previousData: record,
- meta: mutationMeta,
- },
- {
- mutationMode: 'undoable',
- ...otherMutationOptions,
- }
- );
- if (typeof onClick === 'function') {
- onClick(event);
- }
- },
- [
- deleteOne,
- mutationMeta,
- otherMutationOptions,
- onClick,
- record,
- resource,
- ]
- );
+ }
+ controllerHandleDelete();
+ if (typeof onClick === 'function') {
+ onClick(event);
+ }
+ });
return { isPending, isLoading: isPending, handleDelete };
};
@@ -147,22 +69,15 @@ const useDeleteWithUndoController = <
export interface UseDeleteWithUndoControllerParams<
RecordType extends RaRecord = any,
MutationOptionsError = unknown,
-> {
- record?: RecordType;
- redirect?: RedirectionSideEffect;
- resource?: string;
+> extends Omit<
+ UseDeleteControllerParams,
+ 'mutationMode'
+ > {
onClick?: ReactEventHandler;
- mutationOptions?: UseMutationOptions<
- RecordType,
- MutationOptionsError,
- DeleteParams
- >;
- successMessage?: string;
}
-export interface UseDeleteWithUndoControllerReturn {
- isPending: boolean;
- isLoading: boolean;
+export interface UseDeleteWithUndoControllerReturn
+ extends Omit {
handleDelete: ReactEventHandler;
}
diff --git a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx
index 8e665c93bee..56bc9f3fcf7 100644
--- a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx
+++ b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx
@@ -7,18 +7,18 @@ import {
} from '@mui/material/styles';
import clsx from 'clsx';
-import { UseMutationOptions } from '@tanstack/react-query';
import {
- MutationMode,
RaRecord,
- DeleteParams,
- useDeleteWithConfirmController,
useRecordContext,
useResourceContext,
useTranslate,
- RedirectionSideEffect,
useGetRecordRepresentation,
useResourceTranslation,
+ useDeleteController,
+ useNotify,
+ useUnselect,
+ useRedirect,
+ UseDeleteControllerParams,
} from 'ra-core';
import { humanize, singularize } from 'inflection';
@@ -42,7 +42,7 @@ export const DeleteWithConfirmButton = (
label: labelProp,
mutationMode = 'pessimistic',
onClick,
- redirect = 'list',
+ redirect: redirectTo = 'list',
translateOptions = {},
titleTranslateOptions = translateOptions,
contentTranslateOptions = translateOptions,
@@ -54,27 +54,84 @@ export const DeleteWithConfirmButton = (
const translate = useTranslate();
const record = useRecordContext(props);
const resource = useResourceContext(props);
+ const notify = useNotify();
+ const unselect = useUnselect(resource);
+ const redirect = useRedirect();
+ const [open, setOpen] = React.useState(false);
if (!resource) {
throw new Error(
' components should be used inside a component or provided with a resource prop. (The component set the resource prop for all its children).'
);
}
- const {
- open,
- isPending,
- handleDialogOpen,
- handleDialogClose,
- handleDelete,
- } = useDeleteWithConfirmController({
+ const { onSuccess, onError, ...otherMutationOptions } =
+ mutationOptions || {};
+
+ const { isPending, handleDelete } = useDeleteController({
record,
- redirect,
+ redirect: redirectTo,
mutationMode,
- onClick,
- mutationOptions,
+ mutationOptions: {
+ ...otherMutationOptions,
+ onSuccess: (data, variables, context) => {
+ setOpen(false);
+ if (onSuccess) {
+ onSuccess(data, variables, context);
+ } else {
+ notify(
+ successMessage ??
+ `resources.${resource}.notifications.deleted`,
+ {
+ type: 'info',
+ messageArgs: {
+ smart_count: 1,
+ _: translate('ra.notification.deleted', {
+ smart_count: 1,
+ }),
+ },
+ undoable: mutationMode === 'undoable',
+ }
+ );
+ record && unselect([record.id]);
+ redirect(redirectTo, resource);
+ }
+ },
+ onError: (error, variables, context) => {
+ setOpen(false);
+ if (onError) {
+ onError(error, variables, context);
+ } else {
+ notify(
+ typeof error === 'string'
+ ? error
+ : (error as Error)?.message ||
+ 'ra.notification.http_error',
+ {
+ type: 'error',
+ messageArgs: {
+ _:
+ typeof error === 'string'
+ ? error
+ : (error as Error)?.message
+ ? (error as Error).message
+ : undefined,
+ },
+ }
+ );
+ }
+ },
+ },
resource,
successMessage,
});
+
+ const handleDialogOpen: ReactEventHandler = () => {
+ setOpen(true);
+ };
+ const handleDialogClose: ReactEventHandler = () => {
+ setOpen(false);
+ };
+
const getRecordRepresentation = useGetRecordRepresentation(resource);
let recordRepresentation = getRecordRepresentation(record);
const resourceName = translate(`resources.${resource}.forcedCaseName`, {
@@ -156,12 +213,12 @@ const defaultIcon = ;
export interface DeleteWithConfirmButtonProps<
RecordType extends RaRecord = any,
MutationOptionsError = unknown,
-> extends ButtonProps {
+> extends ButtonProps,
+ UseDeleteControllerParams {
confirmTitle?: React.ReactNode;
confirmContent?: React.ReactNode;
icon?: React.ReactNode;
confirmColor?: 'primary' | 'warning';
- mutationMode?: MutationMode;
onClick?: ReactEventHandler;
// May be injected by Toolbar - sanitized in Button
/**
@@ -170,15 +227,6 @@ export interface DeleteWithConfirmButtonProps<
translateOptions?: object;
titleTranslateOptions?: object;
contentTranslateOptions?: object;
- mutationOptions?: UseMutationOptions<
- RecordType,
- MutationOptionsError,
- DeleteParams
- >;
- record?: RecordType;
- redirect?: RedirectionSideEffect;
- resource?: string;
- successMessage?: string;
}
const PREFIX = 'RaDeleteWithConfirmButton';
diff --git a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx
index 1b94740f331..e74544a1778 100644
--- a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx
+++ b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx
@@ -2,17 +2,15 @@ import * as React from 'react';
import { ReactNode, ReactEventHandler } from 'react';
import ActionDelete from '@mui/icons-material/Delete';
import clsx from 'clsx';
-import { UseMutationOptions } from '@tanstack/react-query';
import {
RaRecord,
- useDeleteWithUndoController,
- DeleteParams,
+ useDeleteController,
useRecordContext,
useResourceContext,
- RedirectionSideEffect,
useTranslate,
useGetRecordRepresentation,
useResourceTranslation,
+ UseDeleteControllerParams,
} from 'ra-core';
import { humanize, singularize } from 'inflection';
@@ -50,14 +48,21 @@ export const DeleteWithUndoButton = (
' components should be used inside a component or provided with a resource prop. (The component set the resource prop for all its children).'
);
}
- const { isPending, handleDelete } = useDeleteWithUndoController({
+ const { isPending, handleDelete } = useDeleteController({
record,
resource,
redirect,
- onClick,
+ mutationMode: 'undoable',
mutationOptions,
successMessage,
});
+ const handleClick: ReactEventHandler = event => {
+ handleDelete();
+ if (onClick) {
+ onClick(event);
+ }
+ };
+
const translate = useTranslate();
const getRecordRepresentation = useGetRecordRepresentation(resource);
let recordRepresentation = getRecordRepresentation(record);
@@ -87,7 +92,7 @@ export const DeleteWithUndoButton = (
return (
{label}>}
@@ -108,18 +113,10 @@ const defaultIcon = ;
export interface DeleteWithUndoButtonProps<
RecordType extends RaRecord = any,
MutationOptionsError = unknown,
-> extends ButtonProps {
+> extends ButtonProps,
+ UseDeleteControllerParams {
icon?: ReactNode;
onClick?: ReactEventHandler;
- mutationOptions?: UseMutationOptions<
- RecordType,
- MutationOptionsError,
- DeleteParams
- >;
- record?: RecordType;
- redirect?: RedirectionSideEffect;
- resource?: string;
- successMessage?: string;
}
const PREFIX = 'RaDeleteWithUndoButton';
From ecfc59c12aa74cd4f05256c0d500e0c155253443 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 1 Aug 2025 11:53:29 +0200
Subject: [PATCH 2/6] Modify DeleteWithConfirmButton tests to check dialog is
closed
---
.../button/DeleteWithConfirmButton.spec.tsx | 20 +++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.spec.tsx b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.spec.tsx
index 02bd5e4a4e8..80a1401b736 100644
--- a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.spec.tsx
+++ b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.spec.tsx
@@ -96,8 +96,8 @@ describe('', () => {
it('should allow to override the resource', async () => {
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({
data: { id: 123, title: 'lorem' },
}),
@@ -137,8 +137,8 @@ describe('', () => {
it('should allows to undo the deletion after confirmation if mutationMode is undoable', async () => {
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({
data: { id: 123, title: 'lorem' },
}),
@@ -182,8 +182,8 @@ describe('', () => {
it('should allow to override the success side effects', async () => {
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({
data: { id: 123, title: 'lorem' },
}),
@@ -226,13 +226,17 @@ describe('', () => {
{ snapshot: [] }
);
});
+ await waitFor(() => {
+ // Check that the dialog is closed
+ expect(screen.queryByText('ra.action.confirm')).toBeNull();
+ });
});
it('should allow to override the error side effects', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({
data: { id: 123, title: 'lorem' },
}),
@@ -275,12 +279,16 @@ describe('', () => {
{ snapshot: [] }
);
});
+ await waitFor(() => {
+ // Check that the dialog is closed
+ expect(screen.queryByText('ra.action.confirm')).toBeNull();
+ });
});
it('should allow to override the translateOptions props', async () => {
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({
data: { id: 123, title: 'lorem' },
}),
@@ -322,8 +330,8 @@ describe('', () => {
it('should display success message after successful deletion', async () => {
const successMessage = 'Test Message';
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({
data: { id: 123, title: 'lorem' },
}),
From c1e4beae0734d63f240a6ed77e70b3755732de81 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Mon, 25 Aug 2025 09:54:17 +0200
Subject: [PATCH 3/6] Apply code review
---
.../controller/button/useDeleteController.tsx | 38 +++++++++----------
1 file changed, 19 insertions(+), 19 deletions(-)
diff --git a/packages/ra-core/src/controller/button/useDeleteController.tsx b/packages/ra-core/src/controller/button/useDeleteController.tsx
index 325694adc6b..073f2b5eb01 100644
--- a/packages/ra-core/src/controller/button/useDeleteController.tsx
+++ b/packages/ra-core/src/controller/button/useDeleteController.tsx
@@ -1,4 +1,4 @@
-import { useCallback } from 'react';
+import { useCallback, useMemo } from 'react';
import { UseMutationOptions } from '@tanstack/react-query';
import { useDelete } from '../../dataProvider';
@@ -10,34 +10,31 @@ import { useResourceContext } from '../../core';
import { useTranslate } from '../../i18n';
/**
- * Prepare a set of callbacks for a delete button guarded by confirmation dialog
+ * Prepare a set of callbacks for a delete button
*
* @example
- *
* const DeleteButton = ({
* resource,
* record,
* redirect,
- * onClick,
* ...rest
* }) => {
* const {
- * open,
* isPending,
- * handleDialogOpen,
- * handleDialogClose,
* handleDelete,
- * } = useDeleteWithConfirmController({
+ * } = useDeleteController({
+ * mutationMode: 'pessimistic',
* resource,
* record,
* redirect,
- * onClick,
* });
*
+ * const [open, setOpen] = useState(false);
+ *
* return (
*
*
* );
@@ -69,13 +66,13 @@ export const useDeleteController = <
>(
props: UseDeleteControllerParams
): UseDeleteControllerReturn => {
- const { mutationMode } = props;
const {
record,
redirect: redirectTo = 'list',
+ mutationMode,
mutationOptions = {},
successMessage,
- } = props as UseDeleteControllerParams;
+ } = props;
const { meta: mutationMeta, ...otherMutationOptions } = mutationOptions;
const resource = useResourceContext(props);
const notify = useNotify();
@@ -154,11 +151,14 @@ export const useDeleteController = <
resource,
]);
- return {
- isPending,
- isLoading: isPending,
- handleDelete,
- };
+ return useMemo(
+ () => ({
+ isPending,
+ isLoading: isPending,
+ handleDelete,
+ }),
+ [isPending, handleDelete]
+ );
};
export interface UseDeleteControllerParams<
From 3ddcc803ea0bb1520e0a28bd7f8a432d873b7dd1 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Wed, 27 Aug 2025 16:50:05 +0200
Subject: [PATCH 4/6] Mark useDeleteWithUndoController and
useDeleteWithConfirmController as deprecated
---
.../src/controller/button/useDeleteWithConfirmController.tsx | 1 +
.../src/controller/button/useDeleteWithUndoController.tsx | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
index 492727c4636..7f37b9ac510 100644
--- a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
+++ b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
@@ -14,6 +14,7 @@ import { useEvent } from '../../util';
/**
* Prepare a set of callbacks for a delete button guarded by confirmation dialog
+ * @deprecated prefer the useDeleteController hook instead
*
* @example
*
diff --git a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
index ff6e6cb3a3b..6df1fde0dfe 100644
--- a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
+++ b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
@@ -10,7 +10,7 @@ import { useEvent } from '../../util';
/**
* Prepare callback for a Delete button with undo support
- *
+ * @deprecated prefer the useDeleteController hook instead
* @example
*
* import React from 'react';
From 59c6dca8009d948e46c17ac6ca6cb4d2fc1f7278 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 29 Aug 2025 15:12:58 +0200
Subject: [PATCH 5/6] Make sure to stop event propagation
---
.../ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx | 3 ++-
packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx | 1 +
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx
index 56bc9f3fcf7..321460686f5 100644
--- a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx
+++ b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx
@@ -125,7 +125,8 @@ export const DeleteWithConfirmButton = (
successMessage,
});
- const handleDialogOpen: ReactEventHandler = () => {
+ const handleDialogOpen: ReactEventHandler = event => {
+ event.stopPropagation();
setOpen(true);
};
const handleDialogClose: ReactEventHandler = () => {
diff --git a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx
index e74544a1778..c0bbd1c487b 100644
--- a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx
+++ b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx
@@ -57,6 +57,7 @@ export const DeleteWithUndoButton = (
successMessage,
});
const handleClick: ReactEventHandler = event => {
+ event.stopPropagation();
handleDelete();
if (onClick) {
onClick(event);
From ca228a4cf44c7604a55820b94d5a1659c12067eb Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Mon, 1 Sep 2025 10:32:02 +0200
Subject: [PATCH 6/6] Ensure users can still override only parts of the
mutationOptions
---
.../controller/button/useDeleteWithConfirmController.tsx | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
index 7f37b9ac510..c45b70818ec 100644
--- a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
+++ b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
@@ -79,6 +79,8 @@ const useDeleteWithConfirmController = <
record,
redirect: redirectTo = 'list',
successMessage,
+ mutationOptions = {},
+ ...rest
} = props;
const [open, setOpen] = useState(false);
const resource = useResourceContext(props);
@@ -89,6 +91,7 @@ const useDeleteWithConfirmController = <
const { isPending, handleDelete: controllerHandleDelete } =
useDeleteController({
+ mutationMode,
mutationOptions: {
onSuccess: () => {
setOpen(false);
@@ -130,8 +133,12 @@ const useDeleteWithConfirmController = <
}
);
},
+ ...mutationOptions,
},
- ...props,
+ record,
+ redirect: redirectTo,
+ successMessage,
+ ...rest,
});
const handleDialogOpen = useEvent((e: any) => {