From 71e5559c287a598941711b9d387769de32306c26 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:27:41 +0200 Subject: [PATCH 1/6] Use record representation in `` --- .../src/input/SelectArrayInput.spec.tsx | 29 ++- .../src/input/SelectArrayInput.stories.tsx | 209 ++++++++++++++++-- .../src/input/SelectArrayInput.tsx | 21 +- .../src/input/SelectInput.spec.tsx | 2 +- .../src/input/SelectInput.stories.tsx | 5 +- 5 files changed, 239 insertions(+), 27 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx index 3f8f3eea02f..1473bc6b2a5 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx @@ -11,6 +11,8 @@ import { DifferentIdTypes, TranslateChoice, InsideArrayInput, + InsideReferenceInput, + InsideReferenceInputDefaultValue, } from './SelectArrayInput.stories'; describe('', () => { @@ -177,7 +179,7 @@ describe('', () => { it('should use optionText with an element value as text identifier', () => { const Foobar = () => { const record = useRecordContext(); - return {record.foobar}; + return {record?.foobar}; }; render( @@ -560,7 +562,7 @@ describe('', () => { }); }); - it('should recive a value on change when creating a new choice', async () => { + it('should receive a value on change when creating a new choice', async () => { jest.spyOn(console, 'warn').mockImplementation(() => {}); const choices = [...defaultProps.choices]; const newChoice = { id: 'js_fatigue', name: 'New Kid On The Block' }; @@ -603,7 +605,7 @@ describe('', () => { }); }); - it('should show selected values when ids type are inconsistant', async () => { + it('should show selected values when ids type are inconsistent', async () => { render(); await waitFor(() => { expect(screen.queryByText('artist_1')).not.toBeNull(); @@ -667,4 +669,25 @@ describe('', () => { fireEvent.click(screen.getByLabelText('Add')); expect(await screen.findAllByText('Foo')).toHaveLength(2); }); + + describe('inside ReferenceInput', () => { + it('should use the recordRepresentation as optionText', async () => { + render(); + await screen.findByText('Leo Tolstoy'); + }); + it('should not change an undefined value to empty string', async () => { + const onSuccess = jest.fn(); + render(); + const input = await screen.findByDisplayValue('War and Peace'); + fireEvent.change(input, { target: { value: 'War' } }); + screen.getByText('Save').click(); + await waitFor(() => { + expect(onSuccess).toHaveBeenCalledWith( + expect.objectContaining({ authors: undefined }), + expect.anything(), + expect.anything() + ); + }); + }); + }); }); diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx index f4da18092e1..e4ae8de4d8b 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx @@ -9,7 +9,6 @@ import { TextField, } from '@mui/material'; import fakeRestProvider from 'ra-data-fakerest'; -import { useWatch } from 'react-hook-form'; import { AdminContext } from '../AdminContext'; import { Create, Edit } from '../detail'; @@ -19,23 +18,14 @@ import { ReferenceArrayInput } from './ReferenceArrayInput'; import { useCreateSuggestionContext } from './useSupportCreateSuggestion'; import { TextInput } from './TextInput'; import { ArrayInput, SimpleFormIterator } from './ArrayInput'; +import { Resource, TestMemoryRouter } from 'ra-core'; +import { Admin } from 'react-admin'; +import { FormInspector } from './common'; export default { title: 'ra-ui-materialui/input/SelectArrayInput' }; const i18nProvider = polyglotI18nProvider(() => englishMessages); -const FormInspector = ({ source }) => { - const value = useWatch({ name: source }); - return ( -
- {source} value in form:  - - {JSON.stringify(value)} ({typeof value}) - -
- ); -}; - export const Basic = () => ( ( /> - + @@ -337,3 +327,194 @@ export const TranslateChoice = () => {
); }; + +const authors = [ + { id: 1, first_name: 'Leo', last_name: 'Tolstoy', language: 'Russian' }, + { id: 2, first_name: 'Victor', last_name: 'Hugo', language: 'French' }, + { + id: 3, + first_name: 'William', + last_name: 'Shakespeare', + language: 'English', + }, + { + id: 4, + first_name: 'Charles', + last_name: 'Baudelaire', + language: 'French', + }, + { id: 5, first_name: 'Marcel', last_name: 'Proust', language: 'French' }, +]; + +const dataProviderWithAuthors = { + getOne: () => + Promise.resolve({ + data: { + id: 1, + title: 'War and Peace', + authors: [1], + summary: + "War and Peace broadly focuses on Napoleon's invasion of Russia, and the impact it had on Tsarist society. The book explores themes such as revolution, revolution and empire, the growth and decline of various states and the impact it had on their economies, culture, and society.", + year: 1869, + }, + }), + getMany: (_resource, params) => + Promise.resolve({ + data: authors.filter(author => params.ids.includes(author.id)), + }), + getList: () => + new Promise(resolve => { + // eslint-disable-next-line eqeqeq + setTimeout( + () => + resolve({ + data: authors, + total: authors.length, + }), + 500 + ); + return; + }), + update: (_resource, params) => Promise.resolve(params), + create: (_resource, params) => { + const newAuthor = { + id: authors.length + 1, + first_name: params.data.first_name, + last_name: params.data.last_name, + language: params.data.language, + }; + authors.push(newAuthor); + return Promise.resolve({ data: newAuthor }); + }, +} as any; + +export const InsideReferenceInput = () => ( + + + + `${record.first_name} ${record.last_name}` + } + /> + ( + { + console.log(data); + }, + }} + > + + + + + + + + )} + /> + + +); + +export const InsideReferenceInputDefaultValue = ({ + onSuccess = console.log, +}) => ( + + + Promise.resolve({ + data: { + id: 1, + title: 'War and Peace', + // trigger default value + author: undefined, + summary: + "War and Peace broadly focuses on Napoleon's invasion of Russia, and the impact it had on Tsarist society. The book explores themes such as revolution, revolution and empire, the growth and decline of various states and the impact it had on their economies, culture, and society.", + year: 1869, + }, + }), + }} + > + + `${record.first_name} ${record.last_name}` + } + /> + ( + + + + + + + + + + )} + /> + + +); + +export const InsideReferenceInputWithError = () => ( + + + Promise.reject( + new Error('Error while fetching the authors') + ), + }} + > + + `${record.first_name} ${record.last_name}` + } + /> + ( + { + console.log(data); + }, + }} + > + + + + + + + + )} + /> + + +); diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx index d9eca66dd14..45bea592cd0 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx @@ -19,6 +19,7 @@ import { useChoicesContext, useChoices, RaRecord, + useGetRecordRepresentation, } from 'ra-core'; import { InputHelperText } from './InputHelperText'; import { FormControlProps } from '@mui/material/FormControl'; @@ -105,7 +106,7 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { onChange, onCreate, options = defaultOptions, - optionText = 'name', + optionText, optionValue = 'id', parse, resource: resourceProp, @@ -135,13 +136,6 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { source: sourceProp, }); - const { getChoiceText, getChoiceValue, getDisableValue } = useChoices({ - optionText, - optionValue, - disableValue, - translateChoice: translateChoice ?? !isFromReference, - }); - const { field, isRequired, @@ -158,6 +152,17 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { ...rest, }); + const getRecordRepresentation = useGetRecordRepresentation(resource); + + const { getChoiceText, getChoiceValue, getDisableValue } = useChoices({ + optionText: + optionText ?? + (isFromReference ? getRecordRepresentation : undefined), + optionValue, + disableValue, + translateChoice: translateChoice ?? !isFromReference, + }); + const handleChange = useCallback( (eventOrChoice: ChangeEvent | RaRecord) => { // We might receive an event from the mui component diff --git a/packages/ra-ui-materialui/src/input/SelectInput.spec.tsx b/packages/ra-ui-materialui/src/input/SelectInput.spec.tsx index 4b76c9cb784..907cec1def8 100644 --- a/packages/ra-ui-materialui/src/input/SelectInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/SelectInput.spec.tsx @@ -303,7 +303,7 @@ describe('', () => { const Foobar = () => { const record = useRecordContext(); return ( - + ); }; render( diff --git a/packages/ra-ui-materialui/src/input/SelectInput.stories.tsx b/packages/ra-ui-materialui/src/input/SelectInput.stories.tsx index 898a12da494..15cee12bfa7 100644 --- a/packages/ra-ui-materialui/src/input/SelectInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/SelectInput.stories.tsx @@ -491,7 +491,10 @@ export const InsideReferenceInputWithError = () => ( Promise.reject('error'), + getList: () => + Promise.reject( + new Error('Error while fetching the authors') + ), }} > Date: Wed, 5 Jun 2024 17:20:06 +0200 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Francois Zaninotto --- .../ra-ui-materialui/src/input/SelectArrayInput.spec.tsx | 8 ++++---- .../src/input/SelectArrayInput.stories.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx index 1473bc6b2a5..f2a5390763f 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx @@ -11,8 +11,8 @@ import { DifferentIdTypes, TranslateChoice, InsideArrayInput, - InsideReferenceInput, - InsideReferenceInputDefaultValue, + InsideReferenceArrayInput, + InsideReferenceArrayInputDefaultValue, } from './SelectArrayInput.stories'; describe('', () => { @@ -672,12 +672,12 @@ describe('', () => { describe('inside ReferenceInput', () => { it('should use the recordRepresentation as optionText', async () => { - render(); + render(); await screen.findByText('Leo Tolstoy'); }); it('should not change an undefined value to empty string', async () => { const onSuccess = jest.fn(); - render(); + render(); const input = await screen.findByDisplayValue('War and Peace'); fireEvent.change(input, { target: { value: 'War' } }); screen.getByText('Save').click(); diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx index e4ae8de4d8b..5038ee88297 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx @@ -388,7 +388,7 @@ const dataProviderWithAuthors = { }, } as any; -export const InsideReferenceInput = () => ( +export const InsideReferenceArrayInput = () => ( ( ); -export const InsideReferenceInputDefaultValue = ({ +export const InsideReferenceArrayInputDefaultValue = ({ onSuccess = console.log, }) => ( @@ -475,7 +475,7 @@ export const InsideReferenceInputDefaultValue = ({ ); -export const InsideReferenceInputWithError = () => ( +export const InsideReferenceArrayInputWithError = () => ( Date: Wed, 5 Jun 2024 17:24:54 +0200 Subject: [PATCH 3/6] Fix linter warning --- packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx index f2a5390763f..29721a52256 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx @@ -677,7 +677,9 @@ describe('', () => { }); it('should not change an undefined value to empty string', async () => { const onSuccess = jest.fn(); - render(); + render( + + ); const input = await screen.findByDisplayValue('War and Peace'); fireEvent.change(input, { target: { value: 'War' } }); screen.getByText('Save').click(); From 1b6d1e311fcd2ce285a9e3777fceae00a9466f43 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:46:17 +0200 Subject: [PATCH 4/6] Use record representation in `` --- .../src/input/CheckboxGroupInput.spec.tsx | 11 +++- .../src/input/CheckboxGroupInput.stories.tsx | 57 +++++++++++++------ .../src/input/CheckboxGroupInput.tsx | 17 +++++- 3 files changed, 64 insertions(+), 21 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.tsx b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.tsx index 69cef7f8af8..1873fdc3ce5 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.tsx @@ -11,6 +11,7 @@ import { import { AdminContext } from '../AdminContext'; import { SimpleForm } from '../form'; import { CheckboxGroupInput } from './CheckboxGroupInput'; +import { InsideReferenceArrayInput } from './CheckboxGroupInput.stories'; describe('', () => { const defaultProps = { @@ -145,7 +146,7 @@ describe('', () => { it('should use optionText with an element value as text identifier', () => { const Foobar = () => { const record = useRecordContext(); - return {record.foobar}; + return {record?.foobar}; }; render( @@ -383,4 +384,12 @@ describe('', () => { expect(screen.queryByRole('progressbar')).toBeNull(); }); + + describe('inside ReferenceArrayInput', () => { + it('should use the recordRepresentation as optionText', async () => { + render(); + + await screen.findByText('Option 1 (This is option 1)'); + }); + }); }); diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.stories.tsx b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.stories.tsx index ecfce4ab354..f66857c0262 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.stories.tsx @@ -4,7 +4,13 @@ import englishMessages from 'ra-language-english'; import { Typography } from '@mui/material'; import FavoriteBorder from '@mui/icons-material/FavoriteBorder'; import Favorite from '@mui/icons-material/Favorite'; -import { required, testDataProvider, useRecordContext } from 'ra-core'; +import { + Resource, + TestMemoryRouter, + required, + testDataProvider, + useRecordContext, +} from 'ra-core'; import { useFormContext } from 'react-hook-form'; import { AdminContext } from '../AdminContext'; @@ -13,6 +19,7 @@ import { SimpleForm } from '../form'; import { CheckboxGroupInput } from './CheckboxGroupInput'; import { ReferenceArrayInput } from './ReferenceArrayInput'; import { TextInput } from './TextInput'; +import { Admin } from 'react-admin'; export default { title: 'ra-ui-materialui/input/CheckboxGroupInput' }; @@ -61,23 +68,39 @@ const dataProvider = testDataProvider({ }); export const InsideReferenceArrayInput = () => ( - - + - - - - - - - + + `${record.name} (${record.details})` + } + /> + + + + + + + + } + /> + + ); export const Disabled = () => ( diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx index 6f80d1cc299..66684a69591 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx @@ -8,7 +8,13 @@ import FormControl, { FormControlProps } from '@mui/material/FormControl'; import FormGroup from '@mui/material/FormGroup'; import FormHelperText from '@mui/material/FormHelperText'; import { CheckboxProps } from '@mui/material/Checkbox'; -import { FieldTitle, useInput, ChoicesProps, useChoicesContext } from 'ra-core'; +import { + FieldTitle, + useInput, + ChoicesProps, + useChoicesContext, + useGetRecordRepresentation, +} from 'ra-core'; import { CommonInputProps } from './CommonInputProps'; import { sanitizeInputRestProps } from './sanitizeInputRestProps'; @@ -101,7 +107,7 @@ export const CheckboxGroupInput: FunctionComponent< onBlur, onChange, options, - optionText = 'name', + optionText, optionValue = 'id', parse, resource: resourceProp, @@ -156,6 +162,8 @@ export const CheckboxGroupInput: FunctionComponent< ...rest, }); + const getRecordRepresentation = useGetRecordRepresentation(resource); + const handleCheck = useCallback( (event, isChecked) => { let newValue; @@ -232,7 +240,10 @@ export const CheckboxGroupInput: FunctionComponent< id={id} onChange={handleCheck} options={options} - optionText={optionText} + optionText={ + optionText ?? + (isFromReference ? getRecordRepresentation : 'name') + } optionValue={optionValue} translateChoice={translateChoice ?? !isFromReference} value={value} From e2a947bcd1237d688a4529312ddac9c8c88c5e7c Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:46:30 +0200 Subject: [PATCH 5/6] Use record representation in `` --- .../src/input/RadioButtonGroupInput.spec.tsx | 8 +-- .../input/RadioButtonGroupInput.stories.tsx | 57 ++++++++++++------- .../src/input/RadioButtonGroupInput.tsx | 17 +++++- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.tsx b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.tsx index 5c9374b466f..261d3a5e0c4 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.tsx @@ -249,7 +249,7 @@ describe('', () => { it('should use optionText with an element value as text identifier', () => { const Foobar = () => { const record = useRecordContext(); - return {record.longname}; + return {record?.longname}; }; render( @@ -477,9 +477,9 @@ describe('', () => { it('should use the recordRepresentation as optionText', async () => { render(); - await screen.findByText('Lifestyle'); - await screen.findByText('Tech'); - await screen.findByText('People'); + await screen.findByText('Lifestyle (Lifestyle details)'); + await screen.findByText('Tech (Tech details)'); + await screen.findByText('People (People details)'); }); }); }); diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.stories.tsx b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.stories.tsx index 7c79ce58807..59ece817855 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.stories.tsx @@ -9,14 +9,15 @@ import { RadioButtonGroupInput } from './RadioButtonGroupInput'; import { FormInspector } from './common'; import { ReferenceInput } from './ReferenceInput'; import { ReferenceArrayInput } from './ReferenceArrayInput'; -import { testDataProvider } from 'ra-core'; +import { Resource, TestMemoryRouter, testDataProvider } from 'ra-core'; +import { Admin } from 'react-admin'; export default { title: 'ra-ui-materialui/input/RadioButtonGroupInput' }; const choices = [ - { id: 'tech', name: 'Tech' }, - { id: 'lifestyle', name: 'Lifestyle' }, - { id: 'people', name: 'People' }, + { id: 'tech', name: 'Tech', details: 'Tech details' }, + { id: 'lifestyle', name: 'Lifestyle', details: 'Lifestyle details' }, + { id: 'people', name: 'People', details: 'People details' }, ]; export const Basic = () => ( @@ -77,23 +78,39 @@ const dataProvider = testDataProvider({ } as any); export const InsideReferenceArrayInput = () => ( - - + - - - - - - - + + `${record.name} (${record.details})` + } + /> + + + + + + + + } + /> + + ); export const InsideReferenceArrayInputWithError = () => ( diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx index 5cf4fb1a6d0..b6223d2cd0e 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx @@ -10,7 +10,13 @@ import { import { RadioGroupProps } from '@mui/material/RadioGroup'; import { FormControlProps } from '@mui/material/FormControl'; import get from 'lodash/get'; -import { useInput, FieldTitle, ChoicesProps, useChoicesContext } from 'ra-core'; +import { + useInput, + FieldTitle, + ChoicesProps, + useChoicesContext, + useGetRecordRepresentation, +} from 'ra-core'; import { CommonInputProps } from './CommonInputProps'; import { sanitizeInputRestProps } from './sanitizeInputRestProps'; @@ -93,7 +99,7 @@ export const RadioButtonGroupInput = (props: RadioButtonGroupInputProps) => { onBlur, onChange, options = defaultOptions, - optionText = 'name', + optionText, optionValue = 'id', parse, resource: resourceProp, @@ -143,6 +149,8 @@ export const RadioButtonGroupInput = (props: RadioButtonGroupInputProps) => { ...rest, }); + const getRecordRepresentation = useGetRecordRepresentation(resource); + const { error, invalid } = fieldState; if (isPending) { @@ -193,7 +201,10 @@ export const RadioButtonGroupInput = (props: RadioButtonGroupInputProps) => { Date: Wed, 5 Jun 2024 17:50:01 +0200 Subject: [PATCH 6/6] Update packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx --- packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx index 29721a52256..06f0a86976a 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx @@ -670,7 +670,7 @@ describe('', () => { expect(await screen.findAllByText('Foo')).toHaveLength(2); }); - describe('inside ReferenceInput', () => { + describe('inside ReferenceArrayInput', () => { it('should use the recordRepresentation as optionText', async () => { render(); await screen.findByText('Leo Tolstoy');