From abc789e9c5c394dec97afa56f7cc7450c3a0902d Mon Sep 17 00:00:00 2001 From: Matheus Wichman Date: Wed, 27 Nov 2019 13:45:15 -0300 Subject: [PATCH 1/4] Improve empty state page --- packages/ra-language-english/index.js | 2 + .../ra-ui-materialui/src/list/EmptyState.js | 48 +++++++++++++++++++ packages/ra-ui-materialui/src/list/List.js | 42 ++++++++++++---- 3 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 packages/ra-ui-materialui/src/list/EmptyState.js diff --git a/packages/ra-language-english/index.js b/packages/ra-language-english/index.js index b9673c3228e..18205a45d36 100644 --- a/packages/ra-language-english/index.js +++ b/packages/ra-language-english/index.js @@ -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: { diff --git a/packages/ra-ui-materialui/src/list/EmptyState.js b/packages/ra-ui-materialui/src/list/EmptyState.js new file mode 100644 index 00000000000..47ff7dbf5ab --- /dev/null +++ b/packages/ra-ui-materialui/src/list/EmptyState.js @@ -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(); + + return ( + <> +
+ +

{translate('ra.page.empty', { name: resourceName })}

+
{translate('ra.page.invite')}
+
+
+ +
+ + ); +}; + +export default EmptyState; diff --git a/packages/ra-ui-materialui/src/list/List.js b/packages/ra-ui-materialui/src/list/List.js index 01a28c4e2e6..124d3f69eac 100644 --- a/packages/ra-ui-materialui/src/list/List.js +++ b/packages/ra-ui-materialui/src/list/List.js @@ -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 => ; @@ -100,6 +101,7 @@ const sanitizeRestProps = ({ toggleItem, total, version, + emptyState, ...rest }) => rest; @@ -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 ( -
- - + const renderChildren = () => ( + <> {(filters || actions) && ( <ListToolbar filters={filters} @@ -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> ); }; @@ -215,6 +240,7 @@ ListView.defaultProps = { component: Card, bulkActionButtons: <DefaultBulkActionButtons />, pagination: <DefaultPagination />, + emptyState: <EmptyState />, }; /** From 1d7afa40ccc3b48d49ec14c67f2f75be833d3ad2 Mon Sep 17 00:00:00 2001 From: Matheus Wichman <matheushw@outlook.com> Date: Fri, 29 Nov 2019 09:38:26 -0300 Subject: [PATCH 2/4] Add tests --- .../ra-ui-materialui/src/list/List.spec.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/ra-ui-materialui/src/list/List.spec.js b/packages/ra-ui-materialui/src/list/List.spec.js index c695ae2fe3a..8cd2217a13f 100644 --- a/packages/ra-ui-materialui/src/list/List.spec.js +++ b/packages/ra-ui-materialui/src/list/List.spec.js @@ -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); + }); }); From 264424aa14946d326636116df1c11aca094598ef Mon Sep 17 00:00:00 2001 From: Matheus Wichman <matheushw@outlook.com> Date: Fri, 29 Nov 2019 10:27:09 -0300 Subject: [PATCH 3/4] Add section about the emptyState prop --- docs/List.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/List.md b/docs/List.md index f993decbb5d..7b50a4eef18 100644 --- a/docs/List.md +++ b/docs/List.md @@ -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) Here is the minimal code necessary to display a list of posts: @@ -650,6 +651,7 @@ const PostList = props => ( <List aside={<Aside />} {...props}> ... </List> +); ``` {% endraw %} @@ -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: + +{% 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> + <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. + ### 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: From 0e63a837c558a6831696db94478afa347fd8a4de Mon Sep 17 00:00:00 2001 From: Matheus Wichman <matheushw@outlook.com> Date: Fri, 29 Nov 2019 12:16:16 -0300 Subject: [PATCH 4/4] Update french translations --- packages/ra-language-french/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ra-language-french/index.js b/packages/ra-language-french/index.js index 3c05bf4c610..1abe8042972 100644 --- a/packages/ra-language-french/index.js +++ b/packages/ra-language-french/index.js @@ -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: {