Skip to content

Commit e5ab087

Browse files
committed
Improve <DeleteButton> and <UpdateButton> confirmation using record representation
1 parent 3f7be19 commit e5ab087

File tree

9 files changed

+383
-31
lines changed

9 files changed

+383
-31
lines changed

packages/ra-core/src/core/useGetRecordRepresentation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import get from 'lodash/get';
55
import { useResourceDefinition } from './useResourceDefinition';
66

77
/**
8-
* Get default string representation of a record
8+
* Get the default representation of a record (either a string or a React node)
99
*
1010
* @example // No customization
1111
* const getRecordRepresentation = useGetRecordRepresentation('posts');

packages/ra-language-english/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,14 @@ const englishMessages: TranslationMessages = {
101101
'Delete %{name} |||| Delete %{smart_count} %{name}',
102102
bulk_update_content:
103103
'Are you sure you want to update this %{name}? |||| Are you sure you want to update these %{smart_count} items?',
104+
bulk_update_content_record_representation:
105+
'Are you sure you want to update %{name}? |||| Are you sure you want to update these %{smart_count} items?',
104106
bulk_update_title:
105107
'Update %{name} |||| Update %{smart_count} %{name}',
106108
clear_array_input: 'Are you sure you want to clear the whole list?',
107109
delete_content: 'Are you sure you want to delete this item?',
108110
delete_title: 'Delete %{name} #%{id}',
111+
delete_title_record_representation: 'Delete %{name}',
109112
details: 'Details',
110113
error: "A client error occurred and your request couldn't be completed.",
111114
invalid_form: 'The form is not valid. Please check for errors',

packages/ra-language-french/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,16 @@ const frenchMessages: TranslationMessages = {
102102
'Supprimer %{name} |||| Supprimer %{smart_count} %{name}',
103103
bulk_update_content:
104104
'Êtes-vous sûr(e) de vouloir modifier cet élément ? |||| Êtes-vous sûr(e) de vouloir modifier ces %{smart_count} éléments ?',
105+
bulk_update_content_record_representation:
106+
'Êtes-vous sûr(e) de vouloir modifier cet élément ? |||| Êtes-vous sûr(e) de vouloir modifier ces %{smart_count} éléments ?',
105107
bulk_update_title:
106108
'Modifier %{name} |||| Modifier %{smart_count} %{name}',
107109
clear_array_input:
108110
'Êtes-vous sûr(e) de vouloir supprimer tous les éléments de la liste ?',
109111
delete_content:
110112
'Êtes-vous sûr(e) de vouloir supprimer cet élément ?',
111113
delete_title: 'Supprimer %{name} #%{id}',
114+
delete_title_record_representation: 'Supprimer %{name}',
112115
details: 'Détails',
113116
error: "En raison d'une erreur côté navigateur, votre requête n'a pas pu aboutir.",
114117
invalid_form: "Le formulaire n'est pas valide.",

packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.spec.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import * as React from 'react';
2-
import { screen, render, waitFor, fireEvent } from '@testing-library/react';
2+
import {
3+
screen,
4+
render,
5+
waitFor,
6+
fireEvent,
7+
within,
8+
} from '@testing-library/react';
39
import expect from 'expect';
410
import {
511
CoreAdminContext,
@@ -14,6 +20,10 @@ import { Toolbar, SimpleForm } from '../form';
1420
import { Edit } from '../detail';
1521
import { TextInput } from '../input';
1622
import { Notification } from '../layout';
23+
import {
24+
Basic,
25+
NoRecordRepresentation,
26+
} from './DeleteWithConfirmButton.stories';
1727

1828
const theme = createTheme();
1929

@@ -329,4 +339,28 @@ describe('<DeleteWithConfirmButton />', () => {
329339
]);
330340
});
331341
});
342+
343+
it('should use the record representation in the confirmation title', async () => {
344+
render(<Basic />);
345+
fireEvent.click(
346+
within(
347+
(await screen.findByText('War and Peace')).closest(
348+
'tr'
349+
) as HTMLElement
350+
).getByText('Delete')
351+
);
352+
await screen.findByText('Delete War and Peace');
353+
});
354+
355+
it('should use the default translation in the confirmation title when no record representation is available', async () => {
356+
render(<NoRecordRepresentation />);
357+
fireEvent.click(
358+
within(
359+
(await screen.findByText('Leo Tolstoy')).closest(
360+
'tr'
361+
) as HTMLElement
362+
).getByText('Delete')
363+
);
364+
await screen.findByText('Delete #1');
365+
});
332366
});

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,19 @@ const dataProvider = fakeRestDataProvider({
104104
year: 1922,
105105
},
106106
],
107-
authors: [],
107+
authors: [
108+
{ id: 1, fullName: 'Leo Tolstoy' },
109+
{ id: 2, fullName: 'Jane Austen' },
110+
{ id: 3, fullName: 'Oscar Wilde' },
111+
{ id: 4, fullName: 'Antoine de Saint-Exupéry' },
112+
{ id: 5, fullName: 'Lewis Carroll' },
113+
{ id: 6, fullName: 'Gustave Flaubert' },
114+
{ id: 7, fullName: 'J. R. R. Tolkien' },
115+
{ id: 8, fullName: 'J. K. Rowling' },
116+
{ id: 9, fullName: 'Paulo Coelho' },
117+
{ id: 10, fullName: 'J. D. Salinger' },
118+
{ id: 11, fullName: 'James Joyce' },
119+
],
108120
});
109121

110122
const BookList = ({ children }) => {
@@ -121,6 +133,18 @@ const BookList = ({ children }) => {
121133
);
122134
};
123135

136+
const AuthorList = ({ children }) => {
137+
return (
138+
<List>
139+
<Datagrid>
140+
<TextField source="id" />
141+
<TextField source="fullName" />
142+
{children}
143+
</Datagrid>
144+
</List>
145+
);
146+
};
147+
124148
export const Basic = () => (
125149
<TestMemoryRouter initialEntries={['/books']}>
126150
<AdminContext dataProvider={dataProvider} i18nProvider={i18nProvider}>
@@ -138,6 +162,23 @@ export const Basic = () => (
138162
</TestMemoryRouter>
139163
);
140164

165+
export const NoRecordRepresentation = () => (
166+
<TestMemoryRouter initialEntries={['/authors']}>
167+
<AdminContext dataProvider={dataProvider} i18nProvider={i18nProvider}>
168+
<AdminUI>
169+
<Resource
170+
name="authors"
171+
list={
172+
<AuthorList>
173+
<DeleteWithConfirmButton />
174+
</AuthorList>
175+
}
176+
/>
177+
</AdminUI>
178+
</AdminContext>
179+
</TestMemoryRouter>
180+
);
181+
141182
export const WithCustomDialogContent = () => (
142183
<TestMemoryRouter initialEntries={['/books']}>
143184
<AdminContext dataProvider={dataProvider} i18nProvider={i18nProvider}>

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

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { Fragment, ReactEventHandler } from 'react';
1+
import React, { Fragment, isValidElement, ReactEventHandler } from 'react';
22
import ActionDelete from '@mui/icons-material/Delete';
33
import clsx from 'clsx';
44

@@ -12,6 +12,7 @@ import {
1212
useResourceContext,
1313
useTranslate,
1414
RedirectionSideEffect,
15+
useGetRecordRepresentation,
1516
} from 'ra-core';
1617

1718
import { Confirm } from '../layout';
@@ -23,7 +24,7 @@ export const DeleteWithConfirmButton = <RecordType extends RaRecord = any>(
2324
) => {
2425
const {
2526
className,
26-
confirmTitle = 'ra.message.delete_title',
27+
confirmTitle: confirmTitleProp,
2728
confirmContent = 'ra.message.delete_content',
2829
confirmColor = 'primary',
2930
icon = defaultIcon,
@@ -56,6 +57,24 @@ export const DeleteWithConfirmButton = <RecordType extends RaRecord = any>(
5657
resource,
5758
successMessage,
5859
});
60+
const getRecordRepresentation = useGetRecordRepresentation(resource);
61+
const recordRepresentation = getRecordRepresentation(record);
62+
let confirmNameParam = recordRepresentation;
63+
let confirmTitle = 'ra.message.delete_title_record_representation';
64+
65+
if (isValidElement(recordRepresentation)) {
66+
confirmTitle = 'ra.message.delete_title';
67+
confirmNameParam = translate(`resources.${resource}.forcedCaseName`, {
68+
smart_count: 1,
69+
_: humanize(
70+
translate(`resources.${resource}.name`, {
71+
smart_count: 1,
72+
_: resource ? singularize(resource) : undefined,
73+
}),
74+
true
75+
),
76+
});
77+
}
5978

6079
return (
6180
<Fragment>
@@ -72,20 +91,11 @@ export const DeleteWithConfirmButton = <RecordType extends RaRecord = any>(
7291
<Confirm
7392
isOpen={open}
7493
loading={isPending}
75-
title={confirmTitle}
94+
title={confirmTitleProp ?? confirmTitle}
7695
content={confirmContent}
7796
confirmColor={confirmColor}
7897
translateOptions={{
79-
name: translate(`resources.${resource}.forcedCaseName`, {
80-
smart_count: 1,
81-
_: humanize(
82-
translate(`resources.${resource}.name`, {
83-
smart_count: 1,
84-
_: resource ? singularize(resource) : undefined,
85-
}),
86-
true
87-
),
88-
}),
98+
name: confirmNameParam,
8999
id: record?.id,
90100
...translateOptions,
91101
}}

packages/ra-ui-materialui/src/button/UpdateWithConfirmButton.spec.tsx

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import * as React from 'react';
2-
import { screen, render, waitFor, fireEvent } from '@testing-library/react';
2+
import {
3+
screen,
4+
render,
5+
waitFor,
6+
fireEvent,
7+
within,
8+
} from '@testing-library/react';
39
import expect from 'expect';
410
import { CoreAdminContext, MutationMode, testDataProvider } from 'ra-core';
511
import { createTheme, ThemeProvider } from '@mui/material/styles';
@@ -10,6 +16,10 @@ import { Edit } from '../detail';
1016
import { TextInput } from '../input';
1117
import { Notification } from '../layout';
1218
import { MutationOptions } from './UpdateButton.stories';
19+
import {
20+
Basic,
21+
NoRecordRepresentation,
22+
} from './UpdateWithConfirmButton.stories';
1323

1424
const theme = createTheme();
1525

@@ -252,7 +262,7 @@ describe('<UpdateWithConfirmButton />', () => {
252262
fireEvent.click(await screen.findByText('Reset views'));
253263
await screen.findByRole('dialog');
254264
await screen.findByText(
255-
'Are you sure you want to update this post?',
265+
'Are you sure you want to update Lorem Ipsum?',
256266
undefined,
257267
{ timeout: 4000 }
258268
);
@@ -263,7 +273,34 @@ describe('<UpdateWithConfirmButton />', () => {
263273
// wait until next tick, as the settled side effect is called after the success side effect
264274
await waitFor(() => new Promise(resolve => setTimeout(resolve, 300)));
265275
expect(
266-
screen.queryByText('Are you sure you want to update this post?')
276+
screen.queryByText('Are you sure you want to update Lorem Ipsum?')
267277
).toBeNull();
268278
});
279+
280+
it('should use the record representation in the confirmation title and content', async () => {
281+
render(<Basic />);
282+
fireEvent.click(
283+
within(
284+
(await screen.findByText('War and Peace')).closest(
285+
'tr'
286+
) as HTMLElement
287+
).getByText('Update')
288+
);
289+
await screen.findByText('Update War and Peace');
290+
await screen.findByText(
291+
'Are you sure you want to update War and Peace?'
292+
);
293+
});
294+
295+
it('should use the default translation in the confirmation title when no record representation is available', async () => {
296+
render(<NoRecordRepresentation />);
297+
fireEvent.click(
298+
within(
299+
(await screen.findByText('Leo Tolstoy')).closest(
300+
'tr'
301+
) as HTMLElement
302+
).getByText('Update')
303+
);
304+
await screen.findByText('Update #1');
305+
});
269306
});

0 commit comments

Comments
 (0)