diff --git a/packages/ra-core/src/form/choices/useChoicesContext.ts b/packages/ra-core/src/form/choices/useChoicesContext.ts index f6aef2743cf..0c1de498371 100644 --- a/packages/ra-core/src/form/choices/useChoicesContext.ts +++ b/packages/ra-core/src/form/choices/useChoicesContext.ts @@ -23,7 +23,7 @@ export const useChoicesContext = ( selectedChoices: options.selectedChoices ?? data, displayedFilters: options.selectedChoices ?? list.displayedFilters, - error: options.error ?? list.displayedFilters, + error: options.error, filter: options.filter ?? list.filter, filterValues: options.filterValues ?? list.filterValues, hasNextPage: options.hasNextPage ?? list.hasNextPage, diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx index 5f01ebbe1fd..e5c1b475865 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx @@ -178,6 +178,7 @@ export const AutocompleteInput = < const { allChoices, isLoading, + error: fetchError, resource, source, setFilters, @@ -489,11 +490,14 @@ If you provided a React element for the optionText prop, you must also provide t } /> } - error={(isTouched || isSubmitted) && invalid} + error={ + !!fetchError || + ((isTouched || isSubmitted) && invalid) + } helperText={ } diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx index 74797132f2d..62d687daabc 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx @@ -114,7 +114,13 @@ export const CheckboxGroupInput: FunctionComponent = pr ...rest } = props; - const { allChoices, isLoading, resource, source } = useChoicesContext({ + const { + allChoices, + isLoading, + error: fetchError, + resource, + source, + } = useChoicesContext({ choices: choicesProp, isFetching: isFetchingProp, isLoading: isLoadingProp, @@ -199,7 +205,7 @@ export const CheckboxGroupInput: FunctionComponent = pr @@ -229,10 +235,10 @@ export const CheckboxGroupInput: FunctionComponent = pr /> ))} - + diff --git a/packages/ra-ui-materialui/src/input/DatagridInput.tsx b/packages/ra-ui-materialui/src/input/DatagridInput.tsx index c63a1de4217..a1232431017 100644 --- a/packages/ra-ui-materialui/src/input/DatagridInput.tsx +++ b/packages/ra-ui-materialui/src/input/DatagridInput.tsx @@ -64,6 +64,7 @@ export const DatagridInput = (props: DatagridInputProps) => { allChoices, availableChoices, selectedChoices, + error: fetchError, source, ...choicesContext } = useChoicesContext({ @@ -139,11 +140,19 @@ export const DatagridInput = (props: DatagridInputProps) => { ) ) : null} - - {pagination !== false && pagination} + {!fieldState.error && !fetchError && ( + <> + + {pagination !== false && pagination} + + )} diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx index 1102798c84b..daa6a4f126a 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx @@ -106,7 +106,13 @@ export const RadioButtonGroupInput = (props: RadioButtonGroupInputProps) => { ...rest } = props; - const { allChoices, isLoading, resource, source } = useChoicesContext({ + const { + allChoices, + isLoading, + error: fetchError, + resource, + source, + } = useChoicesContext({ choices: choicesProp, isFetching: isFetchingProp, isLoading: isLoadingProp, @@ -157,7 +163,7 @@ export const RadioButtonGroupInput = (props: RadioButtonGroupInputProps) => { component="fieldset" className={clsx('ra-input', `ra-input-${source}`, className)} margin={margin} - error={(isTouched || isSubmitted) && invalid} + error={fetchError || ((isTouched || isSubmitted) && invalid)} {...sanitizeRestProps(rest)} > { diff --git a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.spec.tsx index 8efe324fbb4..8f5c30e9594 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.spec.tsx @@ -13,6 +13,7 @@ import { SimpleForm } from '../form'; import { DatagridInput } from './DatagridInput'; import { TextField } from '../field'; import { ReferenceArrayInput } from './ReferenceArrayInput'; +import { SelectArrayInput } from './SelectArrayInput'; import { QueryClient } from 'react-query'; describe('', () => { @@ -29,7 +30,6 @@ describe('', () => { it('should display an error if error is defined', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); - const MyComponent = () => ; render( ', () => { > - + ); await waitFor(() => { - expect(screen.queryByDisplayValue('fetch error')).not.toBeNull(); + expect(screen.queryByText('fetch error')).not.toBeNull(); }); }); it('should pass the correct resource down to child component', async () => { diff --git a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx index bb32649a26e..6d923fd49d4 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx @@ -5,6 +5,9 @@ import { AdminContext } from '../AdminContext'; import { DatagridInput } from '../input'; import { TextField } from '../field'; import { ReferenceArrayInput } from './ReferenceArrayInput'; +import { AutocompleteArrayInput } from './AutocompleteArrayInput'; +import { SelectArrayInput } from './SelectArrayInput'; +import { CheckboxGroupInput } from './CheckboxGroupInput'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from 'ra-language-english'; @@ -29,8 +32,136 @@ const dataProvider = testDataProvider({ const i18nProvider = polyglotI18nProvider(() => englishMessages); -export const WithDatagridChild = () => ( +export const WithAutocompleteInput = () => ( +
{}} defaultValues={{ tag_ids: [5] }}> + + + +
+
+); + +export const ErrorAutocomplete = () => ( + Promise.reject(new Error('fetch error')), + getMany: () => + Promise.resolve({ data: [{ id: 5, name: 'test1' }] }), + }} + i18nProvider={i18nProvider} + > +
{}} defaultValues={{ tag_ids: [5] }}> + + + +
+
+); + +export const WithSelectArrayInput = () => ( + +
{}} defaultValues={{ tag_ids: [5] }}> + + + +
+
+); + +export const ErrorSelectArray = () => ( + Promise.reject(new Error('fetch error')), + getMany: () => + Promise.resolve({ data: [{ id: 5, name: 'test1' }] }), + }} + i18nProvider={i18nProvider} + > +
{}} defaultValues={{ tag_ids: [5] }}> + + + +
+
+); + +export const WithCheckboxGroupInput = () => ( + +
{}} defaultValues={{ tag_ids: [5] }}> + + + +
+
+); + +export const ErrorCheckboxGroupInput = () => ( + Promise.reject(new Error('fetch error')), + getMany: () => + Promise.resolve({ data: [{ id: 5, name: 'test1' }] }), + }} + i18nProvider={i18nProvider} + > +
{}} defaultValues={{ tag_ids: [5] }}> + + + +
+
+); + +export const WithDatagridInput = () => ( + +
{}} defaultValues={{ tag_ids: [5] }}> + + + + + +
+
+); + +export const ErrorDatagridInput = () => ( + Promise.reject(new Error('fetch error')), + getMany: () => + Promise.resolve({ data: [{ id: 5, name: 'test1' }] }), + }} + i18nProvider={i18nProvider} + >
{}} defaultValues={{ tag_ids: [5] }}> { - const { children, label, reference } = props; + const { children, reference } = props; if (React.Children.count(children) !== 1) { throw new Error( ' only accepts a single child (like )' @@ -86,12 +85,6 @@ export const ReferenceArrayInput = (props: ReferenceArrayInputProps) => { const controllerProps = useReferenceArrayInputController(props); - // This is not a form error but an unrecoverable error from the - // useReferenceInputController hook - if (controllerProps.error) { - return ; - } - return ( diff --git a/packages/ra-ui-materialui/src/input/ReferenceError.tsx b/packages/ra-ui-materialui/src/input/ReferenceError.tsx index 6da51baeb5e..681d4c8cd4f 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceError.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceError.tsx @@ -14,7 +14,7 @@ export const ReferenceError = ({ error disabled label={label} - value={error?.message} + helperText={error?.message} margin="normal" /> ); diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.spec.tsx b/packages/ra-ui-materialui/src/input/ReferenceInput.spec.tsx index fca6bf7374b..552c63a01a9 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceInput.spec.tsx @@ -20,7 +20,7 @@ describe('', () => { jest.spyOn(console, 'error') .mockImplementationOnce(() => {}) .mockImplementationOnce(() => {}); - const MyComponent = () => ; + render( ', () => { })} > - - - + ); await waitFor(() => { - expect(screen.queryByDisplayValue('fetch error')).not.toBeNull(); + expect(screen.queryByText('fetch error')).not.toBeNull(); }); }); diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx b/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx index 2c8f0a65da5..085842b1b2b 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { createMemoryHistory } from 'history'; import { Admin, AdminContext } from 'react-admin'; +import { QueryClient } from 'react-query'; import { Resource, Form, testDataProvider } from 'ra-core'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from 'ra-language-english'; @@ -8,7 +9,7 @@ import { Stack, Divider, Typography } from '@mui/material'; import { Edit } from '../detail'; import { SimpleForm } from '../form'; -import { SelectInput, TextInput } from '../input'; +import { SelectInput, RadioButtonGroupInput, TextInput } from '../input'; import { ReferenceInput } from './ReferenceInput'; export default { title: 'ra-ui-materialui/input/ReferenceInput' }; @@ -269,3 +270,192 @@ export const Loading = () => (
); + +const book = { + id: 1, + title: 'War and Peace', + author: 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, +}; + +export const ErrorAutocomplete = () => ( + Promise.resolve({ data: book }), + getMany: (resource, params) => + Promise.resolve({ + data: authors.filter(author => + params.ids.includes(author.id) + ), + }), + getList: (resource, params) => + params.filter.q === 'lorem' + ? Promise.reject(new Error('An error occured')) + : Promise.resolve({ + data: authors, + total: authors.length, + }), + } as any + } + history={history} + queryClient={ + new QueryClient({ defaultOptions: { queries: { retry: false } } }) + } + > + `${r.first_name} ${r.last_name}`} + /> + ( + + + + + + )} + /> + +); + +export const WithSelectInput = () => ( + + + `${record.first_name} ${record.last_name}` + } + /> + ( + { + console.log(data); + }, + }} + > + + + + + + + )} + /> + +); + +export const ErrorSelectInput = () => ( + Promise.resolve({ data: book }), + getMany: (resource, params) => + Promise.resolve({ + data: authors.filter(author => + params.ids.includes(author.id) + ), + }), + getList: (resource, params) => + Promise.reject(new Error('An error occured')), + } as any + } + history={history} + queryClient={ + new QueryClient({ defaultOptions: { queries: { retry: false } } }) + } + > + `${r.first_name} ${r.last_name}`} + /> + ( + + + + + + + + )} + /> + +); + +export const WithRadioButtonGroupInput = () => ( + + + `${record.first_name} ${record.last_name}` + } + /> + ( + { + console.log(data); + }, + }} + > + + + + + + + )} + /> + +); + +export const ErrorRadioButtonGroupInput = () => ( + Promise.resolve({ data: book }), + getMany: (resource, params) => + Promise.resolve({ + data: authors.filter(author => + params.ids.includes(author.id) + ), + }), + getList: (resource, params) => + Promise.reject(new Error('An error occured')), + } as any + } + history={history} + queryClient={ + new QueryClient({ defaultOptions: { queries: { retry: false } } }) + } + > + `${r.first_name} ${r.last_name}`} + /> + ( + + + + + + + + )} + /> + +); diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx index 6fdb0c89b7d..cef7cb9ecd5 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx @@ -7,7 +7,6 @@ import { ResourceContextProvider, } from 'ra-core'; -import { ReferenceError } from './ReferenceError'; import { AutocompleteInput } from './AutocompleteInput'; /** @@ -71,7 +70,7 @@ import { AutocompleteInput } from './AutocompleteInput'; * a `setFilters` function. You can call this function to filter the results. */ export const ReferenceInput = (props: ReferenceInputProps) => { - const { children, label, reference } = props; + const { children, reference } = props; const controllerProps = useReferenceInputController(props); @@ -79,12 +78,6 @@ export const ReferenceInput = (props: ReferenceInputProps) => { throw new Error(' only accepts a single child'); } - // This is not a form error but an unrecoverable error from the - // useReferenceInputController hook - if (controllerProps.error) { - return ; - } - return ( diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx index b9c20e14628..3c2b8632d13 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx @@ -112,7 +112,13 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { const inputLabel = useRef(null); - const { allChoices, isLoading, source, resource } = useChoicesContext({ + const { + allChoices, + isLoading, + error: fetchError, + source, + resource, + } = useChoicesContext({ choices: choicesProp, isLoading: isLoadingProp, isFetching: isFetchingProp, @@ -227,15 +233,11 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { - + { autoWidth labelId={`${label}-outlined-label`} multiple - error={(isTouched || isSubmitted) && invalid} + error={ + !!fetchError || ((isTouched || isSubmitted) && invalid) + } renderValue={(selected: any[]) => (
{selected @@ -276,10 +280,10 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { > {finalChoices.map(renderMenuItem)} - + diff --git a/packages/ra-ui-materialui/src/input/SelectInput.tsx b/packages/ra-ui-materialui/src/input/SelectInput.tsx index db23f23a876..7ee4cccf406 100644 --- a/packages/ra-ui-materialui/src/input/SelectInput.tsx +++ b/packages/ra-ui-materialui/src/input/SelectInput.tsx @@ -139,6 +139,7 @@ export const SelectInput = (props: SelectInputProps) => { const { allChoices, isLoading, + error: fetchError, source, resource, isFromReference, @@ -293,11 +294,11 @@ export const SelectInput = (props: SelectInputProps) => { ) } clearAlwaysVisible - error={(isTouched || isSubmitted) && invalid} + error={!!fetchError || ((isTouched || isSubmitted) && invalid)} helperText={ }