Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
a2794c5
Add react-hook-form dependency
djhi Jan 14, 2022
b98b1f4
Migrate FormWithRedirect to react-hook-form
djhi Jan 14, 2022
657e029
Fix side effects and warnings in simple example
djhi Jan 14, 2022
9dfaf4e
Migrate useInput
djhi Jan 14, 2022
96e282d
MIgrate useFormGroup
djhi Jan 14, 2022
fac84a1
Update choices hooks
djhi Jan 14, 2022
1ae47b4
Update exports
djhi Jan 14, 2022
03e1bce
Update form components
djhi Jan 14, 2022
c5f09f1
Upgrade guide todo
djhi Jan 14, 2022
1bc9b53
Fix for validation
djhi Jan 15, 2022
53af961
Fix useWarnWhenUnsavedChanges
djhi Jan 15, 2022
b7d0348
Fix forms
djhi Jan 15, 2022
746b9e6
Simplify UserCreate
djhi Jan 15, 2022
7b4f994
Fix ResettableTextField, TextInput and SelectInput
djhi Jan 16, 2022
30a20d9
Fix BooleanInput
djhi Jan 16, 2022
6581925
Apply format & parse in useInput
djhi Jan 16, 2022
72fa325
Fix NullableBooleanInput
djhi Jan 16, 2022
cb21b41
Add tests for useInput format & parse
djhi Jan 16, 2022
e63966a
Fix FilterForm
djhi Jan 17, 2022
08532da
Fix tests to use AdminContext instead of CoreAdminContext
djhi Jan 17, 2022
4adbdc2
Fix DateInput
djhi Jan 17, 2022
6b8bcb5
Migrate DateTimeInput
djhi Jan 17, 2022
74feef7
Migrate CheckboxGroupInput
djhi Jan 17, 2022
a291230
Add missing file for CommonInputProps
djhi Jan 17, 2022
0c42654
Migrate NumberInput
djhi Jan 17, 2022
4e0e7a0
Migrate RadioButtonGroupInput
djhi Jan 17, 2022
bf066cd
Migrate SearchInput
djhi Jan 17, 2022
ca30a3e
Fix common components
djhi Jan 17, 2022
f2763f3
Fix simple example configuration
djhi Jan 17, 2022
410a506
Fix useReferenceInputController
djhi Jan 17, 2022
10cd9b4
Migrate ReferenceInput
djhi Jan 17, 2022
d8f1467
Fix simple example Comment with posts reference
djhi Jan 17, 2022
7f91af9
Migrate Autocomplete inputs
djhi Jan 17, 2022
a69a9a9
Better CommonInputProps
djhi Jan 17, 2022
0288565
Fix SelectInput sanitization & common sanitize
djhi Jan 17, 2022
be0fd8e
Fix ReferenceInput
djhi Jan 17, 2022
1a8236c
Migrate SelectArrayInput
djhi Jan 18, 2022
5b9d92f
Fix choices related hooks
djhi Jan 18, 2022
2de38c9
Migrate FormDataConsumer
djhi Jan 18, 2022
3e41aef
Ensure Form submit don't propagate to upper forms
djhi Jan 18, 2022
a75a81e
Fix BooleanInput styles
djhi Jan 18, 2022
8ca98d2
Migrate RichTextInput
djhi Jan 18, 2022
85eb5a3
Migrate FileInput and ImageInput
djhi Jan 18, 2022
e47e0e9
Migrate ReferenceArrayInput
djhi Jan 18, 2022
0c01996
Fix Datagrid warnings
djhi Jan 18, 2022
24316bd
Fix TranslatableInputs
djhi Jan 18, 2022
0fa8c45
Revert unnecessary changes for this PR
djhi Jan 19, 2022
81a9087
Migrate ArrayInput
djhi Jan 19, 2022
d8eb7f4
Fix delete buttons tests
djhi Jan 19, 2022
562e5c4
Update react-hook-form
djhi Jan 20, 2022
96fdc2a
Fix BooleanInput name attribute
djhi Jan 20, 2022
d3dec66
Fix default values and create view title
djhi Jan 20, 2022
712e359
Fix toolbar access to record
djhi Jan 20, 2022
e93df1d
Migrate ArrayInput
djhi Jan 20, 2022
d5f5aa0
Fix simple example
djhi Jan 20, 2022
3e6de27
Fix custom form e2e tests
djhi Jan 20, 2022
fb12266
Migrate more e2e tests
djhi Jan 20, 2022
24ceb52
Fix wrong rebase
djhi Jan 20, 2022
48c6292
Fix FileInput
djhi Jan 20, 2022
a5b742e
Fix ImageInput
djhi Jan 20, 2022
07435fd
Fix RichTextInput
djhi Jan 20, 2022
8dc875f
Fix FormWithRedirect props
djhi Jan 20, 2022
0c03303
Fix ra-no-code
djhi Jan 20, 2022
d081a90
Fix warnings in choices inputs
Jan 21, 2022
b33ad8f
Fix FileInput tests
Jan 21, 2022
c40a074
Fix FilterForm
Jan 21, 2022
6764d61
Remove warnings in SaveButton tests
Jan 21, 2022
e8bfa1e
Fix FormTab warnings in tests
Jan 21, 2022
e163e5e
Fix linter warning
Jan 21, 2022
d130415
Fix validation
Jan 21, 2022
3c7c04f
Remove react-hook-form devtool
Jan 21, 2022
1a2a769
Fix default values
Jan 21, 2022
6a4254a
Fix edit and list e2e tests
Jan 21, 2022
8127406
Fix edit e2e tests
Jan 21, 2022
2f6bc25
More submit trick in e2e tests
Jan 21, 2022
9f527c8
Restore DateTimeInput test
Jan 21, 2022
bae1fb2
Fix choices inputs source prop
Jan 21, 2022
f4cdf3d
Fix FormWithRedirect submit handler
Jan 21, 2022
0e9c375
Fix CloneButton types
Jan 21, 2022
ea9de80
Fix simple example conpilation
Jan 21, 2022
e73070d
Fix input common props
Jan 21, 2022
8dccee0
Fix demo
Jan 21, 2022
e6247ba
Rename FormWithRedirect to Form
Jan 22, 2022
4907772
Update upgrade guide
Jan 22, 2022
d3ec691
Update Login Forms
Jan 24, 2022
4442dd4
Remove most references to final-form
Jan 24, 2022
21de5ad
Fix default validation mode
Jan 24, 2022
b88455a
Fix edit e2e tests
Jan 24, 2022
7f912bc
Update UPGRADE guide for useInput
Jan 24, 2022
a1a771f
More cleanup
Jan 24, 2022
4292c5d
Update documentation
Jan 24, 2022
99d9059
Fix unnecessary async
Jan 24, 2022
abf8558
Fix typo in upgrade guide
djhi Jan 24, 2022
b3f8cf1
Fix demo build
Jan 24, 2022
182cfaf
Add support for submission validation
Jan 24, 2022
f890d1c
Update documentation
Jan 24, 2022
ecd3ec5
Merge branch 'next' into react-hook-form
djhi Jan 24, 2022
1e88780
Apply suggestions from code review
djhi Jan 24, 2022
b3ac87f
Cleanup e2e test files
Jan 24, 2022
c887025
Rename Form Types
Jan 24, 2022
8f29c90
Cleanup linter warnings
Jan 24, 2022
3d31c67
Upgrade react-hook-form and fix demos by marking react-hook-form as e…
Jan 25, 2022
1938a34
Restore tests
Jan 25, 2022
b213f45
Apply review
Jan 25, 2022
a3dd55b
Less paranoid build commands
Jan 25, 2022
fb6d1f9
Fix build
Jan 25, 2022
4e480ef
Restore sanitizeEmtyValues tests
Jan 25, 2022
f62a515
Fix linter warnings
Jan 26, 2022
cff7200
Fix Filters
Jan 26, 2022
417cf31
Fix CRM demo
Jan 26, 2022
d4f1ce9
Remove references to final-form
Jan 26, 2022
3247b4c
Fix TextInput Does Not Pass Event Handlers
Jan 26, 2022
bb3a8d2
Fix filters
Jan 26, 2022
af6f82f
Fix demo login styles
Jan 26, 2022
d4a06a4
Fix demo usage of record
Jan 26, 2022
e221837
Review
fzaninotto Jan 26, 2022
93c444c
Fix products links in CategoryEdit
Jan 26, 2022
820e8b2
Fix ArrayInput does not support null values
Jan 26, 2022
5616245
Merge branch 'next' into react-hook-form
djhi Jan 26, 2022
e500715
Fix tests
Jan 26, 2022
d68b60b
Remove unnecessary async
Jan 26, 2022
8bd88de
Update examples/simple/src/data.tsx
djhi Jan 26, 2022
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A frontend Framework for building data-driven applications running in the browse
## Features

* Adapts to any backend (REST, GraphQL, SOAP, etc.)
* Powered by [material-ui](https://material-ui.com/), [redux](https://redux.js.org/), [react-final-form](https://final-form.org/react), [react-router](https://reacttraining.com/react-router/) and a few more
* Powered by [material-ui](https://material-ui.com/), [redux](https://redux.js.org/), [react-hook-form](https://react-hook-form.com), [react-router](https://reacttraining.com/react-router/) and a few more
* Super-fast UI thanks to optimistic rendering (renders before the server returns)
* Undo updates and deletes for a few seconds
* Relationships (many to one, one to many)
Expand Down
292 changes: 292 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,298 @@ If you've declared custom Record types, you'll need to upgrade your code as foll
}
```

## react-final-form Has Been Replaced By react-hook-form

### `<FormWithRedirect>` Has Been Renamed to `<Form>`

The form components don't handle redirection anymore, as redirections are now handled in side effects (`<Create mutationOptions>` and `<Edit mutationOptions>`). As a consequence, the `<FormWithRedirect>` has been renamed to `<Form>`.

To upgrade, replace all occurrences of `<FormWithRedirect>` with `<Form>`.

```diff
- import { FormWithRedirect } from 'react-admin';
+ import { Form } from 'react-admin';

export const MyForm = () => (
- <FormWithRedirect
+ <Form
render={() => ...}
/>
);
```

### Form Props Have Changed

`<FormWithRedirect>` used to accept [`react-final-form` `<Form>` props](https://final-form.org/docs/react-final-form/types/FormProps). It now accepts [`react-hook-form` `useForm` props](https://react-hook-form.com/api/useform). This also affects the other form components (`<SimpleForm>`, `<TabbedForm>`, etc.)

The most commonly used prop is probably `initialValues`, which is now named `defaultValues`:

```diff
const PostCreate = () => (
<Create>
<SimpleForm
- initialValues={{ title: 'A default title' }}
+ defaultValues={{ title: 'A default title' }}
>
...
</SimpleForm>
</Create>
)
```

We kept the `validate` function prop, which we automatically translate to a custom [`react-hook-form` `resolver`](https://react-hook-form.com/api/useform#validationResolver). So even if it's not technically a react-hook-form prop, you can still use `validate` as before.

This also means you can now use [`yup`](https://github.com/jquense/yup), [`zod`](https://github.com/colinhacks/zod), [`joi`](https://github.com/sideway/joi), [superstruct](https://github.com/ianstormtaylor/superstruct), [vest](https://github.com/ealush/vest) or any [resolver](https://react-hook-form.com/api/useform#validationResolver) supported by `react-hook-form` to apply schema validation.

### `sanitizeEmptyValues` Has Been Removed

React-hook-form doesn't remove empty values like react-final-fom did. Therefore, you no longer need to opt out this behavior:

```diff
export const PostEdit = () => (
<Edit>
- <SimpleForm sanitizeEmptyValues={false}>
+ <SimpleForm>
<TextInput source="title" />
<JsonInput source="body" />
</SimpleForm>
</Edit>
);
```

If you actually need to remove empty values, you can use the `parse` prop on a per-input basis:

```diff
+const convertEmptyStringToUndefined = v => v === '' ? undefined : v;

-<TextInput source="title" />
+<TextInput source="title" parse={convertEmptyStringToUndefined} />
```

Or use the `transform` prop on the `<Create>`, `<Edit>`, or `<SaveButton>` components.

### `<FormWithRedirect>` Render Function Arguments Have Changed

`<FormWithRedirect>` used to call its child function with an object containing parts of the `final-form` form state (`valid`, `invalid`, `pristine`, `dirty`). It now only passes the `handleSubmit` function, which must be passed down to the `onSubmit` prop of the underlying form. If you need to access the form state, call [the react-hook-form `useFormState` hook](https://react-hook-form.com/api/useformstate):

```diff
import { FormWithRedirect } from 'react-admin';
+ import { useFormState } from 'react-hook-form';

const MyCustomForm = () => {
return (
- <FormWithRedirect
+ <Form
- render={({ valid, dirty, handleSubmit }) => (
+ render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
...
- <SubmitButton disabled={!dirty || !valid}>Save</SubmitButton>
+ <SubmitButton>Save</SubmitButton>
</form>
)}
/>
);
};

-const SubmitButton = ({ disabled, ...props }) => {
+const SubmitButton = (props) => {
+ const { isDirty, isValid } = useFormState();
return (
- <button disabled={disabled} {...props} />
+ <button disabled={!isDirty || !isValid} {...props} />
);
}
```

### `<Toolbar>` Props Have Changed

The `<Toolbar>` component used to receive the form state props (`dirty`, `invalid`, `pristine` and `valid`). They don't exist in `react-hook-form` and you can get the form state with its `useFormState` hook:

```diff
import Toolbar from '@mui/material/Toolbar';
import { SaveButton } from 'react-admin';

const ReviewEditToolbar = (props: ToolbarProps<Review>) => {
- const { invalid, resource, saving } = props;
+ const { resource, saving } = props;

return (
<Toolbar>
<SaveButton
- invalid={invalid}
saving={saving}
submitOnEnter={true}
/>
</Toolbar>
);
};
```

### `initalValue` and `defaultValue` Have Been Merged Into `defaultValue`

All React Admin inputs used to accept both a `initialValue` and a `defaultValue` prop and they had different meanings in `react-final-form`. With `react-hook-form` there's only `defaultValue`:

```diff
const PostCreate = () => (
<Create>
<SimpleForm>
- <TextInput source="title" initialValue="A default">
+ <TextInput source="title" defaultValue="A default">
</SimpleForm>
</Create>
)
```

### `useInput` Signature And Return Value Have Changed

Just like all inputs, `useInput` now only accept `defaultValue` and will ignore `initialValue`.

Besides, `useInput` used to return `final-form` properties such as `input ` and `meta`. It now returns `field`, `fieldState` and `formState` (see https://react-hook-form.com/api/usecontroller).

Note that the `error` returned by `fieldState` is a not just a simple string anymore but an object with a `message` property.

```diff
import TextField from '@material-ui/core/TextField';
import { useInput, required } from 'react-admin';

const MyInput = ({ helperText, ...props }) => {
const {
- input,
+ field,
- meta: { touched, error },
+ fieldState: { isTouched, invalid, error },
+ formState: { isSubmitted }
isRequired
} = useInput(props);

return (
<TextField
- {...input}
+ {...field}
label={props.label}
- error={touched && !!error}
+ error={(isTouched || isSubmitted) && invalid}
- helperText={touched && !!error ? error : helperText}
+ helperText={(isTouched || isSubmitted) && invalid ? error?.message : helperText}
required={isRequired}
{...rest}
/>
);
};

const UserForm = () => (
<SimpleForm>
- <MyInput initialValue="John" />
+ <MyInput defaultValue="John" />
</SimpleForm>
)
```

### Common Patterns To Access Form State And Values Have Changed

If you used to rely on `react-final-form` hooks and components such as `useForm`, `useFormState` or `<FormSpy>`, you must replace them with their `react-hook-form` equivalent.

For instance, if you used `useFormState` in a component to show something depending on another form value, use `useWatch` instead:

```diff
-import { useFormState } from 'react-final-form';
+import { useWatch } from 'react-hook-form';

const CityInput = props => {
- const { values } = useFormState();
+ const country = useWatch({ name: 'country' });
return (
<SelectInput
- choices={values.country ? toChoices(cities[values.country]) : []}
+ choices={country ? toChoices(cities[country]) : []}
{...props}
/>
);
};
```

If you used `<FormSpy>`:

```diff
-import { FormSpy } from 'react-final-form';
+import { useWatch } from 'react-hook-form';

const CityInput = props => {
+ const country = useWatch({ name: 'country' });
return (
- <FormSpy subscription={{ values: true }}>
- {({ values }) => (
- <SelectInput
- choices={values.country ? toChoices(cities[values.country]) : []}
- {...props}
- />
- )}
- </FormSpy>
+ <SelectInput
+ choices={country ? toChoices(cities[country]) : []}
+ {...props}
+ />
);
};
```

If you had a component setting a form value imperatively via `useForm`, you should use `useFormContext`:

```diff
-import { useForm } from 'react-final-form';
+import { useFormContext } from 'react-hook-form';

const ClearCountry = () => {
- const { change } = useForm();
+ const { setValue } = useFormContext();

const handleClick = () => {
- change('country', '');
+ setValue('country', '');
};

return <button onClick={handleClick}>Clear country</button>
}
```

If you called `useForm` to access the form API, you should now call `useFormContext`:

```diff
-import { useForm } from 'react-final-form';
+import { useFormContext } from 'react-hook-form';

const ResetFormButton = () => {
- const { reset } = useForm();
+ const { reset } = useFormContext();
return <Button onClick={() => reset()}>Reset</Button>
}
```

## `allowEmpty` Has Been Removed From `SelectInput`, `AutocompleteInput` and `AutocompleteArrayInput`

The `SelectInput`, `AutocompleteInput` and `AutocompleteArrayInput` components used to accept an `allowEmpty` prop. When set to `true`, a choice was added for setting the input value to an empty value (empty string by default).

However, the underlying MaterialUI components now require that the current input value has a matching choice. Those components now always accept an empty value (an empty string by default). You can safely remove this prop.

```diff
const choices = [{ id: 1, name: 'value' }, { id: 2, name: 'value 2' }]

const MyOptionalSelect = () => (
- <SelectInput choices={choices} allowEmpty />
+ <SelectInput choices={choices} />
);
```

If you require the input to have a non-empty value, use the `required` validation.

```diff
const MyRequiredSelect = () => (
- <SelectInput choices={choices} />
+ <SelectInput choices={choices} validate={[required()]} />
);
```

# Upgrade to 3.0

We took advantage of the major release to fix all the problems in react-admin that required a breaking change. As a consequence, you'll need to do many small changes in the code of existing react-admin v2 applications. Follow this step-by-step guide to upgrade to react-admin v3.
Expand Down
24 changes: 13 additions & 11 deletions cypress/integration/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ describe('Create Page', () => {
it('should put the ArrayInput default value', () => {
const currentDate = new Date();
const currentDateString = currentDate.toISOString().slice(0, 10);
cy.get(CreatePage.elements.input('backlinks[0].date')).should(el =>
cy.get(CreatePage.elements.input('backlinks.0.date')).should(el =>
expect(el).to.have.value(currentDateString)
);
cy.get(CreatePage.elements.input('backlinks[0].url')).should(el =>
cy.get(CreatePage.elements.input('backlinks.0.url')).should(el =>
expect(el).to.have.value('http://google.com')
);
});

it('should validate ArrayInput', () => {
const backlinksContainer = cy
.get(CreatePage.elements.input('backlinks[0].date'))
.get(CreatePage.elements.input('backlinks.0.date'))
.parents('.ra-input-backlinks');
backlinksContainer.contains('Remove').click();
CreatePage.setValues([
Expand All @@ -60,7 +60,6 @@ describe('Create Page', () => {
value: 'foo',
},
]);
cy.get(CreatePage.elements.submitButton).click();
cy.get('.ra-input-backlinks').contains('Required');
});

Expand All @@ -70,10 +69,10 @@ describe('Create Page', () => {
CreatePage.navigate();
CreatePage.waitUntilVisible();
cy.get(CreatePage.elements.addAuthor).click();
cy.get(CreatePage.elements.input('authors[0].user_id')).should(
cy.get(CreatePage.elements.input('authors.0.user_id')).should(
el => expect(el).to.exist
);
cy.get(CreatePage.elements.input('authors[0].role')).should(
cy.get(CreatePage.elements.input('authors.0.role')).should(
el => expect(el).to.not.exist
);
});
Expand All @@ -87,12 +86,12 @@ describe('Create Page', () => {
CreatePage.setValues([
{
type: 'input',
name: 'authors[0].user_id',
name: 'authors.0.user_id',
value: 'Annamarie Mayer',
},
]);
cy.get('[role="option"]').trigger('click');
cy.get(CreatePage.elements.input('authors[0].role')).should(
cy.get(CreatePage.elements.input('authors.0.role')).should(
el => expect(el).to.exist
);
});
Expand Down Expand Up @@ -300,11 +299,13 @@ describe('Create Page', () => {
);
});

it('should not show rich text input error message when field is untouched', () => {
// FIXME Skipped as we are going to replace the RichTextInput with the tip tap version
it.skip('should not show rich text input error message when field is untouched', () => {
cy.get(CreatePage.elements.richTextInputError).should('not.have.value');
});

it('should show rich text input error message when form is submitted', () => {
// FIXME Skipped as we are going to replace the RichTextInput with the tip tap version
it.skip('should show rich text input error message when form is submitted', () => {
const values = [
{
type: 'input',
Expand All @@ -319,7 +320,8 @@ describe('Create Page', () => {
.contains('Required');
});

it('should not show rich text input error message when form is submitted and input is filled with text', () => {
// FIXME Skipped as we are going to replace the RichTextInput with the tip tap version
it.skip('should not show rich text input error message when form is submitted and input is filled with text', () => {
const values = [
{
type: 'input',
Expand Down
1 change: 1 addition & 0 deletions cypress/integration/custom-forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('Custom Forms', () => {
});

it('should allow to create a new post', () => {
cy.get(CreatePage.elements.postSelect).click();
cy.get(CreatePage.elements.showPostCreateModalButton).click();

CreatePage.setInputValue('input', 'title', 'Bazinga!');
Expand Down
Loading