Skip to content

Commit 0d93bb5

Browse files
authored
Merge pull request #10925 from marmelab/use-bulk-update-controller
Introduce `useBulkUpdateController`
2 parents 0bb605b + d6debe2 commit 0d93bb5

File tree

5 files changed

+230
-154
lines changed

5 files changed

+230
-154
lines changed

packages/ra-core/src/controller/button/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export { useDeleteWithUndoController, useDeleteWithConfirmController };
55
export * from './useDeleteController';
66
export * from './useBulkDeleteController';
77
export * from './useUpdateController';
8+
export * from './useBulkUpdateController';
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { useCallback, useMemo } from 'react';
2+
import { useRefresh } from '../../dataProvider/useRefresh';
3+
import { useListContext } from '../list/useListContext';
4+
import { useNotify } from '../../notification/useNotify';
5+
import { RaRecord, MutationMode } from '../../types';
6+
import { useResourceContext } from '../../core/useResourceContext';
7+
import { useTranslate } from '../../i18n/useTranslate';
8+
import {
9+
useUpdateMany,
10+
UseUpdateManyOptions,
11+
} from '../../dataProvider/useUpdateMany';
12+
13+
export const useBulkUpdateController = <
14+
RecordType extends RaRecord = any,
15+
ErrorType = Error,
16+
>(
17+
props: UseBulkUpdateControllerParams<RecordType, ErrorType>
18+
): UseBulkUpdateControllerReturn => {
19+
const {
20+
onSuccess,
21+
onError,
22+
mutationMode = 'undoable',
23+
mutationOptions = {},
24+
successMessage,
25+
} = props;
26+
const { meta: mutationMeta, ...otherMutationOptions } = mutationOptions;
27+
const resource = useResourceContext(props);
28+
const notify = useNotify();
29+
const refresh = useRefresh();
30+
const translate = useTranslate();
31+
const { selectedIds, onUnselectItems } = useListContext();
32+
33+
const [updateMany, { isPending }] = useUpdateMany<RecordType, ErrorType>(
34+
resource,
35+
undefined,
36+
{
37+
onSuccess:
38+
onSuccess ??
39+
(() => {
40+
notify(
41+
successMessage ??
42+
`resources.${resource}.notifications.updated`,
43+
{
44+
type: 'info',
45+
messageArgs: {
46+
smart_count: selectedIds.length,
47+
_: translate('ra.notification.updated', {
48+
smart_count: selectedIds.length,
49+
}),
50+
},
51+
undoable: mutationMode === 'undoable',
52+
}
53+
);
54+
onUnselectItems();
55+
}),
56+
onError:
57+
onError ??
58+
((error: any) => {
59+
notify(
60+
typeof error === 'string'
61+
? error
62+
: error?.message || 'ra.notification.http_error',
63+
{
64+
type: 'error',
65+
messageArgs: {
66+
_:
67+
typeof error === 'string'
68+
? error
69+
: error?.message,
70+
},
71+
}
72+
);
73+
refresh();
74+
}),
75+
...otherMutationOptions,
76+
}
77+
);
78+
79+
const handleUpdate = useCallback(
80+
(data: Partial<RecordType>) => {
81+
updateMany(
82+
resource,
83+
{
84+
data,
85+
ids: selectedIds,
86+
meta: mutationMeta,
87+
},
88+
{
89+
mutationMode,
90+
...otherMutationOptions,
91+
}
92+
);
93+
},
94+
[
95+
updateMany,
96+
mutationMeta,
97+
mutationMode,
98+
otherMutationOptions,
99+
resource,
100+
selectedIds,
101+
]
102+
);
103+
104+
return useMemo(
105+
() => ({
106+
isPending,
107+
isLoading: isPending,
108+
handleUpdate,
109+
}),
110+
[isPending, handleUpdate]
111+
);
112+
};
113+
114+
export interface UseBulkUpdateControllerParams<
115+
RecordType extends RaRecord = any,
116+
MutationOptionsError = unknown,
117+
> {
118+
/* @deprecated use mutationOptions instead */
119+
onSuccess?: () => void;
120+
/* @deprecated use mutationOptions instead */
121+
onError?: (error: any) => void;
122+
mutationMode?: MutationMode;
123+
mutationOptions?: UseUpdateManyOptions<RecordType, MutationOptionsError>;
124+
resource?: string;
125+
successMessage?: string;
126+
}
127+
128+
export interface UseBulkUpdateControllerReturn<
129+
RecordType extends RaRecord = any,
130+
> {
131+
isLoading: boolean;
132+
isPending: boolean;
133+
handleUpdate: (data: Partial<RecordType>) => void;
134+
}

packages/ra-ui-materialui/src/button/BulkUpdateButton.stories.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import polyglotI18nProvider from 'ra-i18n-polyglot';
33
import englishMessages from 'ra-language-english';
4-
import { Resource, TestMemoryRouter } from 'ra-core';
4+
import { Resource, TestMemoryRouter, useNotify } from 'ra-core';
55
import fakeRestDataProvider from 'ra-data-fakerest';
66
import { BulkUpdateButton } from './BulkUpdateButton';
77
import { AdminContext } from '../AdminContext';
@@ -166,6 +166,47 @@ export const MutationMode = () => (
166166
/>
167167
);
168168

169+
const MutationOptionsButtons = () => {
170+
const notify = useNotify();
171+
return (
172+
<>
173+
<BulkUpdateButton
174+
label="Update Undoable"
175+
data={{ reads: 0 }}
176+
mutationMode="undoable"
177+
mutationOptions={{
178+
onSuccess: () => {
179+
notify('Updated successfully', { undoable: true });
180+
},
181+
}}
182+
/>
183+
<BulkUpdateButton
184+
label="Update Optimistic"
185+
data={{ reads: 0 }}
186+
mutationMode="optimistic"
187+
mutationOptions={{
188+
onSuccess: () => {
189+
notify('Updated successfully');
190+
},
191+
}}
192+
/>
193+
<BulkUpdateButton
194+
label="Update Pessimistic"
195+
data={{ reads: 0 }}
196+
mutationMode="pessimistic"
197+
mutationOptions={{
198+
onSuccess: () => {
199+
notify('Updated successfully');
200+
},
201+
}}
202+
/>
203+
</>
204+
);
205+
};
206+
export const MutationOptions = () => (
207+
<Wrapper bulkActionButtons={<MutationOptionsButtons />} />
208+
);
209+
169210
export const Themed = () => (
170211
<Wrapper
171212
bulkActionButtons={<BulkUpdateButton data={{ reads: 0 }} />}

packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx

Lines changed: 39 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,14 @@ import {
99
import {
1010
useListContext,
1111
useTranslate,
12-
useUpdateMany,
13-
useNotify,
14-
useUnselectAll,
1512
useResourceContext,
16-
type MutationMode,
1713
type RaRecord,
18-
type UpdateManyParams,
14+
useBulkUpdateController,
15+
UseBulkUpdateControllerParams,
1916
} from 'ra-core';
2017

2118
import { Confirm } from '../layout';
2219
import { Button, type ButtonProps } from './Button';
23-
import type { UseMutationOptions } from '@tanstack/react-query';
2420
import { humanize, inflect } from 'inflection';
2521

2622
export const BulkUpdateWithConfirmButton = (
@@ -30,10 +26,8 @@ export const BulkUpdateWithConfirmButton = (
3026
props: inProps,
3127
name: PREFIX,
3228
});
33-
const notify = useNotify();
3429
const translate = useTranslate();
3530
const resource = useResourceContext(props);
36-
const unselectAll = useUnselectAll(resource);
3731
const [isOpen, setOpen] = useState(false);
3832
const { selectedIds } = useListContext();
3933

@@ -45,66 +39,46 @@ export const BulkUpdateWithConfirmButton = (
4539
label = 'ra.action.update',
4640
mutationMode = 'pessimistic',
4741
onClick,
48-
onSuccess = () => {
49-
notify(`resources.${resource}.notifications.updated`, {
50-
type: 'info',
51-
messageArgs: {
52-
smart_count: selectedIds.length,
53-
_: translate('ra.notification.updated', {
54-
smart_count: selectedIds.length,
55-
}),
56-
},
57-
undoable: mutationMode === 'undoable',
58-
});
59-
unselectAll();
60-
setOpen(false);
61-
},
62-
onError = (error: Error | string) => {
63-
notify(
64-
typeof error === 'string'
65-
? error
66-
: error.message || 'ra.notification.http_error',
67-
{
68-
type: 'error',
69-
messageArgs: {
70-
_:
71-
typeof error === 'string'
72-
? error
73-
: error && error.message
74-
? error.message
75-
: undefined,
76-
},
77-
}
78-
);
79-
setOpen(false);
80-
},
81-
mutationOptions = {},
8242
...rest
8343
} = props;
84-
const { meta: mutationMeta, ...otherMutationOptions } = mutationOptions;
85-
86-
const [updateMany, { isPending }] = useUpdateMany(
87-
resource,
88-
{ ids: selectedIds, data, meta: mutationMeta },
89-
{
90-
onSuccess,
91-
onError,
92-
mutationMode,
93-
...otherMutationOptions,
94-
}
95-
);
44+
const { handleUpdate, isPending } = useBulkUpdateController({
45+
...rest,
46+
mutationMode,
47+
mutationOptions: {
48+
...rest.mutationOptions,
49+
onSettled(data, error, variables, context) {
50+
// In pessimistic mode, we wait for the mutation to be completed (either successfully or with an error) before closing
51+
if (mutationMode === 'pessimistic') {
52+
setOpen(false);
53+
}
54+
rest.mutationOptions?.onSettled?.(
55+
data,
56+
error,
57+
variables,
58+
context
59+
);
60+
},
61+
},
62+
});
9663

97-
const handleClick = e => {
98-
setOpen(true);
64+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
9965
e.stopPropagation();
66+
setOpen(true);
10067
};
10168

102-
const handleDialogClose = () => {
69+
const handleDialogClose = (e: React.MouseEvent) => {
70+
e.stopPropagation();
10371
setOpen(false);
10472
};
10573

106-
const handleUpdate = e => {
107-
updateMany();
74+
const handleConfirm = (e: React.MouseEvent<HTMLButtonElement>) => {
75+
e.stopPropagation();
76+
// We close the dialog immediately here for optimistic/undoable modes instead of in onSuccess/onError
77+
// to avoid reimplementing the default side effects
78+
if (mutationMode !== 'pessimistic') {
79+
setOpen(false);
80+
}
81+
handleUpdate(data);
10882

10983
if (typeof onClick === 'function') {
11084
onClick(e);
@@ -155,7 +129,7 @@ export const BulkUpdateWithConfirmButton = (
155129
),
156130
}),
157131
}}
158-
onConfirm={handleUpdate}
132+
onConfirm={handleConfirm}
159133
onClose={handleDialogClose}
160134
/>
161135
</Fragment>
@@ -164,30 +138,20 @@ export const BulkUpdateWithConfirmButton = (
164138

165139
const sanitizeRestProps = ({
166140
label,
167-
onSuccess,
168-
onError,
141+
resource,
142+
successMessage,
169143
...rest
170-
}: Omit<
171-
BulkUpdateWithConfirmButtonProps,
172-
'resource' | 'selectedIds' | 'icon' | 'data'
173-
>) => rest;
144+
}: Omit<BulkUpdateWithConfirmButtonProps, 'icon' | 'data'>) => rest;
174145

175146
export interface BulkUpdateWithConfirmButtonProps<
176147
RecordType extends RaRecord = any,
177148
MutationOptionsError = unknown,
178-
> extends ButtonProps {
149+
> extends Omit<ButtonProps, 'onError'>,
150+
UseBulkUpdateControllerParams<RecordType, MutationOptionsError> {
179151
confirmContent?: React.ReactNode;
180152
confirmTitle?: React.ReactNode;
181153
icon?: React.ReactNode;
182154
data: any;
183-
onSuccess?: () => void;
184-
onError?: (error: any) => void;
185-
mutationMode?: MutationMode;
186-
mutationOptions?: UseMutationOptions<
187-
RecordType,
188-
MutationOptionsError,
189-
UpdateManyParams<RecordType>
190-
> & { meta?: any };
191155
}
192156

193157
const PREFIX = 'RaBulkUpdateWithConfirmButton';

0 commit comments

Comments
 (0)