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} 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) => { ', () => { @@ -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,27 @@ describe('', () => { fireEvent.click(screen.getByLabelText('Add')); expect(await screen.findAllByText('Foo')).toHaveLength(2); }); + + describe('inside ReferenceArrayInput', () => { + 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..5038ee88297 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 InsideReferenceArrayInput = () => ( + + + + `${record.first_name} ${record.last_name}` + } + /> + ( + { + console.log(data); + }, + }} + > + + + + + + + + )} + /> + + +); + +export const InsideReferenceArrayInputDefaultValue = ({ + 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 InsideReferenceArrayInputWithError = () => ( + + + 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') + ), }} >