Skip to content

Commit e84f530

Browse files
authored
Merge pull request #8234 from marmelab/referenceinput-defaultvalue
Fix SelectInput and AutocompleteInput change empty references
2 parents e3b43ab + 5919e5f commit e84f530

6 files changed

+208
-79
lines changed

packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx

+58-38
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { AutocompleteInput } from './AutocompleteInput';
1414
import { useCreateSuggestionContext } from './useSupportCreateSuggestion';
1515
import {
1616
InsideReferenceInput,
17+
InsideReferenceInputDefaultValue,
1718
VeryLargeOptionsNumber,
1819
} from './AutocompleteInput.stories';
1920
import { act } from '@testing-library/react-hooks';
@@ -1150,48 +1151,67 @@ describe('<AutocompleteInput />', () => {
11501151
expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
11511152
});
11521153

1153-
it('should work inside a ReferenceInput field', async () => {
1154-
render(<InsideReferenceInput />);
1155-
await waitFor(() => {
1156-
expect(
1157-
(screen.getByRole('textbox') as HTMLInputElement).value
1158-
).toBe('Leo Tolstoy');
1159-
});
1160-
screen.getByRole('textbox').focus();
1161-
fireEvent.click(screen.getByLabelText('Clear value'));
1162-
await waitFor(() => {
1163-
expect(screen.getByRole('listbox').children).toHaveLength(5);
1164-
});
1165-
fireEvent.change(screen.getByRole('textbox'), {
1166-
target: { value: 'Vic' },
1154+
describe('Inside <ReferenceInput>', () => {
1155+
it('should work inside a ReferenceInput field', async () => {
1156+
render(<InsideReferenceInput />);
1157+
await waitFor(() => {
1158+
expect(
1159+
(screen.getByRole('textbox') as HTMLInputElement).value
1160+
).toBe('Leo Tolstoy');
1161+
});
1162+
screen.getByRole('textbox').focus();
1163+
fireEvent.click(screen.getByLabelText('Clear value'));
1164+
await waitFor(() => {
1165+
expect(screen.getByRole('listbox').children).toHaveLength(5);
1166+
});
1167+
fireEvent.change(screen.getByRole('textbox'), {
1168+
target: { value: 'Vic' },
1169+
});
1170+
await waitFor(
1171+
() => {
1172+
expect(screen.getByRole('listbox').children).toHaveLength(
1173+
1
1174+
);
1175+
},
1176+
{ timeout: 2000 }
1177+
);
1178+
expect(screen.queryByText('Leo Tolstoy')).toBeNull();
11671179
});
1168-
await waitFor(
1169-
() => {
1170-
expect(screen.getByRole('listbox').children).toHaveLength(1);
1171-
},
1172-
{ timeout: 2000 }
1173-
);
1174-
expect(screen.queryByText('Leo Tolstoy')).toBeNull();
1175-
});
11761180

1177-
it('should allow to clear the value inside a ReferenceInput field', async () => {
1178-
render(<InsideReferenceInput />);
1179-
await waitFor(() => {
1180-
expect(
1181-
(screen.getByRole('textbox') as HTMLInputElement).value
1182-
).toBe('Leo Tolstoy');
1181+
it('should allow to clear the value inside a ReferenceInput field', async () => {
1182+
render(<InsideReferenceInput />);
1183+
await waitFor(() => {
1184+
expect(
1185+
(screen.getByRole('textbox') as HTMLInputElement).value
1186+
).toBe('Leo Tolstoy');
1187+
});
1188+
fireEvent.click(screen.getByLabelText('Clear value'));
1189+
userEvent.tab();
1190+
// Couldn't reproduce the infinite loop issue without this timeout
1191+
// See https://github.com/marmelab/react-admin/issues/7482
1192+
await new Promise(resolve => setTimeout(resolve, 2000));
1193+
await waitFor(() => {
1194+
expect(
1195+
(screen.getByRole('textbox') as HTMLInputElement).value
1196+
).toEqual('');
1197+
});
1198+
expect(screen.queryByText('Leo Tolstoy')).toBeNull();
11831199
});
1184-
fireEvent.click(screen.getByLabelText('Clear value'));
1185-
userEvent.tab();
1186-
// Couldn't reproduce the infinite loop issue without this timeout
1187-
// See https://github.com/marmelab/react-admin/issues/7482
1188-
await new Promise(resolve => setTimeout(resolve, 2000));
1189-
await waitFor(() => {
1190-
expect(
1191-
(screen.getByRole('textbox') as HTMLInputElement).value
1192-
).toEqual('');
1200+
1201+
it('should not change an undefined value to empty string', async () => {
1202+
const onSuccess = jest.fn();
1203+
render(<InsideReferenceInputDefaultValue onSuccess={onSuccess} />);
1204+
const input = await screen.findByDisplayValue('War and Peace');
1205+
fireEvent.change(input, { target: { value: 'War' } });
1206+
screen.getByText('Save').click();
1207+
await waitFor(() => {
1208+
expect(onSuccess).toHaveBeenCalledWith(
1209+
expect.objectContaining({ author: null }),
1210+
expect.anything(),
1211+
expect.anything()
1212+
);
1213+
});
11931214
});
1194-
expect(screen.queryByText('Leo Tolstoy')).toBeNull();
11951215
});
11961216

11971217
it("should allow to edit the input if it's inside a FormDataConsumer", () => {

packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx

+61-18
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Edit } from '../detail';
1616
import { SimpleForm } from '../form';
1717
import { AutocompleteInput } from './AutocompleteInput';
1818
import { ReferenceInput } from './ReferenceInput';
19+
import { TextInput } from './TextInput';
1920
import { useCreateSuggestionContext } from './useSupportCreateSuggestion';
2021

2122
export default { title: 'ra-ui-materialui/input/AutocompleteInput' };
@@ -447,27 +448,69 @@ const dataProviderWithAuthors = {
447448
},
448449
} as any;
449450

450-
const BookEditWithReference = () => (
451-
<Edit
452-
mutationMode="pessimistic"
453-
mutationOptions={{
454-
onSuccess: data => {
455-
console.log(data);
456-
},
457-
}}
458-
>
459-
<SimpleForm>
460-
<ReferenceInput reference="authors" source="author">
461-
<AutocompleteInput fullWidth optionText="name" />
462-
</ReferenceInput>
463-
</SimpleForm>
464-
</Edit>
465-
);
466-
467451
export const InsideReferenceInput = () => (
468452
<Admin dataProvider={dataProviderWithAuthors} history={history}>
469453
<Resource name="authors" />
470-
<Resource name="books" edit={BookEditWithReference} />
454+
<Resource
455+
name="books"
456+
edit={() => (
457+
<Edit
458+
mutationMode="pessimistic"
459+
mutationOptions={{
460+
onSuccess: data => {
461+
console.log(data);
462+
},
463+
}}
464+
>
465+
<SimpleForm>
466+
<ReferenceInput reference="authors" source="author">
467+
<AutocompleteInput fullWidth optionText="name" />
468+
</ReferenceInput>
469+
</SimpleForm>
470+
</Edit>
471+
)}
472+
/>
473+
</Admin>
474+
);
475+
476+
export const InsideReferenceInputDefaultValue = ({
477+
onSuccess = console.log,
478+
}) => (
479+
<Admin
480+
dataProvider={{
481+
...dataProviderWithAuthors,
482+
getOne: (resource, params) =>
483+
Promise.resolve({
484+
data: {
485+
id: 1,
486+
title: 'War and Peace',
487+
// trigger default value
488+
author: undefined,
489+
summary:
490+
"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.",
491+
year: 1869,
492+
},
493+
}),
494+
}}
495+
history={history}
496+
>
497+
<Resource name="authors" />
498+
<Resource
499+
name="books"
500+
edit={() => (
501+
<Edit
502+
mutationMode="pessimistic"
503+
mutationOptions={{ onSuccess }}
504+
>
505+
<SimpleForm>
506+
<TextInput source="title" />
507+
<ReferenceInput reference="authors" source="author">
508+
<AutocompleteInput fullWidth optionText="name" />
509+
</ReferenceInput>
510+
</SimpleForm>
511+
</Edit>
512+
)}
513+
/>
471514
</Admin>
472515
);
473516

packages/ra-ui-materialui/src/input/AutocompleteInput.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export const AutocompleteInput = <
135135
createItemLabel,
136136
createValue,
137137
debounce: debounceDelay = 250,
138-
defaultValue = '',
138+
defaultValue,
139139
emptyText,
140140
emptyValue = '',
141141
field: fieldOverride,
@@ -216,7 +216,7 @@ export const AutocompleteInput = <
216216
fieldState: { error, invalid, isTouched },
217217
formState: { isSubmitted },
218218
} = useInput({
219-
defaultValue,
219+
defaultValue: defaultValue ?? (isFromReference ? null : ''),
220220
id: idOverride,
221221
field: fieldOverride,
222222
fieldState: fieldStateOverride,

packages/ra-ui-materialui/src/input/SelectInput.spec.tsx

+19-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { AdminContext } from '../AdminContext';
1111
import { SimpleForm } from '../form';
1212
import { SelectInput } from './SelectInput';
1313
import { useCreateSuggestionContext } from './useSupportCreateSuggestion';
14-
import { InsideReferenceInput, Sort } from './SelectInput.stories';
14+
import {
15+
InsideReferenceInput,
16+
InsideReferenceInputDefaultValue,
17+
Sort,
18+
} from './SelectInput.stories';
1519

1620
describe('<SelectInput />', () => {
1721
const defaultProps = {
@@ -741,5 +745,19 @@ describe('<SelectInput />', () => {
741745
render(<InsideReferenceInput />);
742746
await screen.findByText('Leo Tolstoy');
743747
});
748+
it('should not change an undefined value to empty string', async () => {
749+
const onSuccess = jest.fn();
750+
render(<InsideReferenceInputDefaultValue onSuccess={onSuccess} />);
751+
const input = await screen.findByDisplayValue('War and Peace');
752+
fireEvent.change(input, { target: { value: 'War' } });
753+
screen.getByText('Save').click();
754+
await waitFor(() => {
755+
expect(onSuccess).toHaveBeenCalledWith(
756+
expect.objectContaining({ author: null }),
757+
expect.anything(),
758+
expect.anything()
759+
);
760+
});
761+
});
744762
});
745763
});

packages/ra-ui-materialui/src/input/SelectInput.stories.tsx

+66-18
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import englishMessages from 'ra-language-english';
88
import { Create, Edit } from '../detail';
99
import { SimpleForm } from '../form';
1010
import { SelectInput } from './SelectInput';
11+
import { TextInput } from './TextInput';
1112
import { ReferenceInput } from './ReferenceInput';
1213

1314
export default { title: 'ra-ui-materialui/input/SelectInput' };
@@ -198,23 +199,6 @@ const dataProviderWithAuthors = {
198199
},
199200
} as any;
200201

201-
const BookEditWithReference = () => (
202-
<Edit
203-
mutationMode="pessimistic"
204-
mutationOptions={{
205-
onSuccess: data => {
206-
console.log(data);
207-
},
208-
}}
209-
>
210-
<SimpleForm>
211-
<ReferenceInput reference="authors" source="author">
212-
<SelectInput />
213-
</ReferenceInput>
214-
</SimpleForm>
215-
</Edit>
216-
);
217-
218202
const history = createMemoryHistory({ initialEntries: ['/books/1'] });
219203

220204
export const InsideReferenceInput = () => (
@@ -225,6 +209,70 @@ export const InsideReferenceInput = () => (
225209
`${record.first_name} ${record.last_name}`
226210
}
227211
/>
228-
<Resource name="books" edit={BookEditWithReference} />
212+
<Resource
213+
name="books"
214+
edit={() => (
215+
<Edit
216+
mutationMode="pessimistic"
217+
mutationOptions={{
218+
onSuccess: data => {
219+
console.log(data);
220+
},
221+
}}
222+
>
223+
<SimpleForm>
224+
<ReferenceInput reference="authors" source="author">
225+
<SelectInput />
226+
</ReferenceInput>
227+
</SimpleForm>
228+
</Edit>
229+
)}
230+
/>
231+
</Admin>
232+
);
233+
234+
export const InsideReferenceInputDefaultValue = ({
235+
onSuccess = console.log,
236+
}) => (
237+
<Admin
238+
dataProvider={{
239+
...dataProviderWithAuthors,
240+
getOne: (resource, params) =>
241+
Promise.resolve({
242+
data: {
243+
id: 1,
244+
title: 'War and Peace',
245+
// trigger default value
246+
author: undefined,
247+
summary:
248+
"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.",
249+
year: 1869,
250+
},
251+
}),
252+
}}
253+
history={history}
254+
>
255+
<Resource
256+
name="authors"
257+
recordRepresentation={record =>
258+
`${record.first_name} ${record.last_name}`
259+
}
260+
/>
261+
<Resource
262+
name="books"
263+
edit={() => (
264+
<Edit
265+
mutationMode="pessimistic"
266+
mutationOptions={{ onSuccess }}
267+
>
268+
<SimpleForm>
269+
<TextInput source="title" />
270+
<ReferenceInput reference="authors" source="author">
271+
<SelectInput />
272+
</ReferenceInput>
273+
</SimpleForm>
274+
</Edit>
275+
)}
276+
/>
229277
</Admin>
230278
);

packages/ra-ui-materialui/src/input/SelectInput.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export const SelectInput = (props: SelectInputProps) => {
112112
create,
113113
createLabel,
114114
createValue,
115-
defaultValue = '',
115+
defaultValue,
116116
disableValue,
117117
emptyText,
118118
emptyValue,
@@ -167,7 +167,7 @@ export const SelectInput = (props: SelectInputProps) => {
167167
isRequired,
168168
formState: { isSubmitted },
169169
} = useInput({
170-
defaultValue,
170+
defaultValue: defaultValue ?? (isFromReference ? null : ''),
171171
parse: parse ?? isFromReference ? convertEmptyStringToNull : undefined,
172172
format:
173173
format ?? isFromReference ? convertNullToEmptyString : undefined,

0 commit comments

Comments
 (0)