diff --git a/UPGRADE.md b/UPGRADE.md index 79e5200f060..a384d2721b9 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -154,6 +154,74 @@ 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: @@ -161,3 +229,5 @@ Components deprecated in 2.X have been removed in 3.x. This includes: * `AppBarMobile` (use `AppBar` instead, which is responsive) * `Header` (use `Title` instead) * `ViewTitle` (use `Title` instead) +* `RecordTitle` (use `TitleForRecord` instead) +* `TitleDeprecated` (use `Title` instead) diff --git a/cypress/cypress.json b/cypress/cypress.json index 62cc929869e..03a611645da 100644 --- a/cypress/cypress.json +++ b/cypress/cypress.json @@ -7,5 +7,8 @@ "supportFile": "support/index.js", "videosFolder": "videos", "viewportWidth": 1280, - "viewportHeight": 720 + "viewportHeight": 720, + "blacklistHosts": [ + "source.unsplash.com" + ] } diff --git a/docs/Admin.md b/docs/Admin.md index 4258f1ddf44..592d1dd3ecd 100644 --- a/docs/Admin.md +++ b/docs/Admin.md @@ -182,7 +182,7 @@ const Menu = ({ resources, onMenuClick, logout }) => ( } + leftIcon={LabelIcon} onClick={onMenuClick} /> export const UserCreate = ({ permissions, ...props }) => } + toolbar={props => } 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' && } @@ -173,7 +173,7 @@ const UserFilter = ({ permissions, ...props }) => export const UserList = ({ permissions, ...props }) => } + filters={props => } sort={{ field: 'name', order: 'ASC' }} > ( ); export const PostEdit = (props) => ( - } {...props}> + @@ -107,7 +107,7 @@ 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 element using the `actions` prop: +You can replace the list of default actions by your own component 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={props => } 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 85dba106785..ceb00031900 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: @@ -619,7 +619,7 @@ And if you want to allow users to paginate the list, pass a `` compo ```jsx import { Pagination } from 'react-admin'; -} reference="comments" target="post_id"> + ... ``` diff --git a/docs/Inputs.md b/docs/Inputs.md index 117748139cc..b2675fb0993 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 27ef41ac41e..39006eb2686 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 element used to display the filter form) +* [`filters`](#filters) (a React component 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 an element of your own. +The title can be either a string, or a component of your own. ### Actions -You can replace the list of default actions by your own element using the `actions` prop: +You can replace the list of default actions by your own component 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,13 +104,15 @@ const PostActions = ({ total }) => ( - {filters && React.cloneElement(filters, { - resource, - showFilter, - displayedFilters, - filterValues, - context: 'button', - }) } + {Filters && + + } ( - }> + ... ); @@ -135,7 +137,7 @@ You can also use such a custom `ListActions` prop to omit or reorder buttons bas ```jsx export const PostList = ({ permissions, ...props }) => ( - }> + }> ... ); @@ -224,7 +226,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 element 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 component as the `bulkActionButtons` prop of the `` component: ```jsx import React, { Fragment } from 'react'; @@ -241,7 +243,7 @@ const PostBulkActionButtons = props => ( ); export const PostList = (props) => ( - }> + ... ); @@ -364,7 +366,7 @@ Note that the `crudUpdateMany` action creator is *not* present in the `mapDispat ### Filters -You can add a filter element to the list using the `filters` prop: +You can add a filter component to the list using the `filters` prop: ```jsx const PostFilter = (props) => ( @@ -375,7 +377,7 @@ const PostFilter = (props) => ( ); export const PostList = (props) => ( - }> + ... ); @@ -385,12 +387,12 @@ The filter component must be a `` with `` children. **Tip**: `` is a special component, which renders in two ways: -- as a filter button (to add new filters) -- as a filter form (to enter filter values) +* as a filter button (to add new filters) +* as a filter form (to enter filter values) It does so by inspecting its `context` prop. -**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). +**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). 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: @@ -516,7 +518,7 @@ const PostFilter = (props) => ( ); export const PostList = (props) => ( - } filterDefaultValues={{ is_published: true }}> + ... ); @@ -531,7 +533,7 @@ const filterSentToDataProvider = { ...filterDefaultValues, ...filterChosenByUser ### Pagination -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. +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. For instance, you can modify the default pagination by adjusting the "rows per page" selector. @@ -542,7 +544,7 @@ import { Pagination } from 'react-admin'; const PostPagination = props => export const PostList = (props) => ( - }> + ... ); @@ -564,12 +566,12 @@ const PostPagination = ({ page, perPage, total, setPage }) => { nbPages > 1 && {page > 1 && - } {page !== nbPages && - } @@ -578,7 +580,7 @@ const PostPagination = ({ page, perPage, total, setPage }) => { } export const PostList = (props) => ( - }> + ... ); @@ -600,7 +602,7 @@ const Aside = () => ( ); const PostList = props => ( - } {...props}> + ... ``` @@ -761,8 +763,8 @@ const MyDatagridRow = ({ record, resource, id, onToggleItem, children, selected, ) -const MyDatagridBody = props => } />; -const MyDatagrid = props => } />; +const MyDatagridBody = props => ; +const MyDatagrid = props => ; const PostList = props => ( @@ -842,7 +844,7 @@ const PostPanel = ({ id, record, resource }) => ( const PostList = props => ( - }> + @@ -856,9 +858,9 @@ const PostList = props => ( ![expandable panel](./img/datagrid_expand.gif) -The `expand` prop expects an element as value. When the user chooses to expand the row, the Datagrid clones the element, and passes the current `record`, `id`, and `resource`. +The `expand` prop expects an component as value. When the user chooses to expand the row, the Datagrid render the component, and passes the current `record`, `id`, and `resource`. -**Tip**: Since the `expand` element receives the same props as a detail view, you can actually use a `` view as element for the `expand` prop: +**Tip**: Since the `expand` element receives the same props as a detail view, you can actually use a `` view as component for the `expand` prop: ```js const PostShow = props => ( @@ -875,7 +877,7 @@ const PostShow = props => ( const PostList = props => ( - }> + @@ -888,7 +890,7 @@ const PostList = props => ( The result will be the same as in the previous snippet, except that `` encloses the content inside a material-ui ``. -**Tip**: You can go one step further and use an `` view as `expand` element, albeit with a twist: +**Tip**: You can go one step further and use an `` view as `expand` component, albeit with a twist: ```js const PostEdit = props => ( @@ -908,7 +910,7 @@ const PostEdit = props => ( const PostList = props => ( - }> + @@ -1127,7 +1129,7 @@ const CategoriesActions = props => ( export const CategoriesList = (props) => ( - }> + @@ -1143,7 +1145,7 @@ export const CategoriesList = (props) => ( export const CategoriesList = (props) => ( - }> + @@ -1186,9 +1188,9 @@ const CommentGrid = ({ ids, data, basePath }) => ( {ids.map(id => } - subheader={} - avatar={} />} + title={props => } + subheader={props => } + avatar={props => } {...props} />} /> @@ -1243,7 +1245,7 @@ const UserFilter = ({ permissions, ...props }) => export const UserList = ({ permissions, ...props }) => } + filters={props => } sort={{ field: 'name', order: 'ASC' }} > { return Post {record ? `"${record.title}"` : ''}; }; export const PostShow = (props) => ( - } {...props}> + ... ); @@ -88,7 +88,7 @@ export const PostShow = (props) => ( ### Actions -You can replace the list of default actions by your own element using the `actions` prop: +You can replace the list of default actions by your own component using the `actions` prop: ```jsx import Button from '@material-ui/core/Button'; @@ -103,7 +103,7 @@ const PostShowActions = ({ basePath, data, resource }) => ( ); export const PostShow = (props) => ( - } {...props}> + ... ); @@ -125,7 +125,7 @@ const Aside = () => ( ); const PostShow = props => ( - } {...props}> + ... ``` @@ -258,7 +258,7 @@ import { const ScrollableTabbedShowLayout = props => ( - }> + }> ... @@ -292,7 +292,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 2e237d20960..31c17be1627 100644 --- a/docs/Theming.md +++ b/docs/Theming.md @@ -104,7 +104,7 @@ export const VisitorList = withStyles(listStyles)(({ classes, ...props }) => ( } + filters={VisitorFilter} sort={{ field: 'last_seen', order: 'DESC' }} perPage={25} > @@ -399,12 +399,12 @@ const MyUserMenu = props => ( } + leftIcon={SettingsIcon} /> ); -const MyAppBar = props => } />; +const MyAppBar = props => ; const MyLayout = props => ; ``` @@ -433,9 +433,9 @@ const MyCustomIcon = withStyles(myCustomIconStyle)( ) ); -const MyUserMenu = props => (} />); +const MyUserMenu = props => (); -const MyAppBar = props => } />; +const MyAppBar = props => ; ``` {% endraw %} @@ -851,7 +851,7 @@ const MyError = ({
diff --git a/examples/demo/src/orders/OrderEdit.js b/examples/demo/src/orders/OrderEdit.js index 14ced2caf15..8aedf2d302c 100644 --- a/examples/demo/src/orders/OrderEdit.js +++ b/examples/demo/src/orders/OrderEdit.js @@ -29,13 +29,13 @@ const editStyles = { }; const OrderEdit = props => ( - } aside={} {...props}> + - `${choice.first_name} ${choice.last_name}` + optionText={({ record }) => + `${record.first_name} ${record.last_name}` } /> diff --git a/examples/demo/src/orders/OrderList.js b/examples/demo/src/orders/OrderList.js index a00c491206b..d45dfc22541 100644 --- a/examples/demo/src/orders/OrderList.js +++ b/examples/demo/src/orders/OrderList.js @@ -34,8 +34,8 @@ const OrderFilter = withStyles(filterStyles)(({ classes, ...props }) => ( - `${choice.first_name} ${choice.last_name}` + optionText={({ record }) => + `${record.first_name} ${record.last_name}` } /> @@ -169,7 +169,7 @@ const OrderList = ({ classes, ...props }) => ( filterDefaultValues={{ status: 'ordered' }} sort={{ field: 'date', order: 'DESC' }} perPage={25} - filters={} + filters={OrderFilter} >
diff --git a/examples/demo/src/products/ProductEdit.js b/examples/demo/src/products/ProductEdit.js index 3951dc263cd..f938c049d86 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 27f9f243bfa..114f758f23a 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 => ( } + filters={ProductFilter} perPage={20} sort={{ field: 'id', order: 'ASC' }} > diff --git a/examples/demo/src/reviews/ReviewEdit.js b/examples/demo/src/reviews/ReviewEdit.js index 7d421a086d9..469ea769d45 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={} + toolbar={ReviewEditToolbar} > ( /> - `${choice.first_name} ${choice.last_name}` + optionText={({ record }) => + `${record.first_name} ${record.last_name}` } /> diff --git a/examples/demo/src/reviews/ReviewList.js b/examples/demo/src/reviews/ReviewList.js index 6b4110852a1..acc410eaa95 100644 --- a/examples/demo/src/reviews/ReviewList.js +++ b/examples/demo/src/reviews/ReviewList.js @@ -59,10 +59,8 @@ class ReviewList extends Component { className={classnames(classes.list, { [classes.listWithDrawer]: isMatch, })} - bulkActionButtons={ - - } - filters={} + bulkActionButtons={ReviewsBulkActionButtons} + filters={ReviewFilter} perPage={25} sort={{ field: 'date', order: 'DESC' }} > diff --git a/examples/demo/src/visitors/VisitorEdit.js b/examples/demo/src/visitors/VisitorEdit.js index ba7c4443bb4..99a32243718 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}> + ( } + filters={VisitorFilter} 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 c4e53e2b58a..c92aacb7b7d 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={} - pagination={} + filters={CommentFilter} + pagination={CommentPagination} component="div" > } medium={} /> diff --git a/examples/simple/src/comments/PostQuickCreate.js b/examples/simple/src/comments/PostQuickCreate.js index 82679defb5a..a7d3df33e7c 100644 --- a/examples/simple/src/comments/PostQuickCreate.js +++ b/examples/simple/src/comments/PostQuickCreate.js @@ -67,12 +67,13 @@ class PostQuickCreateView extends Component { save={this.handleSave} saving={submitting} redirect={false} - toolbar={ + toolbar={props => ( - } + )} classes={{ form: classes.form }} > diff --git a/examples/simple/src/posts/PostCreate.js b/examples/simple/src/posts/PostCreate.js index e5944eb8233..9cf6601bc57 100644 --- a/examples/simple/src/posts/PostCreate.js +++ b/examples/simple/src/posts/PostCreate.js @@ -83,7 +83,7 @@ const getDefaultDate = () => new Date(); const PostCreate = ({ permissions, ...props }) => ( } + toolbar={PostCreateToolbar} 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 9aa7ba03c63..4880644d316 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 d7d8500b610..8ebfd9c012a 100644 --- a/examples/simple/src/posts/PostList.js +++ b/examples/simple/src/posts/PostList.js @@ -94,8 +94,8 @@ const PostPanel = ({ id, record, resource }) => ( const PostList = withStyles(styles)(({ classes, ...props }) => ( } - filters={} + bulkActionButtons={PostListBulkActions} + filters={PostFilter} sort={{ field: 'published_at', order: 'DESC' }} > ( /> } medium={ - }> + ( ); const PostShow = props => ( - } {...props}> + {controllerProps => ( - + diff --git a/examples/simple/src/users/UserCreate.js b/examples/simple/src/users/UserCreate.js index 460bef204ca..58a773baef5 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={