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

Fix SelectInput and AutocompleteInput change empty references #8234

Merged
merged 1 commit into from
Oct 5, 2022
Merged
Changes from all commits
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
96 changes: 58 additions & 38 deletions packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import { AutocompleteInput } from './AutocompleteInput';
import { useCreateSuggestionContext } from './useSupportCreateSuggestion';
import {
InsideReferenceInput,
InsideReferenceInputDefaultValue,
VeryLargeOptionsNumber,
} from './AutocompleteInput.stories';
import { act } from '@testing-library/react-hooks';
@@ -1150,48 +1151,67 @@ describe('<AutocompleteInput />', () => {
expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
});

it('should work inside a ReferenceInput field', async () => {
render(<InsideReferenceInput />);
await waitFor(() => {
expect(
(screen.getByRole('textbox') as HTMLInputElement).value
).toBe('Leo Tolstoy');
});
screen.getByRole('textbox').focus();
fireEvent.click(screen.getByLabelText('Clear value'));
await waitFor(() => {
expect(screen.getByRole('listbox').children).toHaveLength(5);
});
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'Vic' },
describe('Inside <ReferenceInput>', () => {
it('should work inside a ReferenceInput field', async () => {
render(<InsideReferenceInput />);
await waitFor(() => {
expect(
(screen.getByRole('textbox') as HTMLInputElement).value
).toBe('Leo Tolstoy');
});
screen.getByRole('textbox').focus();
fireEvent.click(screen.getByLabelText('Clear value'));
await waitFor(() => {
expect(screen.getByRole('listbox').children).toHaveLength(5);
});
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'Vic' },
});
await waitFor(
() => {
expect(screen.getByRole('listbox').children).toHaveLength(
1
);
},
{ timeout: 2000 }
);
expect(screen.queryByText('Leo Tolstoy')).toBeNull();
});
await waitFor(
() => {
expect(screen.getByRole('listbox').children).toHaveLength(1);
},
{ timeout: 2000 }
);
expect(screen.queryByText('Leo Tolstoy')).toBeNull();
});

it('should allow to clear the value inside a ReferenceInput field', async () => {
render(<InsideReferenceInput />);
await waitFor(() => {
expect(
(screen.getByRole('textbox') as HTMLInputElement).value
).toBe('Leo Tolstoy');
it('should allow to clear the value inside a ReferenceInput field', async () => {
render(<InsideReferenceInput />);
await waitFor(() => {
expect(
(screen.getByRole('textbox') as HTMLInputElement).value
).toBe('Leo Tolstoy');
});
fireEvent.click(screen.getByLabelText('Clear value'));
userEvent.tab();
// Couldn't reproduce the infinite loop issue without this timeout
// See https://github.com/marmelab/react-admin/issues/7482
await new Promise(resolve => setTimeout(resolve, 2000));
await waitFor(() => {
expect(
(screen.getByRole('textbox') as HTMLInputElement).value
).toEqual('');
});
expect(screen.queryByText('Leo Tolstoy')).toBeNull();
});
fireEvent.click(screen.getByLabelText('Clear value'));
userEvent.tab();
// Couldn't reproduce the infinite loop issue without this timeout
// See https://github.com/marmelab/react-admin/issues/7482
await new Promise(resolve => setTimeout(resolve, 2000));
await waitFor(() => {
expect(
(screen.getByRole('textbox') as HTMLInputElement).value
).toEqual('');

it('should not change an undefined value to empty string', async () => {
const onSuccess = jest.fn();
render(<InsideReferenceInputDefaultValue onSuccess={onSuccess} />);
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({ author: null }),
expect.anything(),
expect.anything()
);
});
});
expect(screen.queryByText('Leo Tolstoy')).toBeNull();
});

it("should allow to edit the input if it's inside a FormDataConsumer", () => {
79 changes: 61 additions & 18 deletions packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import { Edit } from '../detail';
import { SimpleForm } from '../form';
import { AutocompleteInput } from './AutocompleteInput';
import { ReferenceInput } from './ReferenceInput';
import { TextInput } from './TextInput';
import { useCreateSuggestionContext } from './useSupportCreateSuggestion';

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

const BookEditWithReference = () => (
<Edit
mutationMode="pessimistic"
mutationOptions={{
onSuccess: data => {
console.log(data);
},
}}
>
<SimpleForm>
<ReferenceInput reference="authors" source="author">
<AutocompleteInput fullWidth optionText="name" />
</ReferenceInput>
</SimpleForm>
</Edit>
);

export const InsideReferenceInput = () => (
<Admin dataProvider={dataProviderWithAuthors} history={history}>
<Resource name="authors" />
<Resource name="books" edit={BookEditWithReference} />
<Resource
name="books"
edit={() => (
<Edit
mutationMode="pessimistic"
mutationOptions={{
onSuccess: data => {
console.log(data);
},
}}
>
<SimpleForm>
<ReferenceInput reference="authors" source="author">
<AutocompleteInput fullWidth optionText="name" />
</ReferenceInput>
</SimpleForm>
</Edit>
)}
/>
</Admin>
);

export const InsideReferenceInputDefaultValue = ({
onSuccess = console.log,
}) => (
<Admin
dataProvider={{
...dataProviderWithAuthors,
getOne: (resource, params) =>
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,
},
}),
}}
history={history}
>
<Resource name="authors" />
<Resource
name="books"
edit={() => (
<Edit
mutationMode="pessimistic"
mutationOptions={{ onSuccess }}
>
<SimpleForm>
<TextInput source="title" />
<ReferenceInput reference="authors" source="author">
<AutocompleteInput fullWidth optionText="name" />
</ReferenceInput>
</SimpleForm>
</Edit>
)}
/>
</Admin>
);

4 changes: 2 additions & 2 deletions packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
@@ -135,7 +135,7 @@ export const AutocompleteInput = <
createItemLabel,
createValue,
debounce: debounceDelay = 250,
defaultValue = '',
defaultValue,
emptyText,
emptyValue = '',
field: fieldOverride,
@@ -216,7 +216,7 @@ export const AutocompleteInput = <
fieldState: { error, invalid, isTouched },
formState: { isSubmitted },
} = useInput({
defaultValue,
defaultValue: defaultValue ?? (isFromReference ? null : ''),
id: idOverride,
field: fieldOverride,
fieldState: fieldStateOverride,
20 changes: 19 additions & 1 deletion packages/ra-ui-materialui/src/input/SelectInput.spec.tsx
Original file line number Diff line number Diff line change
@@ -11,7 +11,11 @@ import { AdminContext } from '../AdminContext';
import { SimpleForm } from '../form';
import { SelectInput } from './SelectInput';
import { useCreateSuggestionContext } from './useSupportCreateSuggestion';
import { InsideReferenceInput, Sort } from './SelectInput.stories';
import {
InsideReferenceInput,
InsideReferenceInputDefaultValue,
Sort,
} from './SelectInput.stories';

describe('<SelectInput />', () => {
const defaultProps = {
@@ -741,5 +745,19 @@ describe('<SelectInput />', () => {
render(<InsideReferenceInput />);
await screen.findByText('Leo Tolstoy');
});
it('should not change an undefined value to empty string', async () => {
const onSuccess = jest.fn();
render(<InsideReferenceInputDefaultValue onSuccess={onSuccess} />);
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({ author: null }),
expect.anything(),
expect.anything()
);
});
});
});
});
84 changes: 66 additions & 18 deletions packages/ra-ui-materialui/src/input/SelectInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import englishMessages from 'ra-language-english';
import { Create, Edit } from '../detail';
import { SimpleForm } from '../form';
import { SelectInput } from './SelectInput';
import { TextInput } from './TextInput';
import { ReferenceInput } from './ReferenceInput';

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

const BookEditWithReference = () => (
<Edit
mutationMode="pessimistic"
mutationOptions={{
onSuccess: data => {
console.log(data);
},
}}
>
<SimpleForm>
<ReferenceInput reference="authors" source="author">
<SelectInput />
</ReferenceInput>
</SimpleForm>
</Edit>
);

const history = createMemoryHistory({ initialEntries: ['/books/1'] });

export const InsideReferenceInput = () => (
@@ -225,6 +209,70 @@ export const InsideReferenceInput = () => (
`${record.first_name} ${record.last_name}`
}
/>
<Resource name="books" edit={BookEditWithReference} />
<Resource
name="books"
edit={() => (
<Edit
mutationMode="pessimistic"
mutationOptions={{
onSuccess: data => {
console.log(data);
},
}}
>
<SimpleForm>
<ReferenceInput reference="authors" source="author">
<SelectInput />
</ReferenceInput>
</SimpleForm>
</Edit>
)}
/>
</Admin>
);

export const InsideReferenceInputDefaultValue = ({
onSuccess = console.log,
}) => (
<Admin
dataProvider={{
...dataProviderWithAuthors,
getOne: (resource, params) =>
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,
},
}),
}}
history={history}
>
<Resource
name="authors"
recordRepresentation={record =>
`${record.first_name} ${record.last_name}`
}
/>
<Resource
name="books"
edit={() => (
<Edit
mutationMode="pessimistic"
mutationOptions={{ onSuccess }}
>
<SimpleForm>
<TextInput source="title" />
<ReferenceInput reference="authors" source="author">
<SelectInput />
</ReferenceInput>
</SimpleForm>
</Edit>
)}
/>
</Admin>
);
4 changes: 2 additions & 2 deletions packages/ra-ui-materialui/src/input/SelectInput.tsx
Original file line number Diff line number Diff line change
@@ -112,7 +112,7 @@ export const SelectInput = (props: SelectInputProps) => {
create,
createLabel,
createValue,
defaultValue = '',
defaultValue,
disableValue,
emptyText,
emptyValue,
@@ -167,7 +167,7 @@ export const SelectInput = (props: SelectInputProps) => {
isRequired,
formState: { isSubmitted },
} = useInput({
defaultValue,
defaultValue: defaultValue ?? (isFromReference ? null : ''),
parse: parse ?? isFromReference ? convertEmptyStringToNull : undefined,
format:
format ?? isFromReference ? convertNullToEmptyString : undefined,