diff --git a/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts b/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts
index ad421915a84..4ea74ebaa56 100644
--- a/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts
+++ b/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts
@@ -60,7 +60,7 @@ export const useReferenceArrayFieldController = (
const ids = useMemo(() => {
if (Array.isArray(value)) return value;
- console.warn(`Value of field '${source}' is not an array.`);
+ console.warn(`Value of field '${source}' is not an array.`, value);
return emptyArray;
}, [value, source]);
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx
index 5341c3786bd..6b3121cdefa 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx
@@ -18,6 +18,7 @@ import {
import { TextField } from './TextField';
import { SingleFieldList } from '../list';
import { AdminContext } from '../AdminContext';
+import { DifferentIdTypes } from './ReferenceArrayField.stories';
const theme = createTheme({});
@@ -78,7 +79,7 @@ describe('', () => {
);
expect(queryAllByRole('progressbar')).toHaveLength(0);
- expect(container.firstChild.textContent).not.toBeUndefined();
+ expect(container.firstChild?.textContent).not.toBeUndefined();
expect(getByText('hello')).not.toBeNull();
expect(getByText('world')).not.toBeNull();
});
@@ -106,7 +107,7 @@ describe('', () => {
);
expect(queryAllByRole('progressbar')).toHaveLength(0);
- expect(container.firstChild.textContent).toBe('');
+ expect(container.firstChild?.textContent).toBe('');
});
it('should support record with string identifier', () => {
@@ -138,7 +139,7 @@ describe('', () => {
);
expect(queryAllByRole('progressbar')).toHaveLength(0);
- expect(container.firstChild.textContent).not.toBeUndefined();
+ expect(container.firstChild?.textContent).not.toBeUndefined();
expect(getByText('hello')).not.toBeNull();
expect(getByText('world')).not.toBeNull();
});
@@ -301,4 +302,12 @@ describe('', () => {
await screen.findByText('tag:management');
await screen.findByText('tag:design');
});
+
+ it('should handle IDs of different types', async () => {
+ render();
+
+ expect(await screen.findByText('artist_1')).not.toBeNull();
+ expect(await screen.findByText('artist_2')).not.toBeNull();
+ expect(await screen.findByText('artist_3')).not.toBeNull();
+ });
});
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx
new file mode 100644
index 00000000000..4dff459efd3
--- /dev/null
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx
@@ -0,0 +1,43 @@
+import * as React from 'react';
+import fakeRestProvider from 'ra-data-fakerest';
+
+import { AdminContext } from '../AdminContext';
+import { Datagrid } from '../list';
+import { ReferenceArrayField } from './ReferenceArrayField';
+import { TextField } from './TextField';
+import { Show } from '../detail';
+import { CardContent } from '@mui/material';
+
+const fakeData = {
+ bands: [{ id: 1, name: 'band_1', members: [1, '2', '3'] }],
+ artists: [
+ { id: 1, name: 'artist_1' },
+ { id: 2, name: 'artist_2' },
+ { id: 3, name: 'artist_3' },
+ { id: 4, name: 'artist_4' },
+ ],
+};
+
+export default { title: 'ra-ui-materialui/fields/ReferenceArrayField' };
+
+export const DifferentIdTypes = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx
index 86f9199b2b2..64c312977e3 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx
@@ -17,6 +17,7 @@ import {
InsideReferenceInput,
InsideReferenceInputDefaultValue,
Nullable,
+ NullishValuesSupport,
VeryLargeOptionsNumber,
} from './AutocompleteInput.stories';
import { act } from '@testing-library/react-hooks';
@@ -1477,4 +1478,24 @@ describe('', () => {
expect(input.value).toEqual('');
});
});
+
+ it('should handle nullish values', async () => {
+ render();
+
+ const checkInputValue = async (label: string, expected: any) => {
+ const input = (await screen.findByLabelText(
+ label
+ )) as HTMLInputElement;
+ await waitFor(() => {
+ expect(input.value).toStrictEqual(expected);
+ });
+ };
+
+ await checkInputValue('prefers_empty-string', '');
+ await checkInputValue('prefers_null', '');
+ await checkInputValue('prefers_undefined', '');
+ await checkInputValue('prefers_zero-string', '0');
+ await checkInputValue('prefers_zero-number', '0');
+ await checkInputValue('prefers_valid-value', '1');
+ });
});
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx
index d26e7e0a959..4c509e26a62 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx
@@ -1,16 +1,26 @@
import * as React from 'react';
import { Admin, AdminContext } from 'react-admin';
-import { Resource, required, useCreate, useRecordContext } from 'ra-core';
+import {
+ Resource,
+ required,
+ useCreate,
+ useRecordContext,
+ ListBase,
+ useListContext,
+ RecordContextProvider,
+} from 'ra-core';
import { createMemoryHistory } from 'history';
import {
Dialog,
DialogContent,
- TextField,
DialogActions,
Button,
Stack,
+ TextField,
Typography,
+ Box,
} from '@mui/material';
+import fakeRestProvider from 'ra-data-fakerest';
import { Edit } from '../detail';
import { SimpleForm } from '../form';
@@ -791,3 +801,77 @@ export const EmptyText = () => (
);
+
+const nullishValuesFakeData = {
+ fans: [
+ { id: 'null', name: 'null', prefers: null },
+ { id: 'undefined', name: 'undefined', prefers: undefined },
+ { id: 'empty-string', name: 'empty string', prefers: '' },
+ { id: 'zero-string', name: '0', prefers: 0 },
+ { id: 'zero-number', name: '0', prefers: '0' },
+ { id: 'valid-value', name: '1', prefers: 1 },
+ ],
+ artists: [{ id: 0 }, { id: 1 }],
+};
+
+const FanList = props => {
+ const { data } = useListContext();
+ return data ? (
+ <>
+ {data.map(fan => (
+
+
+
+
+ Fan #{fan.id}
+
+ {`${
+ fan.name
+ } [${typeof fan.prefers}]`}
+
+
+
+ >}>
+ option.id}
+ choices={nullishValuesFakeData.artists}
+ helperText={false}
+ />
+
+
+
+
+ ))}
+ >
+ ) : (
+ <>Loading>
+ );
+};
+
+export const NullishValuesSupport = () => {
+ return (
+
+
+ Test nullish values
+
+
+ Story demonstrating nullish values support: each fan specify a
+ preferred artist. The prefer
value is evaluated
+ against artist IDs.
+
+
+
+
+
+ );
+};
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
index cf4b9d9ef4c..0d44165c802 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
@@ -525,7 +525,7 @@ If you provided a React element for the optionText prop, you must also provide t
]);
const isOptionEqualToValue = (option, value) => {
- return getChoiceValue(option) === getChoiceValue(value);
+ return String(getChoiceValue(option)) === String(getChoiceValue(value));
};
return (
@@ -740,11 +740,17 @@ const getSelectedItems = (
if (multiple) {
return (value || [])
.map(item =>
- choices.find(choice => item === get(choice, optionValue))
+ choices.find(
+ choice => String(item) === String(get(choice, optionValue))
+ )
)
.filter(item => !!item);
}
- return choices.find(choice => get(choice, optionValue) === value) || '';
+ return (
+ choices.find(
+ choice => String(get(choice, optionValue)) === String(value)
+ ) || ''
+ );
};
const DefaultFilterToQuery = searchText => ({ q: searchText });
diff --git a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.spec.tsx
index 8283158f9c5..6ea2d3db8e3 100644
--- a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.spec.tsx
+++ b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.spec.tsx
@@ -21,6 +21,7 @@ import { DatagridInput } from './DatagridInput';
import { TextField } from '../field';
import { ReferenceArrayInput } from './ReferenceArrayInput';
import { SelectArrayInput } from './SelectArrayInput';
+import { DifferentIdTypes } from './ReferenceArrayInput.stories';
describe('', () => {
const defaultProps = {
@@ -85,7 +86,10 @@ describe('', () => {
const dataProvider = testDataProvider({
getList: () =>
// @ts-ignore
- Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }),
+ Promise.resolve({
+ data: [{ id: 1 }, { id: 2 }],
+ total: 2,
+ }),
});
render(
@@ -244,4 +248,44 @@ describe('', () => {
});
});
});
+
+ it('should support different types of ids', async () => {
+ render();
+ await screen.findByText('#1', {
+ selector: 'div.MuiChip-root .MuiChip-label',
+ });
+ expect(
+ screen.queryByText('#2', {
+ selector: 'div.MuiChip-root .MuiChip-label',
+ })
+ ).not.toBeNull();
+ expect(
+ screen.queryByText('#3', { selector: 'div.MuiChip-root' })
+ ).toBeNull();
+ });
+
+ it('should unselect a value when types of ids are different', async () => {
+ render();
+
+ const chip1 = await screen.findByText('#1', {
+ selector: '.MuiChip-label',
+ });
+ const chip2 = await screen.findByText('#2', {
+ selector: '.MuiChip-label',
+ });
+
+ if (chip2.nextSibling) fireEvent.click(chip2.nextSibling);
+ expect(
+ screen.queryByText('#2', {
+ selector: '.MuiChip-label',
+ })
+ ).toBeNull();
+
+ if (chip1.nextSibling) fireEvent.click(chip1.nextSibling);
+ expect(
+ screen.queryByText('#1', {
+ selector: '.MuiChip-label',
+ })
+ ).toBeNull();
+ });
});
diff --git a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx
index ae2bef825d0..e080109fed7 100644
--- a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx
+++ b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx
@@ -1,14 +1,15 @@
import * as React from 'react';
import { createMemoryHistory } from 'history';
-import { Form, testDataProvider } from 'ra-core';
+import { DataProvider, Form, testDataProvider } from 'ra-core';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import { Admin, Resource } from 'react-admin';
+import fakeRestProvider from 'ra-data-fakerest';
import { AdminContext } from '../AdminContext';
-import { Create } from '../detail';
+import { Create, Edit } from '../detail';
import { SimpleForm } from '../form';
-import { DatagridInput } from '../input';
+import { DatagridInput, TextInput } from '../input';
import { TextField } from '../field';
import { ReferenceArrayInput } from './ReferenceArrayInput';
import { AutocompleteArrayInput } from './AutocompleteArrayInput';
@@ -85,11 +86,13 @@ export const WithAutocompleteInput = () => (
export const ErrorAutocomplete = () => (
Promise.reject(new Error('fetch error')),
- getMany: () =>
- Promise.resolve({ data: [{ id: 5, name: 'test1' }] }),
- }}
+ dataProvider={
+ ({
+ getList: () => Promise.reject(new Error('fetch error')),
+ getMany: () =>
+ Promise.resolve({ data: [{ id: 5, name: 'test1' }] }),
+ } as unknown) as DataProvider
+ }
i18nProvider={i18nProvider}
>