mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 03:17:11 +00:00
Merge pull request #11195 from strapi/ctb/empty-states
[CTB] Add empty states
This commit is contained in:
commit
581b22af4c
@ -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",
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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': {
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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`}
|
||||
|
||||
@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user