Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update <SelectArrayInput> to use default record representation when used inside <ReferenceArrayInput> #9902

Merged
merged 6 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
DifferentIdTypes,
TranslateChoice,
InsideArrayInput,
InsideReferenceInput,
InsideReferenceInputDefaultValue,
djhi marked this conversation as resolved.
Show resolved Hide resolved
} from './SelectArrayInput.stories';

describe('<SelectArrayInput />', () => {
Expand Down Expand Up @@ -177,7 +179,7 @@ describe('<SelectArrayInput />', () => {
it('should use optionText with an element value as text identifier', () => {
const Foobar = () => {
const record = useRecordContext();
return <span>{record.foobar}</span>;
return <span>{record?.foobar}</span>;
};
render(
<AdminContext dataProvider={testDataProvider()}>
Expand Down Expand Up @@ -560,7 +562,7 @@ describe('<SelectArrayInput />', () => {
});
});

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' };
Expand Down Expand Up @@ -603,7 +605,7 @@ describe('<SelectArrayInput />', () => {
});
});

it('should show selected values when ids type are inconsistant', async () => {
it('should show selected values when ids type are inconsistent', async () => {
render(<DifferentIdTypes />);
await waitFor(() => {
expect(screen.queryByText('artist_1')).not.toBeNull();
Expand Down Expand Up @@ -667,4 +669,25 @@ describe('<SelectArrayInput />', () => {
fireEvent.click(screen.getByLabelText('Add'));
expect(await screen.findAllByText('Foo')).toHaveLength(2);
});

describe('inside ReferenceInput', () => {
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
it('should use the recordRepresentation as optionText', async () => {
render(<InsideReferenceInput />);
djhi marked this conversation as resolved.
Show resolved Hide resolved
await screen.findByText('Leo Tolstoy');
});
it('should not change an undefined value to empty string', async () => {
const onSuccess = jest.fn();
render(<InsideReferenceInputDefaultValue onSuccess={onSuccess} />);
djhi marked this conversation as resolved.
Show resolved Hide resolved
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()
);
});
});
});
});
209 changes: 195 additions & 14 deletions packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 (
<div style={{ backgroundColor: 'lightgrey' }}>
{source} value in form:&nbsp;
<code>
{JSON.stringify(value)} ({typeof value})
</code>
</div>
);
};

export const Basic = () => (
<AdminContext i18nProvider={i18nProvider} defaultTheme="light">
<Create
Expand Down Expand Up @@ -122,7 +112,7 @@ export const InsideArrayInput = () => (
/>
</SimpleFormIterator>
</ArrayInput>
<FormInspector source="items" />
<FormInspector name="items" />
</SimpleForm>
</Create>
</AdminContext>
Expand Down Expand Up @@ -337,3 +327,194 @@ export const TranslateChoice = () => {
</AdminContext>
);
};

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 = () => (
djhi marked this conversation as resolved.
Show resolved Hide resolved
<TestMemoryRouter initialEntries={['/books/1']}>
<Admin dataProvider={dataProviderWithAuthors}>
<Resource
name="authors"
recordRepresentation={record =>
`${record.first_name} ${record.last_name}`
}
/>
<Resource
name="books"
edit={() => (
<Edit
mutationMode="pessimistic"
mutationOptions={{
onSuccess: data => {
console.log(data);
},
}}
>
<SimpleForm>
<ReferenceArrayInput
reference="authors"
source="authors"
>
<SelectArrayInput />
</ReferenceArrayInput>
<FormInspector name="authors" />
</SimpleForm>
</Edit>
)}
/>
</Admin>
</TestMemoryRouter>
);

export const InsideReferenceInputDefaultValue = ({
djhi marked this conversation as resolved.
Show resolved Hide resolved
onSuccess = console.log,
}) => (
<TestMemoryRouter initialEntries={['/books/1']}>
<Admin
dataProvider={{
...dataProviderWithAuthors,
getOne: () =>
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,
},
}),
}}
>
<Resource
name="authors"
recordRepresentation={record =>
`${record.first_name} ${record.last_name}`
}
/>
<Resource
name="books"
edit={() => (
<Edit
mutationMode="pessimistic"
mutationOptions={{ onSuccess }}
>
<SimpleForm>
<TextInput source="title" />
<ReferenceArrayInput
reference="authors"
source="authors"
>
<SelectArrayInput />
</ReferenceArrayInput>
<FormInspector name="authors" />
</SimpleForm>
</Edit>
)}
/>
</Admin>
</TestMemoryRouter>
);

export const InsideReferenceInputWithError = () => (
djhi marked this conversation as resolved.
Show resolved Hide resolved
<TestMemoryRouter initialEntries={['/books/1']}>
<Admin
dataProvider={{
...dataProviderWithAuthors,
getList: () =>
Promise.reject(
new Error('Error while fetching the authors')
),
}}
>
<Resource
name="authors"
recordRepresentation={record =>
`${record.first_name} ${record.last_name}`
}
/>
<Resource
name="books"
edit={() => (
<Edit
mutationMode="pessimistic"
mutationOptions={{
onSuccess: data => {
console.log(data);
},
}}
>
<SimpleForm>
<ReferenceArrayInput
reference="authors"
source="authors"
>
<SelectArrayInput />
</ReferenceArrayInput>
<FormInspector name="authors" />
</SimpleForm>
</Edit>
)}
/>
</Admin>
</TestMemoryRouter>
);
21 changes: 13 additions & 8 deletions packages/ra-ui-materialui/src/input/SelectArrayInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
useChoicesContext,
useChoices,
RaRecord,
useGetRecordRepresentation,
} from 'ra-core';
import { InputHelperText } from './InputHelperText';
import { FormControlProps } from '@mui/material/FormControl';
Expand Down Expand Up @@ -105,7 +106,7 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => {
onChange,
onCreate,
options = defaultOptions,
optionText = 'name',
optionText,
optionValue = 'id',
parse,
resource: resourceProp,
Expand Down Expand Up @@ -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,
Expand All @@ -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<HTMLInputElement> | RaRecord) => {
// We might receive an event from the mui component
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-ui-materialui/src/input/SelectInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ describe('<SelectInput />', () => {
const Foobar = () => {
const record = useRecordContext();
return (
<span data-value={record.id} aria-label={record.foobar} />
<span data-value={record?.id} aria-label={record?.foobar} />
);
};
render(
Expand Down
5 changes: 4 additions & 1 deletion packages/ra-ui-materialui/src/input/SelectInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,10 @@ export const InsideReferenceInputWithError = () => (
<Admin
dataProvider={{
...dataProviderWithAuthors,
getList: () => Promise.reject('error'),
getList: () =>
Promise.reject(
new Error('Error while fetching the authors')
),
}}
>
<Resource
Expand Down
Loading