Merge pull request #11195 from strapi/ctb/empty-states

[CTB] Add empty states
This commit is contained in:
cyril lopez 2021-10-06 09:36:53 +02:00 committed by GitHub
commit 581b22af4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 930 additions and 734 deletions

View File

@ -16,7 +16,6 @@
"@strapi/plugin-documentation": "3.6.8",
"@strapi/plugin-graphql": "3.6.8",
"@strapi/plugin-i18n": "3.6.8",
"@strapi/plugin-users-permissions": "3.6.8",
"@strapi/provider-email-mailgun": "3.6.8",
"@strapi/provider-upload-aws-s3": "3.6.8",
"@strapi/provider-upload-cloudinary": "3.6.8",

View File

@ -1,16 +1,17 @@
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import styled, { keyframes } from 'styled-components';
import { pxToRem } from '@strapi/helper-plugin';
import Time from '@strapi/icons/Time';
import Reload from '@strapi/icons/Reload';
import { Link } from '@strapi/parts/Link';
import { Box } from '@strapi/parts/Box';
import { Stack } from '@strapi/parts/Stack';
import { Row } from '@strapi/parts/Row';
import { H1, H2 } from '@strapi/parts/Text';
import PropTypes from 'prop-types';
import Overlay from './Overlay';
import { H1, Typography } from '@strapi/parts/Text';
import { Content, IconBox, Overlay } from './Overlay';
const overlayContainer = document.createElement('div');
const ID = 'autoReloadOverlayBlocker';
@ -27,11 +28,11 @@ const rotation = keyframes`
const LoaderReload = styled(Reload)`
animation: ${rotation} 1s infinite linear;
${({ small }) => small && `width: 25px; height: 25px;`}
`;
const Blocker = ({ displayedIcon, description, title, elapsed, isOpen }) => {
const Blocker = ({ displayedIcon, description, title, isOpen }) => {
const { formatMessage } = useIntl();
useEffect(() => {
document.body.appendChild(overlayContainer);
@ -43,38 +44,45 @@ const Blocker = ({ displayedIcon, description, title, elapsed, isOpen }) => {
if (isOpen) {
return ReactDOM.createPortal(
<Overlay>
<Box>
<Row>
{displayedIcon === 'reload' && (
<Box paddingRight={3} style={{ alignSelf: 'baseline' }}>
<LoaderReload width="4rem" height="4rem" />
</Box>
)}
{displayedIcon === 'time' && (
<Box paddingRight={3} style={{ alignSelf: 'center' }}>
<Time width="3.8rem" height="3.8rem" />
</Box>
)}
<Stack size={2}>
<Content size={6}>
<Stack size={2}>
<Row justifyContent="center">
<H1>{formatMessage(title)}</H1>
<H2 textColor="neutral600">{formatMessage(description)}</H2>
<Row>
{elapsed < 15 && (
<Link
href="https://strapi.io/documentation"
target="_blank"
onClick={e => {
e.preventDefault();
window.open('https://strapi.io/documentation', '_blank');
}}
>
Read the documentation
</Link>
)}
</Row>
</Stack>
</Row>
<Row justifyContent="center">
<Typography as="h2" textColor="neutral600" fontSize={4} fontWeight="regular">
{formatMessage(description)}
</Typography>
</Row>
</Stack>
<Row justifyContent="center">
{displayedIcon === 'reload' && (
<IconBox padding={6} background="primary100" borderColor="primary200">
<LoaderReload width={pxToRem(36)} height={pxToRem(36)} />
</IconBox>
)}
{displayedIcon === 'time' && (
<IconBox padding={6} background="primary100" borderColor="primary200">
<Time width={pxToRem(40)} height={pxToRem(40)} />
</IconBox>
)}
</Row>
</Box>
<Row justifyContent="center">
<Box paddingTop={2}>
<Link
href="https://strapi.io/documentation"
target="_blank"
rel="noopener noreferrer nofollow"
>
{formatMessage({
id: 'app.components.BlockLink.documentation',
defaultMessage: 'Read the documentation',
})}
</Link>
</Box>
</Row>
</Content>
</Overlay>,
overlayContainer
);
@ -86,7 +94,6 @@ const Blocker = ({ displayedIcon, description, title, elapsed, isOpen }) => {
Blocker.propTypes = {
displayedIcon: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired,
description: PropTypes.object.isRequired,
elapsed: PropTypes.number.isRequired,
isOpen: PropTypes.bool.isRequired,
title: PropTypes.object.isRequired,
};

View File

@ -1,7 +1,9 @@
import styled from 'styled-components';
import { Box } from '@strapi/parts/Box';
import { Stack } from '@strapi/parts/Stack';
import { pxToRem } from '@strapi/helper-plugin';
// TODO refactor with DS
const Overlay = styled.div`
const Overlay = styled(Box)`
position: fixed;
top: 0;
right: 0;
@ -15,17 +17,27 @@ const Overlay = styled.div`
right: 0;
bottom: 0;
left: 0;
background: ${({ theme }) => theme.colors.neutral200};
opacity: 0.8;
}
> div {
position: fixed;
top: 11.5rem;
left: 50%;
margin-left: -17.5rem;
z-index: 1100;
background: ${({ theme }) => theme.colors.neutral0};
opacity: 0.9;
}
`;
export default Overlay;
const Content = styled(Stack)`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding-top: ${pxToRem(160)};
`;
const IconBox = styled(Box)`
border-radius: 50%;
svg {
> path {
fill: ${({ theme }) => theme.colors.primary600} !important;
}
}
`;
export { Content, IconBox, Overlay };

View File

@ -1,8 +1,10 @@
import React, { useEffect, useRef, useState } from 'react';
import { AutoReloadOverlayBockerContext } from '@strapi/helper-plugin';
import PropTypes from 'prop-types';
import { AutoReloadOverlayBockerContext } from '@strapi/helper-plugin';
import Blocker from './Blocker';
const ELAPSED = 30;
const AutoReloadOverlayBlockerProvider = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [{ elapsed }, setState] = useState({ elapsed: 0, start: 0 });
@ -28,8 +30,7 @@ const AutoReloadOverlayBlockerProvider = ({ children }) => {
if (isOpen) {
timer = setInterval(() => {
// if (elapsed > 15) {
if (elapsed > 30) {
if (elapsed > ELAPSED) {
clearInterval(timer);
return null;
@ -60,7 +61,7 @@ const AutoReloadOverlayBlockerProvider = ({ children }) => {
defaultMessage: 'Waiting for restart',
};
if (elapsed > 15) {
if (elapsed > ELAPSED) {
displayedIcon = 'time';
description = {
@ -81,7 +82,6 @@ const AutoReloadOverlayBlockerProvider = ({ children }) => {
<Blocker
displayedIcon={displayedIcon}
isOpen={isOpen}
elapsed={elapsed}
description={description}
title={title}
/>

View File

@ -126,8 +126,6 @@ function ListView({
return;
}
console.log('iii');
console.error(err);
toggleNotification({
type: 'warning',
@ -355,6 +353,9 @@ export function mapDispatchToProps(dispatch) {
dispatch
);
}
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withConnect = connect(
mapStateToProps,
mapDispatchToProps
);
export default compose(withConnect)(memo(ListView, isEqual));

View File

@ -400,6 +400,10 @@ const DataManagerProvider = ({
const shouldRedirect = useMemo(() => {
const dataSet = isInContentTypeView ? contentTypes : components;
if (currentUid === 'create-content-type') {
return false;
}
return !Object.keys(dataSet).includes(currentUid) && !isLoading;
}, [components, contentTypes, currentUid, isInContentTypeView, isLoading]);
@ -408,7 +412,7 @@ const DataManagerProvider = ({
.filter(uid => get(contentTypes, [uid, 'schema', 'visible'], true))
.sort();
return get(allowedEndpoints, '0', '');
return get(allowedEndpoints, '0', 'create-content-type');
}, [contentTypes]);
if (shouldRedirect) {

View File

@ -5,7 +5,7 @@ const getModalTitleSubHeader = state => {
case 'chooseAttribute':
return getTrad(
`modalForm.sub-header.chooseAttribute.${
state.forTarget.includes('component') ? 'component' : state.kind
state.forTarget.includes('component') ? 'component' : state.kind || 'collectionType'
}`
);
case 'attribute': {

View File

@ -8,10 +8,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { useTracking } from '@strapi/helper-plugin';
import { EmptyBodyTable, useTracking } from '@strapi/helper-plugin';
import { Box } from '@strapi/parts/Box';
import { Button } from '@strapi/parts/Button';
import { TableLabel } from '@strapi/parts/Text';
import { TFooter } from '@strapi/parts/Table';
import { Table, Thead, Tr, Th, TFooter } from '@strapi/parts/Table';
import AddIcon from '@strapi/icons/AddIcon';
import { useIntl } from 'react-intl';
import useListView from '../../hooks/useListView';
@ -44,7 +45,8 @@ function List({
}) {
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const { isInDevelopmentMode, modifiedData } = useDataManager();
const { isInDevelopmentMode, modifiedData, isInContentTypeView } = useDataManager();
const { openModalAddField } = useListView();
const onClickAddField = () => {
trackUsage('hasClickedCTBAddFieldBanner');
@ -163,7 +165,74 @@ function List({
};
if (!targetUid) {
return null;
return (
<Table colCount={2} rowCount={2}>
<Thead>
<Tr>
<Th>
<TableLabel textColor="neutral600">
{formatMessage({ id: 'table.headers.name', defaultMessage: 'Name' })}
</TableLabel>
</Th>
<Th>
<TableLabel textColor="neutral600">
{formatMessage({ id: 'table.headers.type', defaultMessage: 'Type' })}
</TableLabel>
</Th>
</Tr>
</Thead>
<EmptyBodyTable
colSpan={2}
content={{
id: getTrad('table.content.create-first-content-type'),
defaultMessage: 'Create your first Collection-Type',
}}
/>
</Table>
);
}
if (items.length === 0 && isMain) {
return (
<Table colCount={2} rowCount={2}>
<Thead>
<Tr>
<Th>
<TableLabel textColor="neutral600">
{formatMessage({ id: 'table.headers.name', defaultMessage: 'Name' })}
</TableLabel>
</Th>
<Th>
<TableLabel textColor="neutral600">
{formatMessage({ id: 'table.headers.type', defaultMessage: 'Type' })}
</TableLabel>
</Th>
</Tr>
</Thead>
<EmptyBodyTable
action={
<Button onClick={onClickAddField} size="L" startIcon={<AddIcon />} variant="secondary">
{formatMessage({
id: getTrad('table.button.no-fields'),
defaultMessage: 'Add new field',
})}
</Button>
}
colSpan={2}
content={
isInContentTypeView
? {
id: getTrad('table.content.no-fields.collection-type'),
defaultMessage: 'Add your first field to this Collection-Type',
}
: {
id: getTrad('table.content.no-fields.component'),
defaultMessage: 'Add your first field to this component',
}
}
/>
</Table>
);
}
return (

View File

@ -26,6 +26,10 @@ const App = () => {
<Layout sideNav={<ContentTypeBuilderNav />}>
<Suspense fallback={<LoadingIndicatorPage />}>
<Switch>
<Route
path={`/plugins/${pluginId}/content-types/create-content-type`}
component={ListView}
/>
<Route path={`/plugins/${pluginId}/content-types/:uid`} component={ListView} />
<Route
path={`/plugins/${pluginId}/component-categories/:categoryUid`}

View File

@ -19,7 +19,7 @@ import has from 'lodash/has';
import isEqual from 'lodash/isEqual';
import upperFirst from 'lodash/upperFirst';
import { useIntl } from 'react-intl';
import { Prompt, useHistory, useLocation } from 'react-router-dom';
import { Prompt, useHistory, useLocation, useRouteMatch } from 'react-router-dom';
import List from '../../components/List';
import ListRow from '../../components/ListRow';
import ListViewContext from '../../contexts/ListViewContext';
@ -44,6 +44,7 @@ const ListView = () => {
const { push } = useHistory();
const { search } = useLocation();
const [enablePrompt, togglePrompt] = useState(true);
const match = useRouteMatch('/plugins/content-type-builder/:kind/:currentUID');
useEffect(() => {
if (search === '') {
@ -174,9 +175,18 @@ const ListView = () => {
return new Promise(resolve => setTimeout(resolve, 100));
};
const label = get(modifiedData, [firstMainDataPath, 'schema', 'name'], '');
let label = get(modifiedData, [firstMainDataPath, 'schema', 'name'], '');
const kind = get(modifiedData, [firstMainDataPath, 'schema', 'kind'], '');
const isCreatingFirstContentType = match?.params.currentUID === 'create-content-type';
if (!label && isCreatingFirstContentType) {
label = formatMessage({
id: getTrad('button.model.create'),
defaultMessage: 'Create new collection type',
});
}
// const listTitle = [
// formatMessage(
// {
@ -225,6 +235,24 @@ const ListView = () => {
primaryAction={
isInDevelopmentMode && (
<Stack horizontal size={2}>
{/* DON'T display the add field button when the content type has not been created */}
{!isCreatingFirstContentType && (
<Button
startIcon={<AddIcon />}
variant="secondary"
onClick={() => {
const headerDisplayObject = {
header_label_1: currentDataName,
header_icon_name_1:
forTarget === 'contentType' ? contentTypeKind : forTarget,
header_icon_isCustom_1: false,
};
handleClickAddField(forTarget, targetUid, headerDisplayObject);
}}
>
{formatMessage({ id: getTrad('button.attributes.add.another') })}
</Button>
)}
<Button
startIcon={<CheckIcon />}
onClick={() => submitData()}
@ -241,7 +269,8 @@ const ListView = () => {
}
secondaryAction={
isInDevelopmentMode &&
!isFromPlugin && (
!isFromPlugin &&
!isCreatingFirstContentType && (
<Button startIcon={<EditIcon />} variant="tertiary" onClick={onEdit}>
{formatMessage({
id: getTrad('app.utils.edit'),
@ -272,20 +301,6 @@ const ListView = () => {
isInContentTypeView={isInContentTypeView}
contentTypeKind={contentTypeKind}
/> */}
<Button
startIcon={<AddIcon />}
variant="primary"
onClick={() => {
const headerDisplayObject = {
header_label_1: currentDataName,
header_icon_name_1: forTarget === 'contentType' ? contentTypeKind : forTarget,
header_icon_isCustom_1: false,
};
handleClickAddField(forTarget, targetUid, headerDisplayObject);
}}
>
{formatMessage({ id: getTrad('button.attributes.add.another') })}
</Button>
</Stack>
</Row>
<Box background="neutral0" shadow="filterShadow" hasRadius>

View File

@ -183,6 +183,10 @@
"relation.oneWay": "has one",
"table.attributes.title.plural": "{number} fields",
"table.attributes.title.singular": "{number} field",
"table.content.no-fields.collection-type": "Add your first field to this Collection-Type",
"table.content.no-fields.component": "Add your first field to this component",
"table.content.create-first-content-type": "Create your first Collection-Type",
"table.button.no-fields": "Add new field",
"table.headers.name": "Name",
"table.headers.type": "Type"
}