Add component picker

Signed-off-by: soupette <cyril@strapi.io>
This commit is contained in:
soupette 2021-09-24 08:40:37 +02:00
parent fa479708d7
commit ddffdf947d
12 changed files with 221 additions and 220 deletions

View File

@ -49,7 +49,9 @@
"default.dish",
"default.openingtimes",
"default.restaurantservice",
"default.temp"
"default.temp",
"default.apple",
"default.car"
]
}
}

View File

@ -0,0 +1,13 @@
{
"collectionName": "components_default_apples",
"info": {
"name": "apple",
"icon": "apple-alt"
},
"options": {},
"attributes": {
"name": {
"type": "string"
}
}
}

View File

@ -0,0 +1,13 @@
{
"collectionName": "components_default_cars",
"info": {
"name": "car",
"icon": "align-right"
},
"options": {},
"attributes": {
"name": {
"type": "string"
}
}
}

View File

@ -0,0 +1,90 @@
/**
*
* ComponentCard
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Text, Stack, Box } from '@strapi/parts';
import { pxToRem } from '@strapi/helper-plugin';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import styled from 'styled-components';
import { useIntl } from 'react-intl';
const StyledFontAwesomeIcon = styled(FontAwesomeIcon)`
width: ${pxToRem(32)} !important;
height: ${pxToRem(32)} !important;
padding: ${pxToRem(9)};
border-radius: ${pxToRem(64)};
background: ${({ theme }) => theme.colors.neutral150};
path {
fill: ${({ theme }) => theme.colors.neutral500};
}
`;
const ComponentBox = styled(Box)`
flex-shrink: 0;
width: ${pxToRem(140)};
height: ${pxToRem(84)};
border: 1px solid ${({ theme }) => theme.colors.neutral200};
background: ${({ theme }) => theme.colors.neutral100};
border-radius: ${({ theme }) => theme.borderRadius};
display: flex;
justify-content: center;
align-items: center;
&.active,
&:hover {
border: 1px solid ${({ theme }) => theme.colors.primary200};
background: ${({ theme }) => theme.colors.primary100};
${StyledFontAwesomeIcon} {
background: ${({ theme }) => theme.colors.primary200};
path {
fill: ${({ theme }) => theme.colors.primary600};
}
}
${Text} {
color: ${({ theme }) => theme.colors.primary600};
}
}
`;
function ComponentCard({ componentUid, intlLabel, icon, onClick }) {
const { formatMessage } = useIntl();
const handleClick = () => {
onClick(componentUid);
};
return (
<button type="button" onClick={handleClick}>
<ComponentBox borderRadius="borderRadius">
<Stack size={1} style={{ justifyContent: 'center', alignItems: 'center' }}>
<StyledFontAwesomeIcon icon={icon} />
<Text small bold textColor="neutral600">
{formatMessage(intlLabel)}
</Text>
</Stack>
</ComponentBox>
</button>
);
}
ComponentCard.defaultProps = {
icon: 'smile',
onClick: () => {},
};
ComponentCard.propTypes = {
componentUid: PropTypes.string.isRequired,
intlLabel: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
}).isRequired,
icon: PropTypes.string,
onClick: PropTypes.func,
};
export default ComponentCard;

View File

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Accordion, AccordionToggle, AccordionContent } from '@strapi/parts/Accordion';
import { Box } from '@strapi/parts/Box';
import { Grid, GridItem } from '@strapi/parts/Grid';
import ComponentCard from './ComponentCard';
const Category = ({ category, components, isOdd, isOpen, onAddComponent, onToggle }) => {
const handleToggle = () => {
onToggle(category);
};
return (
<Accordion expanded={isOpen} toggle={handleToggle}>
<AccordionToggle
variant={isOdd ? 'primary' : 'secondary'}
title={category}
togglePosition="left"
/>
<AccordionContent>
<Box paddingTop={4} paddingBottom={4}>
<Grid gap={2}>
{components.map(({ componentUid, info: { label, icon, name } }) => {
return (
<GridItem col={2} key={componentUid}>
<ComponentCard
componentUid={componentUid}
intlLabel={{ id: label || name, defaultMessage: label || name }}
icon={icon}
onClick={onAddComponent}
/>
</GridItem>
);
})}
</Grid>
</Box>
</AccordionContent>
</Accordion>
);
};
Category.propTypes = {
category: PropTypes.string.isRequired,
components: PropTypes.array.isRequired,
isOdd: PropTypes.bool.isRequired,
isOpen: PropTypes.bool.isRequired,
onAddComponent: PropTypes.func.isRequired,
onToggle: PropTypes.func.isRequired,
};
export default Category;

View File

@ -1,14 +1,17 @@
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { groupBy } from 'lodash';
import groupBy from 'lodash/groupBy';
import PropTypes from 'prop-types';
import { Collapse } from 'reactstrap';
import { FormattedMessage } from 'react-intl';
import { useIntl } from 'react-intl';
import { KeyboardNavigable } from '@strapi/parts/KeyboardNavigable';
import { Box } from '@strapi/parts/Box';
import { Row } from '@strapi/parts/Row';
import { Text } from '@strapi/parts/Text';
import { getTrad } from '../../../../utils';
import { useContentTypeLayout } from '../../../../hooks';
import Category from './Category';
import Wrapper from './Wrapper';
const Picker = ({ components, isOpen, onClickAddComponent }) => {
const ComponentPicker = ({ components, isOpen, onClickAddComponent }) => {
const { formatMessage } = useIntl();
const { getComponentLayout } = useContentTypeLayout();
const [categoryToOpen, setCategoryToOpen] = useState('');
@ -51,38 +54,58 @@ const Picker = ({ components, isOpen, onClickAddComponent }) => {
[categoryToOpen]
);
if (!isOpen) {
return null;
}
return (
<Collapse isOpen={isOpen}>
<Wrapper>
<div>
<p className="componentPickerTitle">
<FormattedMessage id={getTrad('components.DynamicZone.pick-compo')} />
</p>
<div className="categoriesList">
<Box paddingBottom={6}>
<Box
paddingTop={6}
paddingBottom={6}
paddingLeft={5}
paddingRight={5}
background="neutral0"
shadox="tableShadow"
borderColor="neutral150"
hasRadius
>
<Row justifyContent="center">
<Text bold textColor="neutral600">
{formatMessage({
id: getTrad('components.DynamicZone.ComponentPicker-label'),
defaultMessage: 'Pick one component',
})}
</Text>
</Row>
<Box paddingTop={2}>
<KeyboardNavigable attributeName="data-strapi-accordion-toggle">
{dynamicComponentCategories.map(({ category, components }, index) => {
return (
<Category
key={category}
category={category}
components={components}
isOdd={index % 2 === 1}
isOpen={category === categoryToOpen}
isFirst={index === 0}
// TODO?
// isFirst={index === 0}
onAddComponent={handleAddComponentToDz}
onToggle={handleClickToggle}
/>
);
})}
</div>
</div>
</Wrapper>
</Collapse>
</KeyboardNavigable>
</Box>
</Box>
</Box>
);
};
Picker.propTypes = {
ComponentPicker.propTypes = {
components: PropTypes.array.isRequired,
isOpen: PropTypes.bool.isRequired,
onClickAddComponent: PropTypes.func.isRequired,
};
export default memo(Picker);
export default memo(ComponentPicker);

View File

@ -1,78 +0,0 @@
import styled from 'styled-components';
/* eslint-disable */
const BannerWrapper = styled.button`
display: flex;
height: 36px;
width: 100%;
padding: 0 15px;
border-bottom: 0;
border: 1px solid rgba(227, 233, 243, 0.75);
background-color: ${({ theme }) => theme.main.colors.white};
font-size: ${({ theme }) => theme.main.sizes.fonts.md};
font-weight: ${({ theme }) => theme.main.fontWeights.semiBold};
cursor: pointer;
&:focus {
outline: 0;
}
.img-wrapper {
width: 19px;
height: 19px;
margin-right: 19px;
border-radius: 50%;
background-color: ${({ theme }) => theme.main.colors.mediumGrey};
text-align: center;
}
.label {
text-transform: capitalize;
}
svg {
path {
fill: ${({ theme }) => theme.main.colors.leftMenu['link-color']} !important;
}
}
-webkit-font-smoothing: antialiased;
> div {
align-self: center;
margin-top: -2px;
}
${({ isFirst, theme }) => {
if (isFirst) {
return `
border-top-right-radius: ${theme.main.sizes.borderRadius};
border-top-left-radius: ${theme.main.sizes.borderRadius};
`;
}
}}
${({ isOpen, theme }) => {
if (isOpen) {
return `
border: 1px solid ${theme.main.colors.darkBlue};
background-color: ${theme.main.colors.lightBlue};
color: ${theme.main.colors.mediumBlue};
font-weight: ${theme.main.fontWeights.bold};
.img-wrapper {
background-color: ${theme.main.colors.darkBlue};
transform: rotate(180deg);
}
svg {
path {
fill: ${theme.main.colors.mediumBlue} !important;
}
}
`;
}
}}
`;
export default BannerWrapper;

View File

@ -1,33 +0,0 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React from 'react';
import PropTypes from 'prop-types';
import { Carret } from '@buffetjs/icons';
import Wrapper from './Wrapper';
/* eslint-disable jsx-a11y/no-static-element-interactions */
const Banner = ({ category, isOpen, onToggle, isFirst }) => {
const handleClick = () => {
onToggle(category);
};
return (
<Wrapper type="button" isFirst={isFirst} isOpen={isOpen} onClick={handleClick}>
<div className="img-wrapper">
<Carret />
</div>
<div className="label">{category}</div>
</Wrapper>
);
};
Banner.propTypes = {
category: PropTypes.string.isRequired,
isFirst: PropTypes.bool.isRequired,
isOpen: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired,
};
Banner.displayName = 'Banner';
export default Banner;

View File

@ -1,53 +0,0 @@
import React, { memo, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Collapse } from 'reactstrap';
import DynamicComponentCard from '../../../../DynamicComponentCard';
import Banner from './Banner';
const Category = ({ category, components, isFirst, isOpen, onAddComponent, onToggle }) => {
const [showComponents, setShowComponents] = useState(false);
useEffect(() => {
if (isOpen) {
setShowComponents(true);
}
}, [isOpen]);
const handleExited = () => setShowComponents(false);
return (
<>
<Banner isFirst={isFirst} isOpen={isOpen} category={category} onToggle={onToggle} />
<Collapse isOpen={isOpen} onExited={handleExited}>
{showComponents && (
<div className="componentsList">
{components.map(({ componentUid, info: { name, icon } }) => {
return (
<DynamicComponentCard
key={componentUid}
componentUid={componentUid}
friendlyName={name}
icon={icon}
onClick={() => {
onAddComponent(componentUid);
}}
/>
);
})}
</div>
)}
</Collapse>
</>
);
};
Category.propTypes = {
category: PropTypes.string.isRequired,
components: PropTypes.array.isRequired,
isFirst: PropTypes.bool.isRequired,
isOpen: PropTypes.bool.isRequired,
onAddComponent: PropTypes.func.isRequired,
onToggle: PropTypes.func.isRequired,
};
export default memo(Category);

View File

@ -1,32 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
overflow: hidden;
> div {
margin-top: 15px;
padding: 23px 18px 21px 18px;
background-color: #f2f3f4;
}
.componentPickerTitle {
margin-bottom: 10px;
color: #919bae;
font-weight: 600;
font-size: 13px;
line-height: normal;
}
.componentsList {
display: flex;
flex-wrap: wrap;
padding-top: 10px;
padding-left: 15px;
padding-right: 15px;
}
.categoriesList {
padding-bottom: 4px;
}
`;
export default Wrapper;

View File

@ -4,7 +4,6 @@ import isEqual from 'react-fast-compare';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Box } from '@strapi/parts/Box';
import { Stack } from '@strapi/parts/Stack';
import { Flex } from '@buffetjs/core';
import { LabelIconWrapper, NotAllowedInput, useNotification } from '@strapi/helper-plugin';
import { getTrad } from '../../utils';
@ -19,7 +18,7 @@ import ComponentWrapper from './ComponentWrapper';
import DynamicZoneWrapper from './DynamicZoneWrapper';
import Label from './Label';
import Wrapper from './Wrapper';
import Picker from './components/Picker';
import ComponentPicker from './components/ComponentPicker';
/* eslint-disable react/no-array-index-key */
@ -131,6 +130,11 @@ const DynamicZone = ({
name={name}
onClick={handleClickOpenPicker}
/>
<ComponentPicker
isOpen={isOpen}
components={dynamicZoneAvailableComponents}
onClickAddComponent={handleAddComponent}
/>
</>
)}
</Box>
@ -215,7 +219,7 @@ const DynamicZone = ({
values={{ componentName: metadatas.label }}
/>
</div>
<Picker
<ComponentPicker
isOpen={isOpen}
components={dynamicZoneAvailableComponents}
onClickAddComponent={handleAddComponent}

View File

@ -630,5 +630,6 @@
"clearLabel": "Clear",
"submit": "Submit",
"anErrorOccurred": "Woops! Something went wrong. Please, try again.",
"app.utils.close-label": "Close"
"app.utils.close-label": "Close",
"content-manager.components.DynamicZone.ComponentPicker-label": "Pick one component"
}