-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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: | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
||||||||||
{% 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> | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd use |
||||||||||
<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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||||||||||
|
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't use |
||
|
||
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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} />; | ||
|
||
|
@@ -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 ( | ||
<div | ||
className={classnames('list-page', classes.root, className)} | ||
{...sanitizeRestProps(rest)} | ||
> | ||
<Title title={title} defaultTitle={defaultTitle} /> | ||
|
||
const renderChildren = () => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renderList? |
||
<> | ||
{(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 />, | ||
}; | ||
|
||
/** | ||
|
There was a problem hiding this comment.
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