Merge pull request #10934 from strapi/add-empty-state-components

Add empty state components
This commit is contained in:
cyril lopez 2021-09-13 06:25:25 +02:00 committed by GitHub
commit a42a3747f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 346 additions and 91 deletions

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { CustomContentLayout, useFocusWhenNavigate } from '@strapi/helper-plugin'; import { useFocusWhenNavigate, NoPermissions as NoPermissionsCompo } from '@strapi/helper-plugin';
import { Main } from '@strapi/parts/Main'; import { Main } from '@strapi/parts/Main';
import { HeaderLayout } from '@strapi/parts/Layout'; import { ContentLayout, HeaderLayout } from '@strapi/parts/Layout';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { getTrad } from '../../utils'; import { getTrad } from '../../utils';
@ -18,9 +18,9 @@ const NoPermissions = () => {
defaultMessage: 'Content', defaultMessage: 'Content',
})} })}
/> />
<CustomContentLayout canRead={false}> <ContentLayout>
<div /> <NoPermissionsCompo />
</CustomContentLayout> </ContentLayout>
</Main> </Main>
); );
}; };

View File

@ -6,7 +6,6 @@ import pick from 'lodash/pick';
import get from 'lodash/get'; import get from 'lodash/get';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { import {
CustomContentLayout,
Form, Form,
GenericInput, GenericInput,
SettingsPageTitle, SettingsPageTitle,
@ -15,13 +14,14 @@ import {
useFocusWhenNavigate, useFocusWhenNavigate,
useNotification, useNotification,
useOverlayBlocker, useOverlayBlocker,
LoadingIndicatorPage,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { Formik } from 'formik'; import { Formik } from 'formik';
import { Box } from '@strapi/parts/Box'; import { Box } from '@strapi/parts/Box';
import { Button } from '@strapi/parts/Button'; import { Button } from '@strapi/parts/Button';
import { Grid, GridItem } from '@strapi/parts/Grid'; import { Grid, GridItem } from '@strapi/parts/Grid';
import { HeaderLayout } from '@strapi/parts/Layout'; import { HeaderLayout, ContentLayout } from '@strapi/parts/Layout';
import { H3 } from '@strapi/parts/Text'; import { H3 } from '@strapi/parts/Text';
import { Main } from '@strapi/parts/Main'; import { Main } from '@strapi/parts/Main';
import { Stack } from '@strapi/parts/Stack'; import { Stack } from '@strapi/parts/Stack';
@ -130,7 +130,7 @@ const EditPage = ({ canUpdate }) => {
if (isLoading) { if (isLoading) {
return ( return (
<Main labelledBy="title"> <Main labelledBy="title" aria-busy="true">
<SettingsPageTitle name="Users" /> <SettingsPageTitle name="Users" />
<HeaderLayout <HeaderLayout
id="title" id="title"
@ -141,7 +141,9 @@ const EditPage = ({ canUpdate }) => {
} }
title={title} title={title}
/> />
<CustomContentLayout isLoading /> <ContentLayout>
<LoadingIndicatorPage />
</ContentLayout>
</Main> </Main>
); );
} }
@ -172,7 +174,7 @@ const EditPage = ({ canUpdate }) => {
} }
title={title} title={title}
/> />
<CustomContentLayout isLoading={isLoading}> <ContentLayout>
{data?.registrationToken && ( {data?.registrationToken && (
<Box paddingBottom={6}> <Box paddingBottom={6}>
<MagicLink registrationToken={data.registrationToken} /> <MagicLink registrationToken={data.registrationToken} />
@ -243,7 +245,7 @@ const EditPage = ({ canUpdate }) => {
</Stack> </Stack>
</Box> </Box>
</Stack> </Stack>
</CustomContentLayout> </ContentLayout>
</Form> </Form>
); );
}} }}

View File

@ -1,13 +1,13 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
CustomContentLayout,
Search, Search,
SettingsPageTitle, SettingsPageTitle,
useRBAC, useRBAC,
useNotification, useNotification,
useFocusWhenNavigate, useFocusWhenNavigate,
NoPermissions,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
import { Button, Box, HeaderLayout, Main, Row } from '@strapi/parts'; import { Button, Box, HeaderLayout, Main, Row, ContentLayout } from '@strapi/parts';
import { Mail } from '@strapi/icons'; import { Mail } from '@strapi/icons';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@ -85,7 +85,7 @@ const ListPage = () => {
); );
return ( return (
<Main labelledBy="title"> <Main labelledBy="title" aria-busy={isLoading}>
<SettingsPageTitle name="Users" /> <SettingsPageTitle name="Users" />
<HeaderLayout <HeaderLayout
id="title" id="title"
@ -102,7 +102,8 @@ const ListPage = () => {
{ number: total } { number: total }
)} )}
/> />
<CustomContentLayout canRead={canRead}> <ContentLayout canRead={canRead}>
{!canRead && <NoPermissions />}
{status === 'error' && <div>TODO: An error occurred</div>} {status === 'error' && <div>TODO: An error occurred</div>}
{canRead && ( {canRead && (
<> <>
@ -129,7 +130,7 @@ const ListPage = () => {
<PaginationFooter pagination={data?.pagination} /> <PaginationFooter pagination={data?.pagination} />
</> </>
)} )}
</CustomContentLayout> </ContentLayout>
{isModalOpened && <ModalForm onToggle={handleToggle} queryName={queryName} />} {isModalOpened && <ModalForm onToggle={handleToggle} queryName={queryName} />}
</Main> </Main>
); );

View File

@ -121,6 +121,11 @@ describe('ADMIN | Pages | USERS | ListPage', () => {
padding-right: 8px; padding-right: 8px;
} }
.c13 {
padding-right: 56px;
padding-left: 56px;
}
.c14 { .c14 {
padding-bottom: 16px; padding-bottom: 16px;
} }
@ -748,11 +753,6 @@ describe('ADMIN | Pages | USERS | ListPage', () => {
fill: #666687; fill: #666687;
} }
.c13 {
padding-right: 56px;
padding-left: 56px;
}
.c36 tr:last-of-type { .c36 tr:last-of-type {
border-bottom: none; border-bottom: none;
} }
@ -789,6 +789,7 @@ describe('ADMIN | Pages | USERS | ListPage', () => {
} }
<main <main
aria-busy="true"
aria-labelledby="title" aria-labelledby="title"
class="c0" class="c0"
id="main-content" id="main-content"
@ -861,7 +862,7 @@ describe('ADMIN | Pages | USERS | ListPage', () => {
</div> </div>
</div> </div>
<div <div
class="c13" class="c1 c13"
> >
<div <div
class="c1 c14" class="c1 c14"

View File

@ -1,9 +1,10 @@
import React from 'react'; import React, { useEffect } from 'react';
import { ContentLayout } from '@strapi/parts/Layout'; import { ContentLayout } from '@strapi/parts/Layout';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import EmptyStateLayout from '../EmptyStateLayout'; import EmptyStateLayout from '../EmptyStateLayout';
import LoadingIndicatorPage from '../LoadingIndicatorPage'; import LoadingIndicatorPage from '../LoadingIndicatorPage';
// TODO: REMOVE this component
const CustomContentLayout = ({ const CustomContentLayout = ({
action, action,
canRead, canRead,
@ -12,6 +13,12 @@ const CustomContentLayout = ({
shouldShowEmptyState, shouldShowEmptyState,
...rest ...rest
}) => { }) => {
useEffect(() => {
console.error(
'This component will soon be removed, please check out the PageTemplate in the storybook'
);
}, []);
if (!canRead) { if (!canRead) {
return ( return (
<ContentLayout> <ContentLayout>

View File

@ -0,0 +1,31 @@
<!--- EmptyStateLayout.stories.mdx --->
import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs';
import { Main, Row, Button } from '@strapi/parts';
import NoContent from './index';
<Meta title="components/NoContent" />
# NoContent
This component is used to display an empty state.
## Usage
<Canvas>
<Story name="base">
<Main>
<NoContent
content={{
id: 'app.components.EmptyStateLayout.content-document',
defaultMessage: "There's no content",
}}
action={<Button>Add content</Button>}
/>
</Main>
</Story>
</Canvas>
### Props
<ArgsTable of={NoContent} />

View File

@ -0,0 +1,38 @@
import React from 'react';
import EmptyStateDocument from '@strapi/icons/EmptyStateDocument';
import { EmptyStateLayout } from '@strapi/parts/EmptyStateLayout';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
const NoContent = ({ content, ...rest }) => {
const { formatMessage } = useIntl();
return (
<EmptyStateLayout
icon={<EmptyStateDocument width="10rem" />}
{...rest}
content={formatMessage(
{ id: content.id, defaultMessage: content.defaultMessage },
content.values
)}
/>
);
};
NoContent.defaultProps = {
content: {
id: 'app.components.EmptyStateLayout.content-document',
defaultMessage: "You don't have any content yet...",
values: {},
},
};
NoContent.propTypes = {
content: PropTypes.shape({
id: PropTypes.string,
defaultMessage: PropTypes.string,
values: PropTypes.object,
}),
};
export default NoContent;

View File

@ -0,0 +1,25 @@
<!--- EmptyStateLayout.stories.mdx --->
import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs';
import { Main, Row, Button } from '@strapi/parts';
import NoMedia from './index';
<Meta title="components/NoMedia" />
# NoMedia
This component is used to display an empty state.
## Usage
<Canvas>
<Story name="base">
<Main>
<NoMedia content="There's no media" action={<Button>Add a media</Button>} />
</Main>
</Story>
</Canvas>
### Props
<ArgsTable of={NoMedia} />

View File

@ -0,0 +1,9 @@
import React from 'react';
import EmptyStatePicture from '@strapi/icons/EmptyStatePicture';
import { EmptyStateLayout } from '@strapi/parts/EmptyStateLayout';
const NoMedia = props => {
return <EmptyStateLayout icon={<EmptyStatePicture width="10rem" />} {...props} />;
};
export default NoMedia;

View File

@ -0,0 +1,25 @@
<!--- EmptyStateLayout.stories.mdx --->
import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs';
import { Main, Row } from '@strapi/parts';
import NoPermissions from './index';
<Meta title="components/NoPermissions" />
# NoPermissions
This component is used to display an empty state.
## Usage
<Canvas>
<Story name="base">
<Main>
<NoPermissions />
</Main>
</Story>
</Canvas>
### Props
<ArgsTable of={NoPermissions} />

View File

@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import EmptyStatePermissions from '@strapi/icons/EmptyStatePermissions';
import { EmptyStateLayout } from '@strapi/parts/EmptyStateLayout';
const NoPermissions = ({ action }) => {
const { formatMessage } = useIntl();
return (
<EmptyStateLayout
icon={<EmptyStatePermissions width="10rem" />}
content={formatMessage({
id: 'app.components.EmptyStateLayout.content-permissions',
defaultMessage: "You don't have the permissions to access that content",
})}
action={action}
/>
);
};
NoPermissions.defaultProps = {
action: undefined,
};
NoPermissions.propTypes = {
action: PropTypes.node,
};
export default NoPermissions;

View File

@ -183,6 +183,9 @@ export { default as ConfirmDialog } from './components/ConfirmDialog';
export { default as ContentBox } from './components/ContentBox'; export { default as ContentBox } from './components/ContentBox';
export { default as CustomContentLayout } from './components/CustomContentLayout'; export { default as CustomContentLayout } from './components/CustomContentLayout';
export { default as EmptyStateLayout } from './components/EmptyStateLayout'; export { default as EmptyStateLayout } from './components/EmptyStateLayout';
export { default as NoContent } from './components/NoContent';
export { default as NoMedia } from './components/NoMedia';
export { default as NoPermissions } from './components/NoPermissions';
export { default as EmptyBodyTable } from './components/EmptyBodyTable'; export { default as EmptyBodyTable } from './components/EmptyBodyTable';
export { default as GenericInput } from './components/GenericInput'; export { default as GenericInput } from './components/GenericInput';
export * from './components/InjectionZone'; export * from './components/InjectionZone';

View File

@ -0,0 +1,74 @@
<!--- EmptyStateLayout.stories.mdx --->
import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs';
import { Main, Layout, HeaderLayout, Button, ActionLayout, ContentLayout,Box } from '@strapi/parts';
import AddIcon from '@strapi/icons/AddIcon'
import EditIcon from '@strapi/icons/EditIcon'
import LoadingIndicatorPage from '../../components/LoadingIndicatorPage';
import NoContent from '../../components/NoContent'
import NoPermissions from '../../components/NoPermissions'
<Meta title="templates/PageTemplate" />
# PageTemplate
This component is used to display an empty state.
## Imports
```js
import { Main } from '@strapi/parts/Main';
import { ActionLayout, ContentLayout, HeaderLayout, Layout } from '@strapi/parts/Layout';
import { Button } from '@strapi/parts/Button';
import { Box } from '@strapi/parts/Box';
import { LoadingIndicatorPage, NoContent, NoPermissions } from '@strapi/helper-plugin';
```
## Usage
<Canvas>
<Story name="base">
{() => {
const canRead = false;
const isLoading = true;
const data = [];
return (
<Box background="neutral100">
<Layout>
<Main labelledBy="title" aria-busy={isLoading}>
<HeaderLayout
id="title"
primaryAction={<Button startIcon={<AddIcon />}>Add an entry</Button>}
secondaryAction={
<Button variant="tertiary" startIcon={<EditIcon />}>
Edit
</Button>
}
title="Other CT"
subtitle="36 entries found"
/>
<ActionLayout
startActions={
<>
<Button variant="tertiary">Search</Button>
<Button variant="tertiary">Filter</Button>
</>
}
endActions={
<>
<Button variant="tertiary">Settings</Button>
</>
}
/>
<ContentLayout>
{!canRead && <NoPermissions />}
{(canRead && data && data.length === 0) && <NoContent content="No content available" action={<Button>Add content</Button>}/>}
{isLoading && <LoadingIndicatorPage />}
</ContentLayout>
</Main>
</Layout>
</Box>
);
}}
</Story>
</Canvas>

View File

@ -14,6 +14,7 @@ import {
Th, Th,
TableLabel, TableLabel,
useNotifyAT, useNotifyAT,
ContentLayout,
} from '@strapi/parts'; } from '@strapi/parts';
import { AddIcon, EditIcon } from '@strapi/icons'; import { AddIcon, EditIcon } from '@strapi/icons';
@ -23,8 +24,9 @@ import {
SettingsPageTitle, SettingsPageTitle,
CheckPermissions, CheckPermissions,
useNotification, useNotification,
CustomContentLayout,
useRBAC, useRBAC,
NoPermissions,
LoadingIndicatorPage,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
@ -59,7 +61,10 @@ const RoleListPage = () => {
isLoading: isLoadingForData, isLoading: isLoadingForData,
data: { roles }, data: { roles },
isFetching, isFetching,
} = useQuery('get-roles', () => fetchData(toggleNotification, notifyStatus), { initialData: {} }); } = useQuery('get-roles', () => fetchData(toggleNotification, notifyStatus), {
initialData: {},
enabled: canRead,
});
const isLoading = isLoadingForData || isFetching; const isLoading = isLoadingForData || isFetching;
@ -73,7 +78,7 @@ const RoleListPage = () => {
defaultMessage: 'Roles', defaultMessage: 'Roles',
}); });
const handleClickEdit = (id) => { const handleClickEdit = id => {
push(`/settings/${pluginId}/roles/${id}`); push(`/settings/${pluginId}/roles/${id}`);
}; };
@ -104,70 +109,74 @@ const RoleListPage = () => {
} }
/> />
<CustomContentLayout <ContentLayout
canRead={canRead} canRead={canRead}
shouldShowEmptyState={roles && !roles.length} shouldShowEmptyState={roles && !roles.length}
isLoading={isLoading || isLoadingForPermissions} isLoading={isLoading || isLoadingForPermissions}
> >
<Table colCount={4} rowCount={roles && roles.length + 1}> {!canRead && <NoPermissions />}
<Thead> {(isLoading || isLoadingForPermissions) && <LoadingIndicatorPage />}
<Tr> {canRead && roles && roles.length && (
<Th> <Table colCount={4} rowCount={roles && roles.length + 1}>
<TableLabel> <Thead>
{formatMessage({ id: getTrad('Roles.name'), defaultMessage: 'Name' })} <Tr>
</TableLabel> <Th>
</Th> <TableLabel>
<Th> {formatMessage({ id: getTrad('Roles.name'), defaultMessage: 'Name' })}
<TableLabel> </TableLabel>
{formatMessage({ </Th>
id: getTrad('Roles.description'), <Th>
defaultMessage: 'Description', <TableLabel>
})} {formatMessage({
</TableLabel> id: getTrad('Roles.description'),
</Th> defaultMessage: 'Description',
<Th> })}
<TableLabel> </TableLabel>
{formatMessage({ </Th>
id: getTrad('Roles.users'), <Th>
defaultMessage: 'Users', <TableLabel>
})} {formatMessage({
</TableLabel> id: getTrad('Roles.users'),
</Th> defaultMessage: 'Users',
</Tr> })}
</Thead> </TableLabel>
<Tbody> </Th>
{roles && </Tr>
roles.map((role) => ( </Thead>
<Tr key={role.name}> <Tbody>
<Td width="20%"> {roles &&
<Text>{role.name}</Text> roles.map(role => (
</Td> <Tr key={role.name}>
<Td width="50%"> <Td width="20%">
<Text>{role.description}</Text> <Text>{role.name}</Text>
</Td> </Td>
<Td width="30%"> <Td width="50%">
<Text> <Text>{role.description}</Text>
{`${role.nb_users} ${formatMessage({ </Td>
id: getTrad('Roles.users'), <Td width="30%">
defaultMessage: 'users', <Text>
}).toLowerCase()}`} {`${role.nb_users} ${formatMessage({
</Text> id: getTrad('Roles.users'),
</Td> defaultMessage: 'users',
<Td> }).toLowerCase()}`}
<CheckPermissions permissions={permissions.updateRole}> </Text>
<IconButton </Td>
onClick={() => handleClickEdit(role.id)} <Td>
noBorder <CheckPermissions permissions={permissions.updateRole}>
icon={<EditIcon />} <IconButton
label="Edit" onClick={() => handleClickEdit(role.id)}
/> noBorder
</CheckPermissions> icon={<EditIcon />}
</Td> label="Edit"
</Tr> />
))} </CheckPermissions>
</Tbody> </Td>
</Table> </Tr>
</CustomContentLayout> ))}
</Tbody>
</Table>
)}
</ContentLayout>
</Main> </Main>
</Layout> </Layout>
); );

View File

@ -91,6 +91,11 @@ describe('Admin | containers | RoleListPage', () => {
padding-left: 56px; padding-left: 56px;
} }
.c10 {
padding-right: 56px;
padding-left: 56px;
}
.c5 { .c5 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
@ -136,11 +141,6 @@ describe('Admin | containers | RoleListPage', () => {
outline: none; outline: none;
} }
.c10 {
padding-right: 56px;
padding-left: 56px;
}
.c13 { .c13 {
border: 0; border: 0;
-webkit-clip: rect(0 0 0 0); -webkit-clip: rect(0 0 0 0);