Skip to content

Latest commit

 

History

History
1096 lines (916 loc) · 43 KB

UPGRADE.md

File metadata and controls

1096 lines (916 loc) · 43 KB

Upgrade to 2.0

Admin-on-rest Renamed to React-Admin

We've chosen to remove term REST from the project name, to emphasize the fact that it can adapt to any type of backend - including GraphQL.

So the main package name has changed from admin-on-rest to react-admin. You must update your dependencies:

npm uninstall admin-on-rest
npm install react-admin

As well as all your files depending on the 'admin-on-rest' package:

- import { BooleanField, NumberField, Show } from 'admin-on-rest'; 
+ import { BooleanField, NumberField, Show } from 'react-admin'; 

A global search and replace on the string "admin-on-rest" should do the trick in no time.

restClient Prop Renamed To dataProvider in <Admin> Component

In the <Admin> component, the restClient prop is now called dataProvider:

import restClient from './restClient';
- <Admin restClient={restClient}>
+ <Admin dataProvider={restClient}>
   ...
</Admin>

The signature of the Data Provider function is the same as the REST client function, so you shouldn't need to change anything in your previous REST client function.

Once again, this change de-emphasizes the "REST" term in admin-on-rest.

Default REST Clients Moved to Standalone Packages

simpleRestClient and jsonServerRestClient are no longer part of the core package. They have been moved to standalone packages, where they are the default export:

  • simpleRestClient => ra-data-simple-rest
  • jsonServerRestClient => ra-data-json-server

Update your import statements accordingly:

- import { simpleRestClient } from 'admin-on-rest';
+ import simpleRestClient from 'ra-data-simple-rest';

- import { jsonServerRestClient } from 'admin-on-rest';
+ import jsonServerRestClient from 'ra-data-json-server';

authClient Prop Renamed To authProvider in <Admin> Component

In the <Admin> component, the authClient prop is now called authProvider:

- import authClient from './authClient';
+ import authProvider from './authProvider';
- <Admin authClient={authClient}>
+ <Admin authProvider={authProvider}>
   ...
</Admin>

The signature of the authorizations provider function is the same as the authorizations client function, so you shouldn't need to change anything in your previous authorizations client function.

Default (English) Messages Moved To Standalone Package

The English messages have moved to another package, ra-language-english. The core package still displays the interface messages in English by default (by using ra-language-english as a dependency), but if you overrode some of the messages, you'll need to update the package name:

- import { enMessages } from 'admin-on-rest';
+ import enMessages from 'ra-language-english';
const messages = { 'en': enMessages };

Message Hash Main Key Changed ("aor" => "ra")

The main key of translation message objects was renamed from "aor" ro "ra". You must update your custom messages accordingly if you overrode core interface messages. If you're a language package author, you must also update and republish your package to have it work with react-admin 2.0.

module.exports = {
-    aor: {
+    ra: {
        action: {
            delete: 'Delete',
            show: 'Show',
            ...

Removed the Delete view in Resource

Admin-on-rest used to have a special Delete view, accessible with a special URL, to display a confirmation message after a user clicked on the Delete button. This view added complexity to the early stages of development with admin-on-rest. Besides, it provided a mediocre user experience.

In react-admin, the deletion confirmation is now a Dialog that opens on top of the page where the user currently is.

As a consequence, you no longer need to pass a value to the remove prop in Resources:

-  <Resource name="posts" list={PostList} edit={PostEdit} show={PostShow} remove={Delete} />
+  <Resource name="posts" list={PostList} edit={PostEdit} show={PostShow} />

That also means that if you disabled deletion on a Resource by not passing a remove prop, you will be surprised by Delete buttons popping in the Edit views. The way to remove this button is to Customize the Edit Toolbar.

Replaced messages by i18nProvider in <Admin>

In admin-on-rest, localization messages were passed as an object literal in the messages props of the <Admin> component. To do the same in react-admin, you must now use a slightly more lengthy syntax, and pass a function in the i18nProvider prop instead.

- import { Admin, enMessages } from 'admin-on-rest';
- import frMessages from 'aor-language-french';
+ import { Admin } from 'react-admin';
+ import enMessages from 'ra-language-english';
+ import frMessages from 'ra-language-french';

const messages = {
    en: enMessages,
    fr: frMessages,
};

- const App = () => <Admin locale="en" messages={messages} />;
+ const i18nProvider = locale => messages[locale];
+ const App = () => <Admin locale="en" i18nProvider={i18nProvider} />;

The new i18nProvider allows to load the messages asynchronously - see the i18nProvider documentation for details.

crudSaga renamed to adminSaga

If you don't use the <Admin> component, but prefer to implement your administration inside another root component, you probably followed the Custom App documentation, and used the crudSaga. This property was renamed to adminSaga

// in src/App.js
- import { crudSaga, ... } from 'admin-on-rest';
+ import { adminSaga, ... } from 'react-admin';

// ...
- sagaMiddleware.run(crudSaga(dataProvider, i18nProvider));
+ sagaMiddleware.run(adminSaga(dataProvider, authProvider, i18nProvider));

<AutocompleteInput> no longer accepts a filter prop

Material-ui's implementation of the autocomplete input has radically changed. React-admin maintains backwards compatibility, except for the filter prop, which no longer makes sense in the new implementation.

<Datagrid> No Longer Accepts options, headerOptions, bodyOptions, and rowOptions props

Material-ui's implementation of the <Table> component has reduced dramatically. Therefore, all the advanced features of the datagrid are no longer available from react-admin.

If you need a fixed header, row hover, multi-row selection, or any other material-ui 0.x <Table> feature, you'll need to implement your own <Datagrid> alternative, e.g. using the library recommended by material-ui, DevExtreme React Grid.

<DateInput> Stores a Date String Instead Of a Date Object

The value of the <DateInput> used to be a Date object. It's now a String, i.e. a stringified date. If you used format and parse to convert a string to a Date, you can now remove these props:

- const dateFormatter = v => { // from record to input
-   // v is a string of "YYYY-MM-DD" format
-   const match = /(\d{4})-(\d{2})-(\d{2})/.exec(v);
-   if (match === null) return;
-   const d = new Date(match[1], parseInt(match[2], 10) - 1, match[3]);
-   if (isNaN(d)) return;
-   return d;
- };
- const dateParser = v => { // from input to record
-   // v is a `Date` object
-   if (!(v instanceof Date) || isNaN(v)) return;
-   const pad = '00';
-   const yy = v.getFullYear().toString();
-   const mm = (v.getMonth() + 1).toString();
-   const dd = v.getDate().toString();
-   return `${yy}-${(pad + mm).slice(-2)}-${(pad + dd).slice(-2)}`;
- };
- <DateInput source="isodate" format={dateFormatter} parse={dateParser} label="ISO date" />
+ <DateInput source="isodate" label="ISO date" />

On the other way around, if your data provider expects JavaScript Date objects for value, you now need to do the conversion to and from strings using format and parse:

- <DateInput source="isodate" label="ISO date" />
+ const dateFormatter = v => { // from record to input
+   // v is a `Date` object
+   if (!(v instanceof Date) || isNaN(v)) return;
+   const pad = '00';
+   const yy = v.getFullYear().toString();
+   const mm = (v.getMonth() + 1).toString();
+   const dd = v.getDate().toString();
+   return `${yy}-${(pad + mm).slice(-2)}-${(pad + dd).slice(-2)}`;
+ };
+ const dateParser = v => { // from input to record
+   // v is a string of "YYYY-MM-DD" format
+   const match = /(\d{4})-(\d{2})-(\d{2})/.exec(v);
+   if (match === null) return;
+   const d = new Date(match[1], parseInt(match[2], 10) - 1, match[3]);
+   if (isNaN(d)) return;
+   return d;
+ };
+ <DateInput source="isodate" format={dateFormatter} parse={dateParser} label="ISO date" />

Removed <DateInput> options props

Material-ui 1.0 doesn't provide a real date picker, so the options prop of the <DateInput> is no longer supported.

<SelectArrayInput> does not support autocompletion anymore.

This component relied on material-ui-chip-input which is not yet fully ported to Material-ui 1.0: it doesn't support the autocomplete feature we need. We will add another component for this when material-ui-chip-input is ported.

CSS Classes Changed

React-admin does not rely heavily on CSS classes. Nevertheless, a few components added CSS classes to facilitate per-field theming: <SimpleShowLayout>, <Tab>, and <FormInput>. These CSS classes used to follow the "aor-" naming pattern. They have all been renamed to use the "ra-" pattern instead. Here is the list of concerned classes:

  • aor-field => ra-field
  • aor-field-[source] => ra-field-[source]
  • aor-input => ra-input
  • aor-input-[source] => ra-input-[source]

If you used CSS to customize the look and feel of these components, please update your CSS selectors accordingly.

addField Prop Replaced By addField HOC

Adding the addField prop to a component used to automatically add a redux-form <Field> component around an input component that you wanted to bind to the edit or create form. This feature was moved to a Higher-order component (HOC):

import SelectField from '@material-ui/core/SelectField';
import MenuItem from '@material-ui/core/MenuItem';
+ import { addField } from 'react-admin';
const SexInput = ({ input, meta: { touched, error } }) => (
    <SelectField
        floatingLabelText="Sex"
        errorText={touched && error}
        {...input}
    >
        <MenuItem value="M" primaryText="Male" />
        <MenuItem value="F" primaryText="Female" />
    </SelectField>
);
- SexInput.defaultProps = {
-     addField: true, // require a <Field> decoration
- }
- export default SexInput;
+ export default addField(SexInput);

Admin-on-rest input components all use the new addField HOC. This means that it's no longer necessary to set the addField prop when you compose one of admin-on-rest's components:

- import { SelectInput } from 'admin-on-rest';
+ import { SelectInput } from 'react-admin';
const choices = [
    { id: 'M', name: 'Male' },
    { id: 'F', name: 'Female' },
]
const SexInput = props => <SelectInput {...props} choices={choices}/>;
- SexInput.defaultProps = {
-     addField: true;
- }
export default SexInput;

No More refresh Prop Passed To <List> Actions

The Refresh button now uses Redux to force a refetch of the data. As a consequence, the List view no longer passes the refresh prop to the <Actions> component. If you relied on that prop to refresh the list, you must now use the new <RefreshButton> component.

import CardActions from '@material-ui/core/CardActions';
- import FlatButton from '@material-ui/core/FlatButton';
- import { CreateButton } from 'admin-on-rest';
- import NavigationRefresh from '@material-ui/core/svg-icons/navigation/refresh';
+ import { CreateButton, RefreshButton } from 'react-admin';

- const PostListActions = ({ resource, filters, displayedFilters, filterValues, basePath, showFilter, refresh }) => (
+ const PostListActions = ({ resource, filters, displayedFilters, filterValues, basePath, showFilter }) => (
    <CardActions>
        {filters && React.cloneElement(filters, { resource, showFilter, displayedFilters, filterValues, context: 'button' }) }
        <CreateButton basePath={basePath} />
-       <FlatButton primary label="refresh" onClick={refresh} icon={<NavigationRefresh />} />
+       <RefreshButton />
    </CardActions>
);

Customizing Styles

Following the same path as Material UI, react-admin now uses JSS for styling components instead of the style prop. This approach has many benefits, including a smaller DOM, faster rendering, media queries support, and automated browser prefixing.

All react-admin components now accept a className prop instead of the elStyle prop. But it expects a CSS class name instead of a CSS object. To set custom styles through a class name, you must use the withStyles Higher Order Component supplied by Material-UI.

- import { EmailField, List, Datagrid } from 'admin-on-rest';
- const UserList = props => (
-     <List {...props}>
-         <Datagrid>
-             ...
-             <EmailField source="email" elStyle={{ textDecoration: 'none' }} />
-         </Datagrid>
-     </List>
- );
- export default UserList;
// renders in the datagrid as
//<td>
//    <a style="text-decoration:none" href="mailto:foo@example.com">foo@example.com</a>
//</td>
+ import { EmailField, List, Datagrid } from 'react-admin';
+ import { withStyles } from '@material-ui/core/styles';
+ const styles = {
+     field: {
+         textDecoration: 'none',
+     },
+ };
+ const UserList = ({ classes, ...props }) => (
+     <List {...props}>
+         <Datagrid>
+            ...
+             <EmailField source="email" className={classes.field} />
+         </Datagrid>
+     </List>
+ );
+ export default withStyles(styles)(UserList);

In addition to elStyle, Field and Input components used to support a style prop to override the styles of the container element (the <td> in a datagrid). This prop is no longer supported in react-admin. Instead, the Datagrid component will check if its children have a headerClassName and cellClassName props. If they do, it will apply those classes to the table header and cells respectively.

- import { EmailField, List, Datagrid } from 'admin-on-rest';
+ import { EmailField, List, Datagrid } from 'react-admin';
+ import { withStyles } from '@material-ui/core/styles';

+ const styles = {
+     cell: {
+         backgroundColor: 'lightgrey',
+     },
+     field: {
+         textDecoration: 'none',
+     },
+ };

- const UserList = props => (
+ const UserList = ({ classes, ...props }) => (
    <List {...props}>
        <Datagrid>
-           <EmailField source="email" style={{ backgroundColor: 'lightgrey' }} elStyle={{ textDecoration: 'none' }} />
+           <EmailField source="email" cellClassName={classes.cell} className={classes.field} />
        </Datagrid>
    </List>
);

- export default UserList;
+ export default withStyles(styles)(UserList);
// renders in the datagrid as
// <td style="background-color:lightgrey">
//     <a style="text-decoration:none" href="mailto:foo@example.com">
//         foo@example.com
//     </a>
// </td>

Furthermore, some React-admin components such as the List, Filter, and Datagrid also accept a classes prop. This prop is injected by the withStyles Higher Order Component and allows you to customize the style of some deep children. See the Theming documentation for details.

Tip: When you set the classes prop in the List or Datagrid components, you might see warnings about the cell and field classes being unknown by those components. Those warnings are not displayed in production mode, and are just a way to ensure you know what you're doing. And you can make them disappear by destructuring the classes prop:

import { EmailField, List, Datagrid } from 'react-admin';
import { withStyles } from '@material-ui/core/styles';

const styles = {
    header: { fontWeight: 'bold' },
    actions: { fontWeight: 'bold' },
    emailCellClassName: {
        backgroundColor: 'lightgrey',
    },
    emailFieldClassName: {
        textDecoration: 'none',
    },
};

export const UserList = ({
    classes: { emailCellClassName, emailFieldClassName, ...classes },
    ...props
}) => (
    <List
        {...props}
        filters={<UserFilter />}
        sort={{ field: 'name', order: 'ASC' }}
        classes={classes}
    >
        <Datagrid>
            <EmailField
                source="email"
                cellClassName={emailCellClassName}
                className={emailFieldClassName}
            />
        </Datagrid>
    </List>
);

// renders in the datagrid as
<td style="background-color:lightgrey">
    <a style="text-decoration:none" href="mailto:foo@example.com">
        foo@example.com
    </a>
</td>

Finally, Field and Input components accept a textAlign prop, which can be either left, or right. Through this prop, these components inform their parent component that they look better when aligned to left or right. It's the responsability of the parent component to apply this alignment. For instance, the NumberField component has a default value of right for the textAlign prop, so the Datagrid component uses a right alignment in header and table cell - but form components (SimpleForm and TabbedForm) ignore the prop and display it left aligned.

Authentication: <Restricted> renamed to <Authenticated>

The Restricted component has been renamed to Authenticated. Update your import statements accordingly:

// in src/MyPage.js
import { withRouter } from 'react-router-dom';
- import { Restricted } from 'admin-on-rest';
+ import { Authenticated } from 'react-admin';

const MyPage = ({ location }) => (
-  <Restricted authParams={{ foo: 'bar' }} location={location}>
+  <Authenticated authParams={{ foo: 'bar' }} location={location}>
        <div>
            ...
        </div>
-  </Restricted>
+  </Authenticated>
)

export default withRouter(MyPage);

Authorization: <WithPermission> and <SwitchPermissions> replaced by <WithPermissions>

We removed the WithPermission and SwitchPermissions in favor of a more versatile component: WithPermissions. The WithPermissions component retrieves permissions by calling the authProvider with the AUTH_GET_PERMISSIONS type. It then passes the permissions to the render callback.

This component follows the render callback pattern. Just like the React Router Route component, you can pass a render callback to <WithPermissions> either as its only child, or via its render prop (if both are passed, the render prop is used).

If you were using WithPermission before, here's how to migrate to WithPermissions:

import React from 'react';
- import { MenuItemLink, WithPermission } from 'admin-on-rest';
+ import { MenuItemLink, WithPermissions } from 'react-admin';

export default ({ onMenuClick, logout }) => (
    <div>
        <MenuItemLink to="/posts" primaryText="Posts" onClick={onMenuClick} />
        <MenuItemLink to="/comments" primaryText="Comments" onClick={onMenuClick} />
-       <WithPermission value="admin">
-           <MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} />
-       </WithPermission>
+       <WithPermissions>
+           {({ permissions }) => permissions === 'admin'
+               ? <MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} />
+               : null
+           }
+       </WithPermissions>
        {logout}
    </div>
);

If you were using SwitchPermissions before, here's how to migrate to WithPermissions:

// before
import React from 'react';
import BenefitsSummary from './BenefitsSummary';
import BenefitsDetailsWithSensitiveData from './BenefitsDetailsWithSensitiveData';
- import { ViewTitle, SwitchPermissions, Permission } from 'admin-on-rest';
+ import { ViewTitle, WithPermissions } from 'react-admin';

export default () => (
    <div>
-         <SwitchPermissions>
-             <Permission value="associate">
-                 <BenefitsSummary />
-             </Permission>
-             <Permission value="boss">
-                 <BenefitsDetailsWithSensitiveData />
-             </Permission>
-         </SwitchPermissions>
+         <WithPermissions>
+             {({ permissions }) => {
+                 if (permissions === 'associate') {
+                     return <BenefitsSummary />;
+                 }
+                 if (permissions === 'boss') {
+                     return <BenefitsDetailsWithSensitiveData />;
+                 }
+             }}
+         </WithPermissions>
    </div>
);

We also reviewed how permissions are passed to the List, Edit, Create, Show and Delete components. React-admin now injects the permissions to theses components in the permissions props, without having to use the render callback pattern. It should now be easier to customize behaviors and components according to permissions.

Here's how to migrate a Create component:

const UserCreateToolbar = ({ permissions, ...props }) =>
    <Toolbar {...props}>
        <SaveButton
            label="user.action.save_and_show"
            redirect="show"
            submitOnEnter={true}
        />
        {permissions === 'admin' &&
            <SaveButton
                label="user.action.save_and_add"
                redirect={false}
                submitOnEnter={false}
                variant="flat"
            />}
    </Toolbar>;

- export const UserCreate = ({ ...props }) =>
+ export const UserCreate = ({ permissions, ...props }) =>
    <Create {...props}>
-       {permissions =>
            <SimpleForm
                toolbar={<UserCreateToolbar permissions={permissions} />}
                defaultValue={{ role: 'user' }}
            >
                <TextInput source="name" validate={[required()]} />
                {permissions === 'admin' &&
                    <TextInput source="role" validate={[required()]} />}
            </SimpleForm>
-       }
    </Create>;

Here's how to migrate an Edit component:

// before
- export const UserEdit = ({ ...props }) =>
+ export const UserEdit = ({ permissions, ...props }) =>
    <Edit title={<UserTitle />} {...props}>
-       {permissions =>
            <TabbedForm defaultValue={{ role: 'user' }}>
                <FormTab label="user.form.summary">
                    {permissions === 'admin' && <DisabledInput source="id" />}
                    <TextInput source="name" validate={required()} />
                </FormTab>
                {permissions === 'admin' &&
                    <FormTab label="user.form.security">
                        <TextInput source="role" validate={required()} />
                    </FormTab>}
            </TabbedForm>
-       }
    </Edit>;

Here's how to migrate a List component. Note that the <Filter> component does not support the function as a child pattern anymore. If you need permissions within it, just pass them from the List component.

- const UserFilter = ({ ...props }) =>
+ const UserFilter = ({ permissions, ...props }) =>
    <Filter {...props}>
-       {permissions => [
            <TextInput
                key="user.list.search"
                label="user.list.search"
                source="q"
                alwaysOn
            />,
            <TextInput key="name" source="name" />,
            permissions === 'admin' ? <TextInput source="role" /> : null,
-       ]}
    </Filter>;

- export const UserList = ({ ...props }) =>
+ export const UserList = ({ permissions, ...props }) =>
    <List
        {...props}
-       filters={<UserFilter />}
+       filters={<UserFilter permissions={permissions} />}
        sort={{ field: 'name', order: 'ASC' }}
    >
-       {permissions =>
            <Responsive
                small={
                    <SimpleList
                        primaryText={record => record.name}
                        secondaryText={record =>
                            permissions === 'admin' ? record.role : null}
                    />
                }
                medium={
                    <Datagrid>
                        <TextField source="id" />
                        <TextField source="name" />
                        {permissions === 'admin' && <TextField source="role" />}
                        {permissions === 'admin' && <EditButton />}
                        <ShowButton />
                    </Datagrid>
                }
            />
-       }
    </List>;

Moreover, you won't need the now deprecated <WithPermission> or <SwitchPermissions> components inside a Dashboard to access permissions anymore: react-admin injects permissions to the dashboard, too:

// in src/Dashboard.js
import React from 'react';
import BenefitsSummary from './BenefitsSummary';
import BenefitsDetailsWithSensitiveData from './BenefitsDetailsWithSensitiveData';
- import { ViewTitle, SwitchPermissions, Permission } from 'admin-on-rest';
+ import { ViewTitle } from 'react-admin';

- export default () => (
+ export default ({ permissions }) => (
    <Card>
        <ViewTitle title="Dashboard" />
-       <SwitchPermissions>
-           <Permission value="associate">
-               <BenefitsSummary />
-           </Permission>
-           <Permission value="boss">
-               <BenefitsDetailsWithSensitiveData />
-           </Permission>
-       </SwitchPermissions>
+       {permissions === 'associate' && <BenefitsSummary />}
+       {permissions === 'boss' && <BenefitsDetailsWithSensitiveData />}
    </Card>
);

Finally, you won't need the now deprecated <WithPermission> or <SwitchPermissions> in custom routes either if you want access to permissions. Much like you can restrict access to authenticated users only with the Authenticated component, you may decorate your custom route with the WithPermissions component. It will ensure the user is authenticated and call the authProvider with the AUTH_GET_PERMISSIONS type and the authParams you specify:

{% raw %}

// in src/MyPage.js
import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import { ViewTitle, WithPermissions } from 'react-admin';
import { withRouter } from 'react-router-dom';

const MyPage = ({ permissions }) => (
    <Card>
        <ViewTitle title="My custom page" />
        <CardContent>Lorem ipsum sic dolor amet...</CardContent>
        {permissions === 'admin'
            ? <CardContent>Sensitive data</CardContent>
            : null
        }
    </Card>
)
const MyPageWithPermissions = ({ location, match }) => (
    <WithPermissions
        authParams={{ key: match.path, params: route.params }}
        // location is not required but it will trigger a new permissions check if specified when it changes
        location={location}
        render={({ permissions }) => <MyPage permissions={permissions} /> }
    />
);

export default MyPageWithPermissions;

// in src/customRoutes.js
import React from 'react';
import { Route } from 'react-router-dom';
import Foo from './Foo';
import Bar from './Bar';
import Baz from './Baz';
import MyPageWithPermissions from './MyPage';

export default [
    <Route exact path="/foo" component={Foo} />,
    <Route exact path="/bar" component={Bar} />,
    <Route exact path="/baz" component={Baz} noLayout />,
    <Route exact path="/baz" component={MyPageWithPermissions} />,
];

Custom Layouts

The default layout has been simplified, and this results in a simplified custom layout too. You don't need to pass the AdminRoutes anymore, as the layout receives the component to render as the standard children prop:

import React, { createElement, Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
- import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
+ import { MuiThemeProvider, withStyles } from '@material-ui/core/styles';
- import CircularProgress from '@material-ui/core/CircularProgress';
import {
-   AdminRoutes,
    AppBar,
    Menu,
    Notification,
    Sidebar,
-   setSidebarVisibility,
- } from 'admin-on-rest';
+ } from 'react-admin';

- const styles = {
-     wrapper: {
-         // Avoid IE bug with Flexbox, see #467
-         display: 'flex',
-         flexDirection: 'column',
-     },
-     main: {
-         display: 'flex',
-         flexDirection: 'column',
-         minHeight: '100vh',
-     },
-     body: {
-         backgroundColor: '#edecec',
-         display: 'flex',
-         flex: 1,
-         overflowY: 'hidden',
-         overflowX: 'scroll',
-     },
-     content: {
-         flex: 1,
-         padding: '2em',
-     },
-     loader: {
-         position: 'absolute',
-         top: 0,
-         right: 0,
-         margin: 16,
-         zIndex: 1200,
-     },
- };
+ const styles = theme => ({
+     root: {
+         display: 'flex',
+         flexDirection: 'column',
+         zIndex: 1,
+         minHeight: '100vh',
+         backgroundColor: theme.palette.background.default,
+         position: 'relative',
+     },
+     appFrame: {
+         display: 'flex',
+         flexDirection: 'column',
+         overflowX: 'auto',
+     },
+     contentWithSidebar: {
+         display: 'flex',
+         flexGrow: 1,
+     },
+     content: {
+         display: 'flex',
+         flexDirection: 'column',
+         flexGrow: 2,
+         padding: theme.spacing.unit * 3,
+         marginTop: '4em',
+         paddingLeft: 5,
+     },
+ });

class MyLayout extends Component {
-   componentWillMount() {
-       this.props.setSidebarVisibility(true);
-   }

    render() {
        const {
            children,
-           customRoutes,
            dashboard,
            isLoading,
            logout,
            menu,
+           open,
            title,
        } = this.props;

        return (
            <MuiThemeProvider>
-               <div style={styles.wrapper}>
+               <div className={classes.root}>
-                   <div style={styles.main}>
+                   <div className={classes.appFrame}>
-                       <AppBar title={title} />
+                       <AppBar title={title} open={open} logout={logout} />
-                       <div className="body" style={styles.body}>
+                       <main className={classes.contentWithSidebar}>
-                           <div style={styles.content}>
-                               <AdminRoutes
-                                   customRoutes={customRoutes}
-                                   dashboard={dashboard}
-                               >
-                                   {children}
-                               </AdminRoutes>
                            <Sidebar>
                                {createElement(menu || Menu, {
                                    logout,
                                    hasDashboard: !!dashboard,
                                })}
                            </Sidebar>
+                           <div className={classes.content}>
+                               {children}</div>
+                           </div>
-                       </div>
+                       </main>
                        <Notification />
-                       {isLoading && (
-                           <CircularProgress
-                               color="#fff"
-                               size={30}
-                               thickness={2}
-                               style={styles.loader}
-                           />
-                       )}
                    </div>
                </div>
            </MuiThemeProvider>
        );
    }
}

MyLayout.propTypes = {
-   authClient: PropTypes.func,
+   children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
+   classes: PropTypes.object,
-   customRoutes: PropTypes.array,
    dashboard: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
-   isLoading: PropTypes.bool.isRequired,
+   logout: PropTypes.oneOfType([PropTypes.node PropTypes.func, PropTypes.string]),
    menu: PropTypes.element,
+   open: PropTypes.bool,
-   resources: PropTypes.array,
-   setSidebarVisibility: PropTypes.func.isRequired,
    title: PropTypes.string.isRequired,
};

const mapStateToProps = state => ({
-   isLoading: state.admin.loading > 0
+   open: state.admin.ui.sidebarOpen,
});

- export default connect(mapStateToProps, { setSidebarVisibility })(MyLayout);
+ export default connect(mapStateToProps, {})(withStyles(styles)(MyLayout));

Tip: React-admin's theme is a bot more complex than that, as it is reponsive. Check out the default layout source for details.

Menu onMenuTap prop has been renamed onMenuClick

Material-ui renamed all xxxTap props to xxxClick, so did we.

import React from 'react';
import { connect } from 'react-redux';
- import { MenuItemLink, getResources } from 'admin-on-rest';
+ import { MenuItemLink, getResources } from 'react-admin';

- const Menu = ({ resources, onMenuTap, logout }) => (
+ const Menu = ({ resources, onMenuClick, logout }) => (
    <div>
        {resources.map(resource => (
-           <MenuItemLink to={`/${resource.name}`} primaryText={resource.name} onClick={onMenuTap} />
+           <MenuItemLink to={`/${resource.name}`} primaryText={resource.name} onClick={onMenuClick} />
        ))}
-       <MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuTap} />
+       <MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} />
        {logout}
    </div>
);

const mapStateToProps = state => ({
    resources: getResources(state),
});

export default connect(mapStateToProps)(Menu);

Logout is Now Displayed in the AppBar on Desktop

The Logout button is now displayed in the AppBar on desktop, but is still displayed as a menu item on small devices.

This impacts how you build a custom menu, as you'll now have to check whether you are on small devices before displaying the logout:

// in src/Menu.js
import React from 'react';
import { connect } from 'react-redux';
- import { MenuItemLink, getResources } from 'admin-on-rest';
+ import { MenuItemLink, getResources, Responsive } from 'react-admin';
import { withRouter } from 'react-router-dom';

const Menu = ({ resources, onMenuClick, logout }) => (
    <div>
        {resources.map(resource => (
            <MenuItemLink to={`/${resource.name}`} primaryText={resource.name} onClick={onMenuClick} />
        ))}
        <MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} />
-       {logout}
+        <Responsive xsmall={logout} medium={null} />
    </div>
);

const mapStateToProps = state => ({
    resources: getResources(state),
});

export default withRouter(connect(mapStateToProps)(Menu));

It also impacts custom layouts if you're using the default AppBar. You now have to pass the logout prop to the AppBar:

// in src/MyLayout.js
const MyLayout () => ({ logout, ...props }) => (
    <MuiThemeProvider>
        ...
-          <AppBar title={title} />
+          <AppBar title={title} logout={logout} />
        ...
    </MuiThemeProvider>
);

Data Providers Should Support Two More Types For Bulk Actions

The List component now support bulk actions. The consequence is that data providers should support them too. We introduced two new message types for the dataProvider: DELETE_MANY and UPDATE_MANY.

Both will be called with an ids property in their params, containing an array of resource ids. In addition, UPDATE_MANY will also get a data property in its params, defining how to update the resources.

Please refer to the dataProvider documentation for more information.

react-admin Addon Packages Renamed With ra Prefix And Moved Into Root Repository

The aor-graphql and aor-realtime packages have been migrated into the main react-admin repository and renamed with the new prefix. Besides, aor-graphql-client and aor-graphql-client-graphcool follow the new dataProvider packages naming.

  • aor-realtime => ra-realtime
  • aor-graphql-client => ra-data-graphql
  • aor-graphql-client-graphcool => ra-data-graphcool

Update your import statements accordingly:

- import realtimeSaga from 'aor-realtime';
+ import realtimeSaga from 'ra-realtime';

- import buildGraphQLProvider from 'aor-graphql-client';
+ import buildGraphQLProvider from 'ra-data-graphql';

- import buildGraphcoolProvider from 'aor-graphql-client-graphcool';
+ import buildGraphcoolProvider from 'ra-data-graphcool';

aor-dependent-input Was Removed

The aor-dependent-input package has been removed.

You can achieve a similar effect to the old <DependentInput> component by using the new <FormDataConsumer> component.

To display a component based on the value of the current (edited) record, wrap that component with <FormDataConsumer>, which uses grabs the form data from the redux-form state, and passes it to a child function:

- import { DependentInput } from 'aor-dependent-input';
+ import { FormDataConsumer } from 'react-admin';

export const UserCreate = (props) => (
    <Create {...props}>
        <SimpleForm>
            <TextInput source="firstName" />
            <TextInput source="lastName" />
            <BooleanInput source="hasEmail" label="Has email ?" />
-           <DependentInput dependsOn="hasEmail">
-                <TextInput source="email" />
-           </DependentInput>
+           <FormDataConsumer>
+               {({ formData, ...rest }) => formData.hasEmail && 
+                   <TextInput source="email" {...rest} />
+               }
+           </FormDataConsumer>
        </SimpleForm>
    </Create>
);

As for the <DependentField> in a <Show> view, you need to use an alternative approach, taking advantage of the structure of <Show>, which in fact decomposes into a controller and a view component:

// inside react-admin
const Show = props => (
    <ShowController {...props}>
        {controllerProps => <ShowView {...props} {...controllerProps} />}
    </ShowController>
);

The <ShowController> fetches the record from the data provider, and passes it to its child function when received (among the controllerProps). That means the following code:

import { Show, SimpleShowLayout, TextField } from 'react-admin';

const UserShow = props => (
    <Show {...props}>
        <SimpleShowLayout>
            <TextField source="username" />
            <TextField source="email" />
        </SimpleShowLayout>
    </Show>
);

Is equivalent to:

import { ShowController, ShowView, SimpleShowLayout, TextField } from 'react-admin';

const UserShow = props => (
    <ShowController {...props}>
        {controllerProps => 
            <ShowView {...props} {...controllerProps}>
                <SimpleShowLayout>
                    <TextField source="username" />
                    <TextField source="email" />
                </SimpleShowLayout>
            </ShowView>
        }
    </ShowController>
);

If you want one field to be displayed based on the record, for instance to display the email field only if the hasEmail field is true, you just need to test the value from controllerProps.record, as follows:

import { ShowController, ShowView, SimpleShowLayout, TextField } from 'react-admin';

const UserShow = props => (
    <ShowController {...props}>
        {controllerProps => 
            <ShowView {...props} {...controllerProps}>
                <SimpleShowLayout>
                    <TextField source="username" />
                    {controllerProps.record && controllerProps.record.hasEmail && 
                        <TextField source="email" />
                    }
                </SimpleShowLayout>
            </ShowView>
        }
    </ShowController>
);

Validators should be initialized

The required,number and email validators must now be executed just like the other validators, not passed as function arguments.

Update your require,number and email validations.

-<TextInput source="foo" validate={[required,maxSize(2)]} />
+<TextInput source="foo" validate={[required(),maxSize(2)]} />

-<TextInput source="foo" validate={number} />
+<TextInput source="foo" validate={number()} />

-<TextInput source="foo" validate={email} />
+<TextInput source="foo" validate={email()} />