React-admin 2.2 Is Out With Export, Clone, And More

François Zaninotto
François ZaninottoAugust 16, 2018
#admin-on-rest#react-admin#react#oss

We've been using react-admin a lot lately, and our customers have been using it a lot, too. The feedback from our developers and our customers helped us prioritize much needed features, and that's what react-admin 2.2 brings out. Upgrade as soon as you can to get the following enhancements:

Export Button

The ability to export data from a list was the longest outstanding enhancement request, opened almost two years ago. It turns out it was not easy to make, and that explains why it took us so long to implement such an essential feature.

You see, exporting data in CSV is easy. But what people expect from an export button is much more than a simple JSON to CSV conversion. It's the ability to get the same data they currently have on screen, including filters, sorts, column order, and, most important, relationships. React-admin fetches related fields lazily, on render, and through Redux. It is not something that can be reused in an export feature. And we didn't want to reuse the Field components for the exports, because they render as HTML, not as raw export.

Long story short, we found a way. It's elegant, versatile, and it's now enabled by default on all list views.

Post List With Export Button

By default, clicking on the Export button does the following:

  1. Call the dataProvider with the current sort and filter (but without pagination),
  2. Transform the result into a CSV string,
  3. Download the CSV file.

The columns of the CSV file match all the fields of the records in the dataProvider response. That means that the export doesn't take into account the selection and ordering of fields in your <List> via Field components. If you want to customize the result, pass a custom exporter function to the <List>. This function will receive the data from the dataProvider (after step 1), and replace steps 2-3 (i.e. it's in charge of transforming, converting, and downloading the file).

Tip: For CSV conversion, you can import Papaparse, a CSV parser and stringifier which is already a react-admin dependency. And for CSV download, take advantage of react-admin's downloadCSV function.

Here is an example for a Posts exporter, omitting, adding, and reordering fields:

// in PostList.js
import { List, downloadCSV } from 'react-admin';
import { unparse as convertToCSV } from 'papaparse/papaparse.min';

const exporter = posts => {
    const postsForExport = posts.map(post => {
        // add a field from an embedded resource
        post.author_name = post.author.name;
        return postForExport;
    });
    const csv = convertToCSV({
        data: postsForExport,
        // select and order fields in the export
        fields: ['id', 'title', 'author_name', 'body']
    });
    downloadCSV(csv, 'posts'); // download as 'posts.csv` file
})

const PostList = props => (
    <List {...props} exporter={exporter}>
        ...
    </List>
)

In many cases, you'll need more than simple object manipulation. You'll need to augment your objects based on relationships. For instance, the export for comments should include the title of the related post - but the export only exposes a post_id by default. For that purpose, the exporter receives a fetchRelatedRecords function as second parameter. It fetches related records using your dataProvider and Redux, and returns a Promise.

Here is an example for a Comments exporter, fetching related Posts:

// in CommentList.js
import { List, downloadCSV } from 'react-admin';
import { unparse as convertToCSV } from 'papaparse/papaparse.min';

const exporter = (records, fetchRelatedRecords) => {
    fetchRelatedRecords(records, 'post_id', 'posts').then(posts => {
        const data = posts.map(record => ({
                ...record,
                post_title: posts[record.post_id].title,
        }));
        const csv = convertToCSV({
            data,
            fields: ['id', 'post_id', 'post_title', 'body'],
        });
        downloadCSV(csv, 'comments');
    });
};

const CommentList = props => (
    <List {...props} exporter={exporter}>
        ...
    </List>
)

Under the hood, fetchRelatedRecords() uses react-admin's sagas, which trigger the loading spinner while loading. As a bonus, all the records fetched during an export are kepts in the main Redux store, so further browsing the admin will be accelerated.

Tip: The <ExportButton> limits the main request to the dataProvider to 1,000 records. If you want to increase or decrease this limit, pass a maxResults prop to the <ExportButton> in a custom <ListActions> component.

Tip: For complex (or large) exports, fetching all the related records and assembling them client-side can be slow. In that case, create the CSV on the server side, and replace the <ExportButton> component by a custom one, fetching the CSV route.

Dense AppBar On Desktop

User Interface Designers have a special word for what's not data in user interfaces: "Chrome". The following definition comes from Nielsen Norman Group:

'Chrome' is the user interface overhead that surrounds user data and web page content. Although chrome obesity can eat half of the available pixels, a reasonable amount enhances usability.

Material Design naturally leads to a lot of chrome in the UI, sometimes too much. In admin interfaces, efficiency is king. Our customers have told us that the current layout of react-admin doesn't leave enough screen real estate to data. We believe that to find the right balance between chrome and content, we must remove some chrome. So we're in the process of reducing the chrome, and that starts by a simple change: the AppBar is now dense on desktop.

Before After
Normal app bar dense app bar

We'll continue to work on the react-admin UI in the upcoming releases, removing more and more chrome, in order to maximize the usability for admin users.

Callback Side Effect

Let's face it: Redux can sometimes be a pain, especially when you want to sequence several actions in a row. It's achievable (through a custom saga), but it's really not convenient. We've experienced it ourselves while building complex interactions in custom admins.

React-admin already offers the ability to trigger specific side effects after a fetch action succeeds or fail, using the onSuccess and onFailure metas. The following example shows a Redux action creator calling the dataProvider (via the fetch meta), and displaying a notification on success and on failure:

// in src/comment/commentActions.js
import { UPDATE } from 'react-admin';
export const COMMENT_APPROVE = 'COMMENT_APPROVE';
export const commentApprove = (id, data, basePath) => ({
    type: COMMENT_APPROVE,
    payload: { id, data: { ...data, is_approved: true } },
    meta: {
        resource: 'comments',
        fetch: UPDATE,
        onSuccess: {
            notification: {
                body: 'resources.comments.notification.approved_success',
                level: 'info',
            },
        },
        onFailure: {
            notification: {
                body: 'resources.comments.notification.approved_failure',
                level: 'warning',
            },
        },
    },
});

As explained in the custom actions documentation, react-admin supports a wide range of side effects, including notification, redirect, refresh, and unselectAll. Starting with react-admin 2.2, you can add your own custom side effects to an action creator, using the callback meta:

// in src/comment/commentActions.js
import { UPDATE } from 'react-admin';
export const COMMENT_APPROVE = 'COMMENT_APPROVE';
export const commentApprove = (id, data, basePath) => ({
    type: COMMENT_APPROVE,
    payload: { id, data: { ...data, is_approved: true } },
    meta: {
        resource: 'comments',
        fetch: UPDATE,
        onSuccess: {
            notification: {
                body: 'resources.comments.notification.approved_success',
                level: 'info',
            },
+           callback: ({ payload, requestPayload }) => { /* your own logic */ }
        },
        onFailure: {
            notification: {
                body: 'resources.comments.notification.approved_failure',
                level: 'warning',
            },
+           callback: ({ payload, requestPayload }) => { /* your own logic */ }
        },
    },
});

Here is an example usage of this new side effect: we've built a custom Creation form, embedded in a dialog, to quickly create tags while setting the list of tags for a post. Here is the gist of this quick create form:

    handleSave = values => {
        const { dispatch, onSave } = this.props;
        dispatch({
            type: 'QUICK_CREATE',
            payload: { data: values },
            meta: {
                fetch: CREATE,
                resource: 'posts',
                onSuccess: {
                    callback: ({ payload: { data } }) => onSave(data),
                },
                onError: {
                    callback: ({ error }) => this.setState({ error }),
                },
            },
        });
     };

The callback side effect greatly reduces the need for a custom saga, so it should make your code easier to write and maintain.

Error Page

We all write code with no bugs, right? Or maybe not all the time. Sometimes we let a few bugs slip inside a beautifully crafted admin. A runtime bug in react-admin used to lead to a blank screen. Developers still had the ability to get the stack trace from the console, but the admin users were left in deep skepticism and apprehension due to the White Screen Of Death™.

React-admin 2.2 introduces the Error page, which lets users navigate back after a runtime error. It even displays the stack trace in development mode.

Error page

You can customize the Error page for your admin, by passing an Error component to the <Layout>, as follows:

// in src/MyError.js
import React from 'react';

const MyError = ({ error, errorInfo }) => (
    <div>
        <h2>{translate(error.toString())}</h2>
        {errorInfo.componentStack}
    </div>
);

export default MyError;

// in src/MyLayout.js
import React from 'react';
import { Layout } from 'react-admin';
import MyError from './MyError';

const MyLayout = (props) => <Layout error={MyError} {...props} />;

export default MyLayout;

// in src/App.js
import React from 'react';
import MyLayout from './MyLayout';

const App = () => (
    <Admin appLayout={MyLayout}>
        // ...
    </Admin>
);

Clear Button On Inputs

In Edition and Filter forms, users often want to clear the current value before entering a new one. As in classic form inputs, users currently do that by selecting the value with the mouse, then clicking the Delete key. But that's a complicated interaction. Modern User Interfaces designers have long invented an easier way to do it: the clear button.

It's 2018, and it's time react-admin embraces Modern User Interfaces. Without further ado, here it is in action:

Clear button on input

To enable the clear button on Textinput, LongTextInput, or SelectInput, just add the resettable prop:

import { TextInput } from 'react-admin';

 <TextInput source="title" resettable />

Clone Button

How hard is it to create a record based on another one? With react-admin 2.2, it's as simple as adding the <CloneButton>. It copies the current record (minus the id), redirects the user to a Create view, and pastes the data in the form:

clone button in action

Use the <CloneButton> in any <List>, <Show>, or <Edit> view, just like you use the <EditButton>. Note that in the case of the <Edit> view, the <CloneButton> copies the unedited values, so any changes in the current form aren't copied back to the new record.

import { List, Datagrid, TextField, CloneButton } from 'react-admin';
const PostList = props => (
    <List {...props}>
        <Datagrid>
            <TextField source="title" />
            <CloneButton />
        </Datagrid>
    </List>
)

<List> Passes The Same Props To All Its Children

The <List> component usually displays actions, filters, a list of data, and pagination. <List> collects and prepares props for all these components, and until now, it only passed to each component a subset of the props. So for instance, <List> didn't inject the currentSort prop to the <Pagination> component, or the setFilters to the <Datagrid> component. This drastically reduced the level of customization of the <List> children.

Starting with react-admin 2.2, <List> injects all its computed props to the actions, filters, data list, and pagination components. Here are all the props injected to the these components:

  • currentSort
  • data
  • defaultTitle
  • displayedFilters
  • filterValues
  • hasCreate
  • hideFilter
  • ids
  • isLoading
  • onSelect
  • onToggleItem
  • onUnselectItems
  • page
  • perPage
  • resource
  • selectedIds
  • setFilters
  • setPage
  • setPerPage
  • setSort
  • showFilter
  • total

Getting access to the filters in the Datagrid allows us to create cool custom grids, like this Tabbed Datagrid, available in the demo:

Tabbed Datagrid

So if you want to add a "Reset Sort" button to the actions list, that's possible with react-admin 2.2:

// in src/PostList.js
import Button from '@material-ui/core/Button';
import { CardActions, CreateButton, ExportButton, RefreshButton } from 'react-admin';

const ResetButton = ({ setSort }) => (
    <Button label="Reset sort" onClick={() => setSort((({ field: 'id', order: 'DESC' })))} />
);

const PostActions = ({ setSort }) => (
    <CardActions>
        ...
        <ResetButton setSort={setSort} />
    </CardActions>
);

export const PostList = (props) => (
    <List {...props} actions={<PostActions />}>
        ...
    </List>
);

Tip: Incidentally, this change enabled the <ExportButton> enhancement that I've mentioned earlier, because the list of actions now has the currentSort prop injected.

Conclusion

There are more enhancements in this release, that I can't detail in this post or it'll be too long:

  • Add validation for dataProvider response format
  • Add Tooltip To Icon Buttons
  • Add ability to alter values after submission and before saving them
  • Add support for forms in lists
  • Add support for asyncBlurFields in forms
  • Add redirection to the previous page when a FETCH_ERROR occurs

The 2.2 release also pushes a few bug fixes. The full Changelog is available at GitHub.

React-admin 2.2 is backwards compatible with react-admin 2.0 and 2.1, so the upgrade should be super smooth. As always, please open an issue in the react-admin GitHub tracker if it's not the case.

React-admin evolves quickly, thanks to the abundant feedback of developers and end users. We're getting a clear idea of what makes a great admin, and what makes a great library for building a great admin. Look forward to more enhancements in the future releases!