Skip to content
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

[RFR] Add invite to create the first record when the list is empty #4065

Closed
wants to merge 4 commits into from
Closed
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
30 changes: 30 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)
* [`emptyState`](#empty-state)
Copy link
Member

Choose a reason for hiding this comment

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

I'd call it empty instead


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

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

Expand Down Expand Up @@ -683,6 +685,34 @@ const Aside = ({ data, ids }) => (
```
{% endraw %}

### Empty state

When there is no result, and there is no active filter, and the resource has a create page then a special page inviting the user to create the first record is displayed. Use the `emptyState` prop to modify that page, passing your custom component:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
When there is no result, and there is no active filter, and the resource has a create page then a special page inviting the user to create the first record is displayed. Use the `emptyState` prop to modify that page, passing your custom component:
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 `emptyState` prop to replace that page by a custom component:


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

const EmptyState = ({ basePath }) => (
<div style={{ textAlign: 'center', margin: '1em' }}>
<h1>No products available</h1>
Copy link
Member

Choose a reason for hiding this comment

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

I'd use resource in the example

<div>Create one or import from a file</div>
<CreateButton basePath={basePath} />
<Button onClick={...}>Import</Button>
</div>
);

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

The `emptyState` component receives the same props as the `aside` prop. Read the [section above](#aside-component) to check them.
Copy link
Member

Choose a reason for hiding this comment

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

no, please repeat them - developers don't read the docs sequentially.


### CSS API

The `List` 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/components/#overriding-styles-with-classes)). This property accepts the following keys:
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 ?',
},
input: {
file: {
Expand Down
48 changes: 48 additions & 0 deletions packages/ra-ui-materialui/src/list/EmptyState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import Inbox from '@material-ui/icons/Inbox';
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',
fontFamily: 'Roboto, sans-serif',
opacity: 0.5,
margin: '0 1em',
},
icon: {
width: '9em',
height: '9em',
},
toolbar: {
textAlign: 'center',
marginTop: '2em',
},
});

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

const resourceName = translate(`resources.${resource}.name`, {
smart_count: 0,
_: inflection.humanize(resource),
}).toLowerCase();
Copy link
Member

Choose a reason for hiding this comment

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

We don't use toLowerCase anywhere else on the naming, and I humanize should do the trick.


return (
<>
<div className={classes.message}>
<Inbox className={classes.icon} />
<h1>{translate('ra.page.empty', { name: resourceName })}</h1>
<div>{translate('ra.page.invite')}</div>
</div>
<div className={classes.toolbar}>
<CreateButton variant="contained" basePath={basePath} />
</div>
</>
);
};

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

const DefaultBulkActionButtons = props => <BulkDeleteButton {...props} />;

Expand Down Expand Up @@ -100,6 +101,7 @@ const sanitizeRestProps = ({
toggleItem,
total,
version,
emptyState,
...rest
}) => rest;

Expand All @@ -117,20 +119,24 @@ export const ListView = props => {
component: Content,
exporter,
title,
emptyState,
...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);

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

const renderChildren = () => (
Copy link
Member

Choose a reason for hiding this comment

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

renderList?

<>
{(filters || actions) && (
<ListToolbar
filters={filters}
Expand Down Expand Up @@ -162,6 +168,25 @@ export const ListView = props => {
</Content>
{aside && cloneElement(aside, controllerProps)}
</div>
</>
);

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

return (
<div
className={classnames('list-page', classes.root, className)}
{...sanitizeRestProps(rest)}
>
<Title title={title} defaultTitle={defaultTitle} />
{shouldRenderEmptyState
? cloneElement(emptyState, controllerProps)
: renderChildren()}
</div>
);
};
Expand Down Expand Up @@ -215,6 +240,7 @@ ListView.defaultProps = {
component: Card,
bulkActionButtons: <DefaultBulkActionButtons />,
pagination: <DefaultPagination />,
emptyState: <EmptyState />,
};

/**
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('ra.page.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('ra.page.empty')).toHaveLength(0);
});
});