diff --git a/UPGRADE.md b/UPGRADE.md index a384d2721b9..f0567ad0576 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -154,74 +154,6 @@ const App = () => ( ); ``` -## Injected Elements Replaced By Injected Components - -Every time that you used an *element* as a prop in a react-admin component, you must now use a *component*. This basically means removing the enclosing angle brackets in props: - -```diff -const PostEdit = (props) => ( -- } actions={} {...props}> -+ -``` - -If an element prop depended on higher props, you must use an inline component instead: - -```diff -const PostEdit = ({ permissions, ...props }) => ( -- } {...props}> -+ } {...props}> -``` - -We are aware that this will require many changes in existing codebases. Fortunately, it can be automated for the most part. You might find the following regular expressions useful for migrating. - -The first, `{<(.+)\/>}`, searches for all element injections, for instance: - -* `{}` -* `{}` - -You can then use `{props => <$1{...props} />}` as the replacement pattern. The result will be: - -* `{props => }` -* `{props => }` - -However, in most cases you do not need an inline component, so you might want to use another replacement pattern afterwards: `{props => <(\w+) {\.\.\.props} \/>}`. It searches for simple component injections without extra props, such as `{props => }`, but will not match more complex cases like `{props => }`. You can then use `{$1}` as the replacement pattern, which will produce `{EditActions}`. - -For reference, you can read the [RFC](https://github.com/marmelab/react-admin/issues/3246) about this change to understand the rationale. - -## Remove optionText={function} for Input Components - -Many Input components for selecting items in a list expose an `optionText` prop. This prop used to accept 3 types of values: a field name, a function, and a React element. Here is an example with `AutocompleteInput` using a function as `optionText` prop: - -```jsx -const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, -]; -const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; - -``` - -However, as these Input components no longer accept element as props (but components), there is no way react-admin can distinguish simple functions from elements. Indeed, they might be functions accepting props. - -So `optionText` still works, but no longer accepts a simple function - only a component. - -The migration shouldn't be too hard though. To turn a function into a component, wrap it inside Fragment tags: - -```diff --const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; -- -+const Option = ({ record }) => <>{record.first_name} {record.last_name}; -+ -``` - -This change concerns the following components: - -* `CheckboxGroupInput` -* `SelectArrayInput` -* `SelectInput` -* `SelectField` -* `RadioButtonGroupInput` - ## Deprecated components were removed Components deprecated in 2.X have been removed in 3.x. This includes: diff --git a/docs/Authorization.md b/docs/Authorization.md index b4affdcb0e8..4a08e4f00e1 100644 --- a/docs/Authorization.md +++ b/docs/Authorization.md @@ -122,7 +122,7 @@ const UserCreateToolbar = ({ permissions, ...props }) => export const UserCreate = ({ permissions, ...props }) => } + toolbar={} defaultValue={{ role: 'user' }} > @@ -140,7 +140,7 @@ This also works inside an `Edition` view with a `TabbedForm`, and you can hide a {% raw %} ```jsx export const UserEdit = ({ permissions, ...props }) => - + } {...props}> {permissions === 'admin' && } diff --git a/docs/CreateEdit.md b/docs/CreateEdit.md index cf18ea0a721..5c7e8d3c815 100644 --- a/docs/CreateEdit.md +++ b/docs/CreateEdit.md @@ -58,7 +58,7 @@ export const PostCreate = (props) => ( ); export const PostEdit = (props) => ( - + } {...props}> @@ -100,14 +100,14 @@ export const PostEdit = (props) => ( ); ``` -More interestingly, you can pass a component as `title`. React-admin clones this component and, in the ``, injects the current `record`. This allows to customize the title according to the current record: +More interestingly, you can pass an element as `title`. React-admin clones this element and, in the ``, injects the current `record`. This allows to customize the title according to the current record: ```jsx const PostTitle = ({ record }) => { return Post {record ? `"${record.title}"` : ''}; }; export const PostEdit = (props) => ( - + } {...props}> ... ); @@ -115,7 +115,7 @@ export const PostEdit = (props) => ( ### Actions -You can replace the list of default actions by your own component using the `actions` prop: +You can replace the list of default actions by your own element using the `actions` prop: ```jsx import Button from '@material-ui/core/Button'; @@ -130,7 +130,7 @@ const PostEditActions = ({ basePath, data, resource }) => ( ); export const PostEdit = (props) => ( - + } {...props}> ... ); @@ -152,7 +152,7 @@ const Aside = () => ( ); const PostEdit = props => ( - + } {...props}> ... ``` @@ -218,7 +218,7 @@ const CustomToolbar = withStyles(toolbarStyles)(props => ( const PostEdit = props => ( - + }> ... @@ -718,7 +718,7 @@ const PostCreateToolbar = props => ( export const PostCreate = (props) => ( - + } redirect="show"> ... @@ -738,7 +738,7 @@ const PostEditToolbar = props => ( export const PostEdit = (props) => ( - + }> ... @@ -811,7 +811,7 @@ const UserCreateToolbar = ({ permissions, ...props }) => export const UserCreate = ({ permissions, ...props }) => } + toolbar={} defaultValue={{ role: 'user' }} > @@ -829,7 +829,7 @@ This also works inside an `Edition` view with a `TabbedForm`, and you can hide a {% raw %} ```jsx export const UserEdit = ({ permissions, ...props }) => - + } {...props}> {permissions === 'admin' && } diff --git a/docs/Fields.md b/docs/Fields.md index ceb00031900..dd1f6606013 100644 --- a/docs/Fields.md +++ b/docs/Fields.md @@ -438,7 +438,7 @@ const choices = [ { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const FullNameField = ({ record }) => {record.first_name} {record.last_name}; - +}/> ``` The current choice is translated by default, so you can use translation identifiers as choices: @@ -614,12 +614,12 @@ By default, react-admin restricts the possible values to 25 and displays no pagi ``` -And if you want to allow users to paginate the list, pass a `` component as the `pagination` prop: +And if you want to allow users to paginate the list, pass a `` element as the `pagination` prop: ```jsx import { Pagination } from 'react-admin'; - +} reference="comments" target="post_id"> ... ``` diff --git a/docs/Inputs.md b/docs/Inputs.md index b2675fb0993..117748139cc 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -13,7 +13,7 @@ import React from 'react'; import { Edit, DisabledInput, LongTextInput, ReferenceInput, SelectInput, SimpleForm, TextInput } from 'react-admin'; export const PostEdit = (props) => ( - + } {...props}> @@ -433,7 +433,7 @@ const choices = [ { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const FullNameField = ({ record }) => {record.first_name} {record.last_name}; - +}/> ``` The choices are translated by default, so you can use translation identifiers as choices: @@ -685,7 +685,7 @@ const choices = [ { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const FullNameField = ({ record }) => {record.first_name} {record.last_name}; - +}/> ``` The choices are translated by default, so you can use translation identifiers as choices: @@ -1001,7 +1001,7 @@ const choices = [ { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const FullNameField = ({ record }) => {record.first_name} {record.last_name}; - +}/> ``` Enabling the `allowEmpty` props adds an empty choice (with a default `null` value, which you can overwrite with the `emptyValue` prop) on top of the options, and makes the value nullable: diff --git a/docs/List.md b/docs/List.md index 2478e2e11c1..6e484720d91 100644 --- a/docs/List.md +++ b/docs/List.md @@ -21,7 +21,7 @@ Here are all the props accepted by the `` component: * [`actions`](#actions) * [`exporter`](#exporter) * [`bulkActionButtons`](#bulk-action-buttons) -* [`filters`](#filters) (a React component used to display the filter form) +* [`filters`](#filters) (a React element used to display the filter form) * [`perPage`](#records-per-page) * [`sort`](#default-sort-field) * [`filter`](#permanent-filter) (the permanent filter used in the REST request) @@ -79,11 +79,11 @@ export const PostList = (props) => ( ); ``` -The title can be either a string, or a component of your own. +The title can be either a string, or an element of your own. ### Actions -You can replace the list of default actions by your own component using the `actions` prop: +You can replace the list of default actions by your own element using the `actions` prop: ```jsx import Button from '@material-ui/core/Button'; @@ -95,7 +95,7 @@ const PostActions = ({ currentSort, displayedFilters, exporter, - filters: Filters, + filters, filterValues, onUnselectItems, resource, @@ -104,15 +104,13 @@ const PostActions = ({ total }) => ( - {Filters && - - } + {filters && React.cloneElement(filters, { + resource, + showFilter, + displayedFilters, + filterValues, + context: 'button', + })} ( - + }> ... ); @@ -137,7 +135,7 @@ You can also use such a custom `ListActions` prop to omit or reorder buttons bas ```jsx export const PostList = ({ permissions, ...props }) => ( - }> + }> ... ); @@ -226,7 +224,7 @@ Under the hood, `fetchRelatedRecords()` uses react-admin's sagas, which trigger ### Bulk Action Buttons -Bulk action buttons are buttons that affect several records at once, like mass deletion for instance. In the `` component, the bulk actions toolbar appears when a user ticks the checkboxes in the first column of the table. The user can then choose a button from the bulk actions toolbar. By default, all list views have a single bulk action button, the bulk delete button. You can add other bulk action buttons by passing a custom component as the `bulkActionButtons` prop of the `` component: +Bulk action buttons are buttons that affect several records at once, like mass deletion for instance. In the `` component, the bulk actions toolbar appears when a user ticks the checkboxes in the first column of the table. The user can then choose a button from the bulk actions toolbar. By default, all list views have a single bulk action button, the bulk delete button. You can add other bulk action buttons by passing a custom element as the `bulkActionButtons` prop of the `` component: ```jsx import React, { Fragment } from 'react'; @@ -243,7 +241,7 @@ const PostBulkActionButtons = props => ( ); export const PostList = (props) => ( - + }> ... ); @@ -377,7 +375,7 @@ const PostFilter = (props) => ( ); export const PostList = (props) => ( - + }> ... ); @@ -392,7 +390,7 @@ The filter component must be a `` with `` children. It does so by inspecting its `context` prop. -**Tip**: Don't mix up this `filters` prop, expecting a React component, with the `filter` props, which expects an object to define permanent filters (see below). +**Tip**: Don't mix up this `filters` prop, expecting a React element, with the `filter` props, which expects an object to define permanent filters (see below). The `Filter` component accepts the usual `className` prop but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/overrides/#overriding-with-classes)). This property accepts the following keys: @@ -518,7 +516,7 @@ const PostFilter = (props) => ( ); export const PostList = (props) => ( - + } filterDefaultValues={{ is_published: true }}> ... ); @@ -533,7 +531,7 @@ const filterSentToDataProvider = { ...filterDefaultValues, ...filterChosenByUser ### Pagination -You can replace the default pagination component by your own, using the `pagination` prop. The pagination element receives the current page, the number of records per page, the total number of records, as well as a `setPage()` function that changes the page. +You can replace the default pagination element by your own, using the `pagination` prop. The pagination element receives the current page, the number of records per page, the total number of records, as well as a `setPage()` function that changes the page. For instance, you can modify the default pagination by adjusting the "rows per page" selector. @@ -544,7 +542,7 @@ import { Pagination } from 'react-admin'; const PostPagination = props => export const PostList = (props) => ( - + }> ... ); @@ -580,7 +578,7 @@ const PostPagination = ({ page, perPage, total, setPage }) => { } export const PostList = (props) => ( - + }> ... ); @@ -602,7 +600,7 @@ const Aside = () => ( ); const PostList = props => ( - + } {...props}> ... ``` @@ -763,8 +761,8 @@ const MyDatagridRow = ({ record, resource, id, onToggleItem, children, selected, ) -const MyDatagridBody = props => ; -const MyDatagrid = props => ; +const MyDatagridBody = props => } />; +const MyDatagrid = props => } />; const PostList = props => ( @@ -845,7 +843,7 @@ const PostPanel = ({ id, record, resource }) => ( const PostList = props => ( - + }> @@ -878,7 +876,7 @@ const PostShow = props => ( const PostList = props => ( - + }> @@ -911,7 +909,7 @@ const PostEdit = props => ( const PostList = props => ( - + }> @@ -1130,7 +1128,7 @@ const CategoriesActions = props => ( export const CategoriesList = (props) => ( - + }> @@ -1146,7 +1144,7 @@ export const CategoriesList = (props) => ( export const CategoriesList = (props) => ( - + }> @@ -1189,9 +1187,9 @@ const CommentGrid = ({ ids, data, basePath }) => ( {ids.map(id => } - subheader={props => } - avatar={props => } {...props} />} + title={} + subheader={} + avatar={} />} /> @@ -1246,7 +1244,7 @@ const UserFilter = ({ permissions, ...props }) => export const UserList = ({ permissions, ...props }) => } + filters={} sort={{ field: 'name', order: 'ASC' }} > { return Post {record ? `"${record.title}"` : ''}; }; export const PostShow = (props) => ( - + } {...props}> ... ); @@ -103,7 +103,7 @@ const PostShowActions = ({ basePath, data, resource }) => ( ); export const PostShow = (props) => ( - + } {...props}> ... ); @@ -128,6 +128,7 @@ const PostShow = props => ( ... +); ``` {% endraw %} @@ -258,7 +259,7 @@ import { const ScrollableTabbedShowLayout = props => ( - }> + }> ... @@ -292,7 +293,7 @@ const PostShowActions = ({ permissions, basePath, data, resource }) => ( ); export const PostShow = ({ permissions, ...props }) => ( - } {...props}> + } {...props}> diff --git a/docs/Theming.md b/docs/Theming.md index 31c17be1627..56d412c2616 100644 --- a/docs/Theming.md +++ b/docs/Theming.md @@ -104,7 +104,7 @@ export const VisitorList = withStyles(listStyles)(({ classes, ...props }) => ( } sort={{ field: 'last_seen', order: 'DESC' }} perPage={25} > @@ -399,14 +399,14 @@ const MyUserMenu = props => ( } /> ); -const MyAppBar = props => ; +const MyAppBar = props => } />; -const MyLayout = props => ; +const MyLayout = props => } />; ``` You can also customize the default icon by setting the `icon` prop to the `` component. @@ -851,7 +851,7 @@ const MyError = ({
diff --git a/examples/demo/src/orders/OrderEdit.js b/examples/demo/src/orders/OrderEdit.js index 8aedf2d302c..31f659e31a9 100644 --- a/examples/demo/src/orders/OrderEdit.js +++ b/examples/demo/src/orders/OrderEdit.js @@ -29,14 +29,13 @@ const editStyles = { }; const OrderEdit = props => ( - + } aside={} {...props}> - `${record.first_name} ${record.last_name}` - } + optionText={choice => + `${choice.first_name} ${choice.last_name}`} /> ( - `${record.first_name} ${record.last_name}` + optionText={choice => + `${choice.first_name} ${choice.last_name}` } /> @@ -169,7 +169,7 @@ const OrderList = ({ classes, ...props }) => ( filterDefaultValues={{ status: 'ordered' }} sort={{ field: 'date', order: 'DESC' }} perPage={25} - filters={OrderFilter} + filters={} >
diff --git a/examples/demo/src/products/ProductEdit.js b/examples/demo/src/products/ProductEdit.js index f938c049d86..3951dc263cd 100644 --- a/examples/demo/src/products/ProductEdit.js +++ b/examples/demo/src/products/ProductEdit.js @@ -35,7 +35,7 @@ const styles = { }; const ProductEdit = ({ classes, ...props }) => ( - + }> @@ -71,7 +71,7 @@ const ProductEdit = ({ classes, ...props }) => ( reference="reviews" target="product_id" addLabel={false} - pagination={Pagination} + pagination={} > diff --git a/examples/demo/src/products/ProductList.js b/examples/demo/src/products/ProductList.js index 114f758f23a..27f9f243bfa 100644 --- a/examples/demo/src/products/ProductList.js +++ b/examples/demo/src/products/ProductList.js @@ -48,7 +48,7 @@ export const ProductFilter = props => ( const ProductList = props => ( } perPage={20} sort={{ field: 'id', order: 'ASC' }} > diff --git a/examples/demo/src/reviews/ReviewEdit.js b/examples/demo/src/reviews/ReviewEdit.js index 469ea769d45..7d421a086d9 100644 --- a/examples/demo/src/reviews/ReviewEdit.js +++ b/examples/demo/src/reviews/ReviewEdit.js @@ -63,7 +63,7 @@ const ReviewEdit = ({ classes, onCancel, ...props }) => ( version={controllerProps.version} redirect="list" resource="reviews" - toolbar={ReviewEditToolbar} + toolbar={} > ( /> - `${record.first_name} ${record.last_name}` + optionText={choice => + `${choice.first_name} ${choice.last_name}` } /> diff --git a/examples/demo/src/reviews/ReviewList.js b/examples/demo/src/reviews/ReviewList.js index acc410eaa95..f685d8acd1b 100644 --- a/examples/demo/src/reviews/ReviewList.js +++ b/examples/demo/src/reviews/ReviewList.js @@ -59,8 +59,8 @@ class ReviewList extends Component { className={classnames(classes.list, { [classes.listWithDrawer]: isMatch, })} - bulkActionButtons={ReviewsBulkActionButtons} - filters={ReviewFilter} + bulkActionButtons={} + filters={} perPage={25} sort={{ field: 'date', order: 'DESC' }} > diff --git a/examples/demo/src/visitors/VisitorEdit.js b/examples/demo/src/visitors/VisitorEdit.js index 99a32243718..ba7c4443bb4 100644 --- a/examples/demo/src/visitors/VisitorEdit.js +++ b/examples/demo/src/visitors/VisitorEdit.js @@ -27,7 +27,7 @@ const VisitorTitle = ({ record }) => record ? : null; const VisitorEdit = ({ classes, ...props }) => ( - + } {...props}> ( } sort={{ field: 'last_seen', order: 'DESC' }} perPage={25} > diff --git a/examples/simple/src/comments/CommentList.js b/examples/simple/src/comments/CommentList.js index ad049c8a2ed..34e9d328c6f 100644 --- a/examples/simple/src/comments/CommentList.js +++ b/examples/simple/src/comments/CommentList.js @@ -208,8 +208,8 @@ const CommentList = props => ( {...props} perPage={6} exporter={exporter} - filters={CommentFilter} - pagination={CommentPagination} + filters={} + pagination={} component="div" > } medium={} /> diff --git a/examples/simple/src/comments/PostQuickCreate.js b/examples/simple/src/comments/PostQuickCreate.js index fa7cea21f4d..90244ea188d 100644 --- a/examples/simple/src/comments/PostQuickCreate.js +++ b/examples/simple/src/comments/PostQuickCreate.js @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import { useSelector, useDispatch } from 'react-redux'; import { makeStyles } from '@material-ui/core/styles'; @@ -66,16 +66,12 @@ const PostQuickCreate = ({ onCancel, onSave }) => { save={handleSave} saving={submitting} redirect={false} - toolbar={useMemo( - () => props => ( - - ), - [onCancel, submitting], - )} + toolbar={ + + } classes={{ form: classes.form }} > diff --git a/examples/simple/src/index.js b/examples/simple/src/index.js index 9da7645ded5..db301470d42 100644 --- a/examples/simple/src/index.js +++ b/examples/simple/src/index.js @@ -27,10 +27,14 @@ render( } noLayout />, - , + } + />, ]} > {permissions => [ diff --git a/examples/simple/src/posts/PostCreate.js b/examples/simple/src/posts/PostCreate.js index b0acfaebfb1..4d7bf4733c2 100644 --- a/examples/simple/src/posts/PostCreate.js +++ b/examples/simple/src/posts/PostCreate.js @@ -72,7 +72,7 @@ const getDefaultDate = () => new Date(); const PostCreate = ({ permissions, ...props }) => ( } defaultValue={{ average_note: 0 }} validate={values => { const errors = {}; diff --git a/examples/simple/src/posts/PostEdit.js b/examples/simple/src/posts/PostEdit.js index 4880644d316..9aa7ba03c63 100644 --- a/examples/simple/src/posts/PostEdit.js +++ b/examples/simple/src/posts/PostEdit.js @@ -44,7 +44,7 @@ const EditActions = ({ basePath, data, hasShow }) => ( ); const PostEdit = props => ( - + } actions={} {...props}> diff --git a/examples/simple/src/posts/PostList.js b/examples/simple/src/posts/PostList.js index dfa70b14c2b..e377cde429c 100644 --- a/examples/simple/src/posts/PostList.js +++ b/examples/simple/src/posts/PostList.js @@ -101,8 +101,8 @@ const PostList = props => { return ( } + filters={} sort={{ field: 'published_at', order: 'DESC' }} > { /> } medium={ - + }> ( const PostShow = props => ( {controllerProps => ( - + }> diff --git a/examples/simple/src/users/UserCreate.js b/examples/simple/src/users/UserCreate.js index 58a773baef5..ae87b0031bf 100644 --- a/examples/simple/src/users/UserCreate.js +++ b/examples/simple/src/users/UserCreate.js @@ -32,8 +32,8 @@ const UserEditToolbar = ({ permissions, ...props }) => ( ); const UserCreate = ({ permissions, ...props }) => ( - - }> + }> + }> { }; const UserEdit = ({ permissions, ...props }) => ( - + } aside={