mirror of
https://github.com/strapi/strapi.git
synced 2025-08-24 16:49:28 +00:00
Add component picker
Signed-off-by: soupette <cyril@strapi.io>
This commit is contained in:
parent
fa479708d7
commit
ddffdf947d
@ -49,7 +49,9 @@
|
||||
"default.dish",
|
||||
"default.openingtimes",
|
||||
"default.restaurantservice",
|
||||
"default.temp"
|
||||
"default.temp",
|
||||
"default.apple",
|
||||
"default.car"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
13
examples/getstarted/src/components/default/apple.json
Normal file
13
examples/getstarted/src/components/default/apple.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"collectionName": "components_default_apples",
|
||||
"info": {
|
||||
"name": "apple",
|
||||
"icon": "apple-alt"
|
||||
},
|
||||
"options": {},
|
||||
"attributes": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
13
examples/getstarted/src/components/default/car.json
Normal file
13
examples/getstarted/src/components/default/car.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"collectionName": "components_default_cars",
|
||||
"info": {
|
||||
"name": "car",
|
||||
"icon": "align-right"
|
||||
},
|
||||
"options": {},
|
||||
"attributes": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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}
|
||||
|
@ -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"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user