Merge branch 'cm-rep-component' of github.com:strapi/strapi into cm-improvements

This commit is contained in:
soupette 2021-11-02 11:46:38 +01:00
commit ce2cd176e0
8 changed files with 464 additions and 124 deletions

View File

@ -0,0 +1,94 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Box } from '@strapi/design-system/Box';
import { Text } from '@strapi/design-system/Text';
import { Flex } from '@strapi/design-system/Flex';
import { KeyboardNavigable } from '@strapi/design-system/KeyboardNavigable';
const AccordionFooter = styled(Box)`
border-bottom: 1px solid ${({ theme }) => theme.colors.neutral200};
border-right: 1px solid ${({ theme }) => theme.colors.neutral200};
border-left: 1px solid ${({ theme }) => theme.colors.neutral200};
border-radius: 0 0 ${({ theme }) => theme.borderRadius} ${({ theme }) => theme.borderRadius};
`;
const EnhancedGroup = styled(Box)`
> div {
& > * {
border-radius: unset;
border-right: 1px solid ${({ theme }) => theme.colors.neutral200};
border-left: 1px solid ${({ theme }) => theme.colors.neutral200};
border-bottom: 1px solid ${({ theme }) => theme.colors.neutral200};
}
}
> div:first-of-type {
> div {
border-radius: ${({ theme }) => theme.borderRadius} ${({ theme }) => theme.borderRadius} 0 0;
}
> div:not([data-strapi-expanded='true']) {
border-top: 1px solid ${({ theme }) => theme.colors.neutral200};
&:hover {
border-top: 1px solid ${({ theme }) => theme.colors.primary600};
}
}
> span {
border-radius: ${({ theme }) => theme.borderRadius} ${({ theme }) => theme.borderRadius} 0 0;
border-top: 1px solid ${({ theme }) => theme.colors.neutral200};
}
}
& [data-strapi-expanded='true'] {
border: 1px solid ${({ theme }) => theme.colors.primary600};
}
${({ theme, footer }) => `
&:not(${footer}) {
& > *:last-of-type {
border-radius: 0 0 ${theme.borderRadius} ${theme.borderRadius};
}
}
`}
`;
const LabelAction = styled(Box)`
svg path {
fill: ${({ theme }) => theme.colors.neutral500};
}
`;
const AccordionGroupCustom = ({ children, footer, label, labelAction }) => {
return (
<KeyboardNavigable attributeName="data-strapi-accordion-toggle">
{label && (
<Flex paddingBottom={1}>
<Text as="label" textColor="neutral800" small bold>
{label}
</Text>
{labelAction && <LabelAction paddingLeft={1}>{labelAction}</LabelAction>}
</Flex>
)}
<EnhancedGroup footer={footer}>{children}</EnhancedGroup>
{footer && <AccordionFooter>{footer}</AccordionFooter>}
</KeyboardNavigable>
);
};
AccordionGroupCustom.defaultProps = {
footer: null,
label: null,
labelAction: undefined,
};
AccordionGroupCustom.propTypes = {
children: PropTypes.node.isRequired,
footer: PropTypes.node,
label: PropTypes.string,
labelAction: PropTypes.node,
};
export default AccordionGroupCustom;

View File

@ -44,7 +44,7 @@ const DragPreview = ({ displayedValue }) => {
paddingBottom={3}
hasRadius
background="neutral0"
style={{ width: '20vw' }}
width="300px"
>
<Flex justifyContent="space-between">
<ToggleButton type="button">

View File

@ -1,8 +0,0 @@
import styled from 'styled-components';
import { IconButton } from '@strapi/design-system/IconButton';
const DragHandleWrapper = styled(IconButton)`
cursor: move;
`;
export default DragHandleWrapper;

View File

@ -0,0 +1,72 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { Stack } from '@strapi/design-system/Stack';
import { Flex } from '@strapi/design-system/Flex';
import { TextButton } from '@strapi/design-system/TextButton';
import { Icon } from '@strapi/design-system/Icon';
import { Text } from '@strapi/design-system/Text';
import Trash from '@strapi/icons/Trash';
import Drag from '@strapi/icons/Drag';
import DropdownIcon from '@strapi/icons/CarretDown';
import { CustomIconButtonSibling } from './IconButtonCustoms';
const SiblingWrapper = styled.span`
display: flex;
justify-content: space-between;
padding-left: ${({ theme }) => theme.spaces[4]};
padding-right: ${({ theme }) => theme.spaces[4]};
background-color: ${({ theme }) => theme.colors.neutral0};
height: ${50 / 16}rem;
`;
const ToggleButton = styled(TextButton)`
text-align: left;
svg {
width: ${14 / 16}rem;
height: ${14 / 16}rem;
path {
fill: ${({ theme, expanded }) =>
expanded ? theme.colors.primary600 : theme.colors.neutral500};
}
}
`;
const DraggingSibling = ({ displayedValue }) => {
return (
<SiblingWrapper>
<Stack horizontal size={3} flex={1}>
<Flex
justifyContent="center"
borderRadius="50%"
height={`${24 / 16}rem}`}
width={`${24 / 16}rem}`}
aria-hidden
as="span"
background="neutral200"
>
<Icon as={DropdownIcon} width={`${8 / 16}rem}`} color="neutral600" />
</Flex>
<ToggleButton onClick={() => {}} flex={1}>
<Text bold textColor="neutral700">
{displayedValue}
</Text>
</ToggleButton>
</Stack>
<Stack horizontal size={0}>
<CustomIconButtonSibling noBorder onClick={() => {}} icon={<Trash />} />
<CustomIconButtonSibling icon={<Drag />} noBorder />
</Stack>
</SiblingWrapper>
);
};
DraggingSibling.propTypes = {
displayedValue: PropTypes.string.isRequired,
};
export default DraggingSibling;

View File

@ -0,0 +1,36 @@
import styled from 'styled-components';
import { IconButton } from '@strapi/design-system/IconButton';
export const CustomIconButton = styled(IconButton)`
background-color: transparent;
svg {
path {
fill: ${({ theme, expanded }) =>
expanded ? theme.colors.primary600 : theme.colors.neutral600};
}
}
&:hover {
svg {
path {
fill: ${({ theme }) => theme.colors.primary600};
}
}
}
`;
export const DragHandleWrapper = styled(CustomIconButton)`
cursor: move;
`;
export const CustomIconButtonSibling = styled(IconButton)`
background-color: transparent;
svg {
path {
fill: ${({ theme, expanded }) =>
expanded ? theme.colors.primary600 : theme.colors.neutral600};
}
}
`;

View File

@ -1,20 +1,16 @@
import React from 'react';
import styled from 'styled-components';
import { Box } from '@strapi/design-system/Box';
import { Flex } from '@strapi/design-system/Flex';
const StyledBox = styled(Box)`
border: 1px dashed ${({ theme }) => theme.colors.primary500};
const StyledSpan = styled.span`
display: block;
background-color: ${({ theme }) => theme.colors.primary100};
outline: 1px dashed ${({ theme }) => theme.colors.primary500};
outline-offset: -1px;
padding: ${({ theme }) => theme.spaces[6]};
`;
const Preview = () => {
return (
<StyledBox padding={8} background="primary100">
<Box>
<Flex />
</Box>
</StyledBox>
);
return <StyledSpan padding={6} background="primary100" />;
};
export default Preview;

View File

@ -5,32 +5,21 @@ import { useDrag, useDrop } from 'react-dnd';
import { getEmptyImage } from 'react-dnd-html5-backend';
import { useIntl } from 'react-intl';
import toString from 'lodash/toString';
import styled from 'styled-components';
import { Accordion, AccordionToggle, AccordionContent } from '@strapi/design-system/Accordion';
import { Box } from '@strapi/design-system/Box';
import { Flex } from '@strapi/design-system/Flex';
import { IconButton } from '@strapi/design-system/IconButton';
import { Grid, GridItem } from '@strapi/design-system/Grid';
import { Stack } from '@strapi/design-system/Stack';
import { Box } from '@strapi/design-system/Box';
import Trash from '@strapi/icons/Trash';
import DragHandle from '@strapi/icons/Drag';
import Drag from '@strapi/icons/Drag';
import ItemTypes from '../../../utils/ItemTypes';
import getTrad from '../../../utils/getTrad';
import Inputs from '../../Inputs';
import FieldComponent from '../../FieldComponent';
import DragHandleWrapper from './DragHandleWrapper';
import Preview from './Preview';
import DraggingSibling from './DraggingSibling';
import { CustomIconButton, DragHandleWrapper } from './IconButtonCustoms';
import { connect, select } from './utils';
// FIXME
// Temporary workaround to remove the overflow until we migrate the react-select for the relations
// to the DS one
const StyledBox = styled(Box)`
> div {
overflow: visible;
}
`;
/* eslint-disable react/no-array-index-key */
// Issues:
@ -44,8 +33,7 @@ const DraggedItem = ({
// hasErrors,
// hasMinError,
// isFirst,
isOdd,
isDraggingSibling,
isOpen,
isReadOnly,
onClickToggle,
@ -54,6 +42,7 @@ const DraggedItem = ({
// Retrieved from the select function
moveComponentField,
removeRepeatableField,
setIsDraggingSiblig,
triggerFormValidation,
// checkFormErrors,
displayedValue,
@ -137,6 +126,7 @@ const DraggedItem = ({
end: () => {
// Update the errors
triggerFormValidation();
setIsDraggingSiblig(false);
},
collect: monitor => ({
isDragging: monitor.isDragging(),
@ -147,6 +137,12 @@ const DraggedItem = ({
preview(getEmptyImage(), { captureDraggingState: false });
}, [preview]);
useEffect(() => {
if (isDragging) {
setIsDraggingSiblig(true);
}
}, [isDragging, setIsDraggingSiblig]);
// Create the refs
// We need 1 for the drop target
// 1 for the drag target
@ -155,102 +151,180 @@ const DraggedItem = ({
dropRef: drop(dropRef),
};
const accordionTitle = toString(displayedValue);
return (
<StyledBox ref={refs ? refs.dropRef : null}>
<Box ref={refs ? refs.dropRef : null}>
{isDragging && <Preview />}
{!isDragging && (
<Accordion expanded={isOpen} toggle={onClickToggle} id={componentFieldName}>
{!isDragging && isDraggingSibling && (
<DraggingSibling displayedValue={accordionTitle} componentFieldName={componentFieldName} />
)}
{!isDragging && !isDraggingSibling && (
<Accordion expanded={isOpen} toggle={onClickToggle} id={componentFieldName} size="S">
<AccordionToggle
variant={isOdd ? 'primary' : 'secondary'}
title={toString(displayedValue)}
togglePosition="left"
action={
isReadOnly ? null : (
<Flex>
<IconButton
<Stack horizontal size={0}>
<CustomIconButton
expanded={isOpen}
noBorder
onClick={() => {
removeRepeatableField(componentFieldName);
toggleCollapses();
}}
label={formatMessage({
id: getTrad('containers.Edit.delete'),
defaultMessage: 'Edit',
defaultMessage: 'Delete',
})}
icon={<Trash />}
/>
<Box paddingLeft={2}>
<DragHandleWrapper
ref={refs.dragRef}
label={formatMessage({
id: getTrad('components.DragHandle-label'),
defaultMessage: 'Drag',
})}
icon={<DragHandle />}
/>
</Box>
</Flex>
<DragHandleWrapper
expanded={isOpen}
icon={<Drag />}
label={formatMessage({
id: getTrad('components.DragHandle-label'),
defaultMessage: 'Drag',
})}
noBorder
ref={refs.dragRef}
/>
</Stack>
)
}
title={accordionTitle}
togglePosition="left"
/>
<AccordionContent>
<Box
background="neutral100"
paddingLeft={6}
paddingRight={6}
paddingTop={6}
paddingBottom={6}
>
<Stack size={6}>
{fields.map((fieldRow, key) => {
return (
<Grid gap={4} key={key}>
{fieldRow.map(({ name, fieldSchema, metadatas, queryInfos, size }) => {
const isComponent = fieldSchema.type === 'component';
const keys = `${componentFieldName}.${name}`;
<Stack background="neutral100" padding={6} size={6}>
{fields.map((fieldRow, key) => {
return (
<Grid gap={4} key={key}>
{fieldRow.map(({ name, fieldSchema, metadatas, queryInfos, size }) => {
const isComponent = fieldSchema.type === 'component';
const keys = `${componentFieldName}.${name}`;
if (isComponent) {
const componentUid = fieldSchema.component;
return (
<GridItem col={size} s={12} xs={12} key={name}>
<FieldComponent
componentUid={componentUid}
intlLabel={{
id: metadatas.label,
defaultMessage: metadatas.label,
}}
isRepeatable={fieldSchema.repeatable}
isNested
name={keys}
max={fieldSchema.max}
min={fieldSchema.min}
required={fieldSchema.required}
/>
</GridItem>
);
}
if (isComponent) {
const componentUid = fieldSchema.component;
return (
<GridItem key={keys} col={size} s={12} xs={12}>
<Inputs
fieldSchema={fieldSchema}
keys={keys}
metadatas={metadatas}
// onBlur={hasErrors ? checkFormErrors : null}
queryInfos={queryInfos}
<GridItem col={size} s={12} xs={12} key={name}>
<FieldComponent
componentUid={componentUid}
intlLabel={{
id: metadatas.label,
defaultMessage: metadatas.label,
}}
isRepeatable={fieldSchema.repeatable}
isNested
name={keys}
max={fieldSchema.max}
min={fieldSchema.min}
required={fieldSchema.required}
/>
</GridItem>
);
})}
</Grid>
);
})}
</Stack>
</Box>
}
return (
<GridItem key={keys} col={size} s={12} xs={12}>
<Inputs
fieldSchema={fieldSchema}
keys={keys}
metadatas={metadatas}
// onBlur={hasErrors ? checkFormErrors : null}
queryInfos={queryInfos}
/>
</GridItem>
);
})}
</Grid>
);
})}
</Stack>
</AccordionContent>
</Accordion>
)}
</StyledBox>
</Box>
// <StyledBox ref={refs ? refs.dropRef : null}>
// {isDragging && <Preview />}
// {!isDragging && (
// <Accordion expanded={isOpen} toggle={onClickToggle} id={componentFieldName}>
// <AccordionToggle
// action={
// isReadOnly ? null : (
// <Flex>
// <IconButton
// icon={<Trash />}
// />
// <Box paddingLeft={2}>
// <DragHandleWrapper
// ref={refs.dragRef}
// icon={<Drag />}
// />
// </Box>
// </Flex>
// )
// }
// />
// <AccordionContent>
// <Box
// background="neutral100"
// paddingLeft={6}
// paddingRight={6}
// paddingTop={6}
// paddingBottom={6}
// >
// <Stack size={6}>
// {fields.map((fieldRow, key) => {
// return (
// <Grid gap={4} key={key}>
// {fieldRow.map(({ name, fieldSchema, metadatas, queryInfos, size }) => {
// const isComponent = fieldSchema.type === 'component';
// const keys = `${componentFieldName}.${name}`;
// if (isComponent) {
// const componentUid = fieldSchema.component;
// return (
// <GridItem col={size} s={12} xs={12} key={name}>
// <FieldComponent
// componentUid={componentUid}
// intlLabel={{
// id: metadatas.label,
// defaultMessage: metadatas.label,
// }}
// isRepeatable={fieldSchema.repeatable}
// isNested
// name={keys}
// max={fieldSchema.max}
// min={fieldSchema.min}
// required={fieldSchema.required}
// />
// </GridItem>
// );
// }
// return (
// <GridItem key={keys} col={size} s={12} xs={12}>
// <Inputs
// fieldSchema={fieldSchema}
// keys={keys}
// metadatas={metadatas}
// // onBlur={hasErrors ? checkFormErrors : null}
// queryInfos={queryInfos}
// />
// </GridItem>
// );
// })}
// </Grid>
// );
// })}
// </Stack>
// </Box>
// </AccordionContent>
// </Accordion>
// )}
// </StyledBox>
);
};
@ -259,7 +333,9 @@ DraggedItem.defaultProps = {
// hasErrors: false,
// hasMinError: false,
// isFirst: false,
isDraggingSibling: false,
isOpen: false,
setIsDraggingSiblig: () => {},
toggleCollapses: () => {},
};
@ -269,7 +345,7 @@ DraggedItem.propTypes = {
// hasErrors: PropTypes.bool,
// hasMinError: PropTypes.bool,
// isFirst: PropTypes.bool,
isOdd: PropTypes.bool.isRequired,
isDraggingSibling: PropTypes.bool,
isOpen: PropTypes.bool,
isReadOnly: PropTypes.bool.isRequired,
onClickToggle: PropTypes.func.isRequired,
@ -277,6 +353,7 @@ DraggedItem.propTypes = {
toggleCollapses: PropTypes.func,
moveComponentField: PropTypes.func.isRequired,
removeRepeatableField: PropTypes.func.isRequired,
setIsDraggingSiblig: PropTypes.func,
triggerFormValidation: PropTypes.func.isRequired,
// checkFormErrors: PropTypes.func.isRequired,
displayedValue: PropTypes.string.isRequired,

View File

@ -1,12 +1,17 @@
import React, { memo, useCallback, useMemo, useState } from 'react';
/* eslint-disable import/no-cycle */
import { useDrop } from 'react-dnd';
import { useIntl } from 'react-intl';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import take from 'lodash/take';
// import { FormattedMessage } from 'react-intl';
import { useNotification } from '@strapi/helper-plugin';
import { Box } from '@strapi/design-system/Box';
import { Flex } from '@strapi/design-system/Flex';
import { TextButton } from '@strapi/design-system/TextButton';
import Plus from '@strapi/icons/Plus';
// import { ErrorMessage } from '@buffetjs/styles';
import { getMaxTempKey, getTrad } from '../../utils';
import { useContentTypeLayout } from '../../hooks';
@ -14,8 +19,20 @@ import ItemTypes from '../../utils/ItemTypes';
import ComponentInitializer from '../ComponentInitializer';
import connect from './utils/connect';
import select from './utils/select';
import Button from './AddFieldButton';
import DraggedItem from './DraggedItem';
import AccordionGroupCustom from './AccordionGroupCustom';
const TextButtonCustom = styled(TextButton)`
height: 100%;
width: 100%;
border-radius: 0 0 4px 4px;
display: flex;
justify-content: center;
span {
font-weight: 600;
font-size: 14px;
}
`;
const RepeatableComponent = ({
addRepeatableComponentToField,
@ -30,7 +47,9 @@ const RepeatableComponent = ({
name,
}) => {
const toggleNotification = useNotification();
const { formatMessage } = useIntl();
const [collapseToOpen, setCollapseToOpen] = useState('');
const [isDraggingSibling, setIsDraggingSiblig] = useState(false);
const [, drop] = useDrop({ accept: ItemTypes.COMPONENT });
const { getComponentLayout } = useContentTypeLayout();
const componentLayoutData = useMemo(() => getComponentLayout(componentUid), [
@ -94,8 +113,19 @@ const RepeatableComponent = ({
}
return (
<Box hasRadius borderColor="neutral200">
<Box ref={drop}>
<Box hasRadius background="neutral0" shadow="tableShadow" ref={drop}>
<AccordionGroupCustom
footer={
<Flex justifyContent="center" height="48px" background="neutral0" hasRadius>
<TextButtonCustom disabled={isReadOnly} onClick={handleClick} startIcon={<Plus />}>
{formatMessage({
id: getTrad('containers.EditView.add.new-entry'),
defaultMessage: 'Add an entry',
})}
</TextButtonCustom>
</Flex>
}
>
{componentValue.map((data, index) => {
const key = data.__temp_key__;
const isOpen = collapseToOpen === key;
@ -116,8 +146,8 @@ const RepeatableComponent = ({
doesPreviousFieldContainErrorsAndIsOpen={doesPreviousFieldContainErrorsAndIsOpen}
hasErrors={hasErrors}
hasMinError={hasMinError}
isDraggingSibling={isDraggingSibling}
isFirst={index === 0}
isOdd={index % 2 === 1}
isOpen={isOpen}
isReadOnly={isReadOnly}
key={key}
@ -130,24 +160,67 @@ const RepeatableComponent = ({
}}
parentName={name}
schema={componentLayoutData}
setIsDraggingSiblig={setIsDraggingSiblig}
toggleCollapses={toggleCollapses}
/>
);
})}
</Box>
<Button
// TODO
// hasMinError={hasMinError}
disabled={isReadOnly}
// TODO
// doesPreviousFieldContainErrorsAndIsClosed={
// componentValueLength > 0 &&
// componentErrorKeys.includes(`${name}.${componentValueLength - 1}`) &&
// componentValue[componentValueLength - 1].__temp_key__ !== collapseToOpen
// }
onClick={handleClick}
/>
</AccordionGroupCustom>
</Box>
// <Box hasRadius borderColor="neutral200">
// <Box ref={drop}>
// {componentValue.map((data, index) => {
// const key = data.__temp_key__;
// const isOpen = collapseToOpen === key;
// const componentFieldName = `${name}.${index}`;
// const previousComponentTempKey = get(componentValue, [index - 1, '__temp_key__']);
// const doesPreviousFieldContainErrorsAndIsOpen =
// componentErrorKeys.includes(`${name}.${index - 1}`) &&
// index !== 0 &&
// collapseToOpen === previousComponentTempKey;
// const hasErrors = componentErrorKeys.includes(componentFieldName);
// return (
// <DraggedItem
// componentFieldName={componentFieldName}
// componentUid={componentUid}
// // TODO
// doesPreviousFieldContainErrorsAndIsOpen={doesPreviousFieldContainErrorsAndIsOpen}
// hasErrors={hasErrors}
// hasMinError={hasMinError}
// isFirst={index === 0}
// isOdd={index % 2 === 1}
// isOpen={isOpen}
// isReadOnly={isReadOnly}
// key={key}
// onClickToggle={() => {
// if (isOpen) {
// setCollapseToOpen('');
// } else {
// setCollapseToOpen(key);
// }
// }}
// parentName={name}
// schema={componentLayoutData}
// toggleCollapses={toggleCollapses}
// />
// );
// })}
// </Box>
// <Button
// // TODO
// // hasMinError={hasMinError}
// disabled={isReadOnly}
// // TODO
// // doesPreviousFieldContainErrorsAndIsClosed={
// // componentValueLength > 0 &&
// // componentErrorKeys.includes(`${name}.${componentValueLength - 1}`) &&
// // componentValue[componentValueLength - 1].__temp_key__ !== collapseToOpen
// // }
// onClick={handleClick}
// />
// </Box>
);
// return (