Skip to content

Add invite to create the first record when the list is empty #4113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/List.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Here are all the props accepted by the `<List>` component:
* [`filterDefaultValues`](#filter-default-values) (the default values for `alwaysOn` filters)
* [`pagination`](#pagination)
* [`aside`](#aside-component)
* [`empty`](#empty-page)

Here is the minimal code necessary to display a list of posts:

Expand Down Expand Up @@ -592,6 +593,7 @@ const PostList = props => (
<List aside={<Aside />} {...props}>
...
</List>
);
```
{% endraw %}

Expand Down Expand Up @@ -625,6 +627,55 @@ const Aside = ({ data, ids }) => (
```
{% endraw %}

### Empty page

When there is no result, and there is no active filter, and the resource has a create page, react-admin displays a special page inviting the user to create the first record.

You can use the `empty` prop to replace that page by a custom component:

{% raw %}
```jsx
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import { CreateButton, List } from 'react-admin';

const Empty = ({ basePath, resource }) => (
<Box textAlign="center" m={1}>
<Typography variant="h4" paragraph>
No products available
</Typography>
<Typography variant="body1">
Create one or import from a file
</Typography>
<CreateButton basePath={basePath} />
<Button onClick={...}>Import</Button>
</Box>
);

const ProductList = props => (
<List empty={<Empty />} {...props}>
...
</List>
);
```
{% endraw %}

The `empty` component receives the same props as the `List` child component, including the following:

- `basePath`,
- `currentSort`,
- `data`,
- `defaultTitle`,
- `filterValues`,
- `ids`,
- `page`,
- `perPage`,
- `resource`,
- `selectedIds`,
- `total`,
- `version`,

### Component

By default, the List view renders the main content area inside a material-ui `<Card>` element. The actual layout of the list depends on the child component you're using (`<Datagrid>`, `<SimpleList>`, or a custom layout component).
Expand Down
9 changes: 9 additions & 0 deletions docs/Translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,15 @@ const messages = {
};
```

## Translating The Empty Page

React-admin uses the keys `ra.page.empty` and `ra.page.invite` when displaying the page inviting the user to create the first record.

If you want to override these messages in a specific resource you can add the following keys to your translation:

- `resources.${resourceName}.empty` for the primary message (e.g. "No posts yet.")
- `resources.${resourceName}.invite` for the message inviting the user to create one (e.g. "Do you want to create one?")

## Silencing Translation Warnings

By default, the `polyglotI18nProvider` logs a warning in the console each time it is called with a message that can't be found in the current translations. This is a Polyglot feature that helps tracking missing translation messages.
Expand Down
2 changes: 2 additions & 0 deletions packages/ra-language-english/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ module.exports = {
loading: 'Loading',
not_found: 'Not Found',
show: '%{name} #%{id}',
empty: 'No %{name} yet.',
invite: 'Do you want to add one?',
},
input: {
file: {
Expand Down
2 changes: 2 additions & 0 deletions packages/ra-language-french/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ module.exports = {
loading: 'Chargement',
not_found: 'Page manquante',
show: '%{name} #%{id}',
empty: 'Pas encore de %{name}.',
invite: 'Voulez-vous en créer un ?',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause bad wording when the resource name is feminine (ex: promotion). But I don't see an easy way to fix that, so we'll go with that translation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could leave to user to override the empty text, by looking for a resources.foobar.empty key. If there's no such key we use the default one ra.page.empty.

The invite key could also be customizable, like resources.foobar.invite.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. Would you mind adding that feature to your PR?

Copy link
Contributor Author

@m4theushw m4theushw Dec 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll work on it

},
input: {
file: {
Expand Down
62 changes: 62 additions & 0 deletions packages/ra-ui-materialui/src/list/Empty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import Inbox from '@material-ui/icons/Inbox';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/styles';
import { useTranslate } from 'ra-core';
import { CreateButton } from '../button';
import inflection from 'inflection';

const useStyles = makeStyles({
message: {
textAlign: 'center',
opacity: 0.5,
margin: '0 1em',
},
icon: {
width: '9em',
height: '9em',
},
toolbar: {
textAlign: 'center',
marginTop: '2em',
},
});

const Empty = ({ resource, basePath }) => {
const classes = useStyles();
const translate = useTranslate();

const resourceName = inflection.humanize(
translate(`resources.${resource}.name`, {
smart_count: 0,
_: inflection.pluralize(resource),
}),
true
);

const emptyMessage = translate('ra.page.empty', { name: resourceName });
const inviteMessage = translate('ra.page.invite');

return (
<>
<div className={classes.message}>
<Inbox className={classes.icon} />
<Typography variant="h4" paragraph>
{translate(`resources.${resource}.empty`, {
_: emptyMessage,
})}
</Typography>
<Typography variant="body1">
{translate(`resources.${resource}.invite`, {
_: inviteMessage,
})}
</Typography>
</div>
<div className={classes.toolbar}>
<CreateButton variant="contained" basePath={basePath} />
</div>
</>
);
};

export default Empty;
93 changes: 59 additions & 34 deletions packages/ra-ui-materialui/src/list/List.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import DefaultPagination from './Pagination';
import BulkDeleteButton from '../button/BulkDeleteButton';
import BulkActionsToolbar from './BulkActionsToolbar';
import DefaultActions from './ListActions';
import Empty from './Empty';

/**
* List page component
Expand Down Expand Up @@ -115,53 +116,75 @@ export const ListView = props => {
component: Content,
exporter = defaultExporter,
title,
empty,
...rest
} = props;
useCheckMinimumRequiredProps('List', ['children'], props);
const classes = useStyles({ classes: classesOverride });
const { defaultTitle, version } = rest;
const {
defaultTitle,
version,
total,
loaded,
loading,
hasCreate,
filterValues,
} = rest;
const controllerProps = getListControllerProps(rest);

const renderList = () => (
<>
{(filters || actions) && (
<ListToolbar
filters={filters}
{...controllerProps}
actions={actions}
exporter={exporter} // deprecated, use ExporterContext instead
permanentFilter={filter}
/>
)}
<div className={classes.main}>
<Content
className={classnames(classes.content, {
[classes.bulkActionsDisplayed]:
controllerProps.selectedIds.length > 0,
})}
key={version}
>
{bulkActionButtons !== false && bulkActionButtons && (
<BulkActionsToolbar {...controllerProps}>
{bulkActionButtons}
</BulkActionsToolbar>
)}
{children &&
cloneElement(Children.only(children), {
...controllerProps,
hasBulkActions: bulkActionButtons !== false,
})}
{pagination && cloneElement(pagination, controllerProps)}
</Content>
{aside && cloneElement(aside, controllerProps)}
</div>
</>
);

const shouldRenderEmptyPage =
hasCreate &&
loaded &&
!loading &&
!total &&
!Object.keys(filterValues).length;

return (
<ExporterContext.Provider value={exporter}>
<div
className={classnames('list-page', classes.root, className)}
{...sanitizeRestProps(rest)}
>
<Title title={title} defaultTitle={defaultTitle} />

{(filters || actions) && (
<ListToolbar
filters={filters}
{...controllerProps}
actions={actions}
exporter={exporter} // deprecated, use ExporterContext instead
permanentFilter={filter}
/>
)}
<div className={classes.main}>
<Content
className={classnames(classes.content, {
[classes.bulkActionsDisplayed]:
controllerProps.selectedIds.length > 0,
})}
key={version}
>
{bulkActionButtons !== false && bulkActionButtons && (
<BulkActionsToolbar {...controllerProps}>
{bulkActionButtons}
</BulkActionsToolbar>
)}
{children &&
cloneElement(Children.only(children), {
...controllerProps,
hasBulkActions: bulkActionButtons !== false,
})}
{pagination &&
cloneElement(pagination, controllerProps)}
</Content>
{aside && cloneElement(aside, controllerProps)}
</div>
{shouldRenderEmptyPage
? cloneElement(empty, controllerProps)
: renderList()}
</div>
</ExporterContext.Provider>
);
Expand Down Expand Up @@ -218,6 +241,7 @@ ListView.defaultProps = {
component: Card,
bulkActionButtons: <DefaultBulkActionButtons />,
pagination: <DefaultPagination />,
empty: <Empty />,
};

export const useStyles = makeStyles(
Expand Down Expand Up @@ -304,6 +328,7 @@ const sanitizeRestProps = ({
toggleItem,
total,
version,
empty,
...rest
}) => rest;

Expand Down
30 changes: 30 additions & 0 deletions packages/ra-ui-materialui/src/list/List.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,34 @@ describe('<List />', () => {
);
expect(queryAllByText('Hello')).toHaveLength(1);
});

it('should render an invite when the list is empty', () => {
const Dummy = () => <div />;
const { queryAllByText } = renderWithRedux(
<ThemeProvider theme={theme}>
<ListView {...defaultProps} total={0} hasCreate loaded>
<Dummy />
</ListView>
</ThemeProvider>
);
expect(queryAllByText('resources.post.empty')).toHaveLength(1);
});

it('should not render an invite when a filter is active', () => {
const Dummy = () => <div />;
const { queryAllByText } = renderWithRedux(
<ThemeProvider theme={theme}>
<ListView
{...defaultProps}
filterValues={{ q: 'foo' }}
total={0}
hasCreate
loaded
>
<Dummy />
</ListView>
</ThemeProvider>
);
expect(queryAllByText('resources.post.empty')).toHaveLength(0);
});
});