feat(wip): add dnd to dynamic zones

This commit is contained in:
Josh 2022-11-25 16:58:13 +00:00
parent 23bf8c5dac
commit f0b851a7b9
11 changed files with 176 additions and 132 deletions

View File

@ -57,19 +57,33 @@
},
"enumeration": {
"type": "enumeration",
"enum": ["A", "B", "C", "D", "E"]
"enum": [
"A",
"B",
"C",
"D",
"E"
]
},
"single_media": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": ["images", "files", "videos"]
"allowedTypes": [
"images",
"files",
"videos"
]
},
"multiple_media": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": ["images", "files", "videos"]
"allowedTypes": [
"images",
"files",
"videos"
]
},
"json": {
"type": "json"
@ -86,7 +100,10 @@
},
"dynamiczone": {
"type": "dynamiczone",
"components": ["basic.simple"]
"components": [
"basic.simple",
"blog.test-como"
]
},
"one_way_tag": {
"type": "relation",

View File

@ -4,6 +4,7 @@ import styled from 'styled-components';
import { pxToRem } from '@strapi/helper-plugin';
import { Box, Flex, Typography, IconButton } from '@strapi/design-system';
import { Trash, Drag, CarretDown } from '@strapi/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const DragPreviewBox = styled(Box)`
border: 1px solid ${({ theme }) => theme.colors.neutral200};
@ -26,6 +27,13 @@ const DropdownIconWrapper = styled(Box)`
}
`;
const Icon = styled(FontAwesomeIcon)`
width: 14px;
height: 14px;
color: ${({ theme }) => theme.colors.neutral600};
`;
const ToggleButton = styled.button`
border: none;
background: transparent;
@ -35,7 +43,7 @@ const ToggleButton = styled.button`
padding: 0;
`;
const DragPreview = ({ displayedValue }) => {
const DragPreview = ({ displayedValue, icon }) => {
return (
<DragPreviewBox
paddingLeft={3}
@ -52,11 +60,12 @@ const DragPreview = ({ displayedValue }) => {
<DropdownIconWrapper background="neutral200">
<CarretDown />
</DropdownIconWrapper>
<Box paddingLeft={6} maxWidth={pxToRem(150)}>
<Flex gap={2} paddingLeft={icon ? 3 : 6} maxWidth={pxToRem(150)}>
{icon ? <Icon icon={icon} /> : null}
<Typography textColor="neutral700" ellipsis>
{displayedValue}
</Typography>
</Box>
</Flex>
</Flex>
</ToggleButton>
<Box paddingLeft={3}>
@ -76,8 +85,13 @@ const DragPreview = ({ displayedValue }) => {
);
};
DragPreview.defaultProps = {
icon: undefined,
};
DragPreview.propTypes = {
displayedValue: PropTypes.string.isRequired,
icon: PropTypes.string,
};
export default DragPreview;

View File

@ -5,7 +5,7 @@ import LayoutDndProvider from '../LayoutDndProvider';
import ItemTypes from '../../utils/ItemTypes';
import CardPreview from '../../pages/ListSettingsView/components/CardPreview';
import RepeatableComponentPreview from './RepetableComponentDragPreview';
import ComponentPreview from './ComponentDragPreview';
const layerStyles = {
position: 'fixed',
@ -52,11 +52,14 @@ const CustomDragLayer = () => {
<LayoutDndProvider>
<div style={layerStyles}>
<div style={getItemStyles(initialOffset, currentOffset, mouseOffset)} className="col-md-2">
{[ItemTypes.EDIT_RELATION, ItemTypes.EDIT_FIELD, ItemTypes.FIELD].includes(itemType) && (
{[ItemTypes.EDIT_FIELD, ItemTypes.FIELD].includes(itemType) && (
<CardPreview labelField={item.labelField} />
)}
{itemType === ItemTypes.COMPONENT && (
<RepeatableComponentPreview displayedValue={item.displayedValue} />
<ComponentPreview displayedValue={item.displayedValue} />
)}
{itemType === ItemTypes.DYNAMIC_ZONE && (
<ComponentPreview icon={item.icon} displayedValue={item.displayedValue} />
)}
</div>
</div>

View File

@ -1,24 +1,25 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { useIntl } from 'react-intl';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import get from 'lodash/get';
import { getEmptyImage } from 'react-dnd-html5-backend';
import { Accordion, AccordionToggle, AccordionContent } from '@strapi/design-system/Accordion';
import { IconButton } from '@strapi/design-system/IconButton';
import { Box } from '@strapi/design-system/Box';
import { Flex } from '@strapi/design-system/Flex';
import { Stack } from '@strapi/design-system/Stack';
import {
Accordion,
AccordionToggle,
AccordionContent,
IconButton,
Box,
Flex,
Stack,
} from '@strapi/design-system';
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
import { Trash, Drag } from '@strapi/icons';
import Trash from '@strapi/icons/Trash';
import ArrowDown from '@strapi/icons/ArrowDown';
import ArrowUp from '@strapi/icons/ArrowUp';
import { useContentTypeLayout } from '../../../hooks';
import { getTrad } from '../../../utils';
import { useContentTypeLayout, useDragAndDrop } from '../../../hooks';
import { composeRefs, getTrad, ItemTypes } from '../../../utils';
import FieldComponent from '../../FieldComponent';
@ -46,17 +47,22 @@ const Rectangle = styled(Box)`
height: ${({ theme }) => theme.spaces[4]};
`;
const Preview = 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 DynamicZoneComponent = ({
componentUid,
formErrors,
index,
isFieldAllowed,
onMoveComponentDownClick,
onMoveComponentUpClick,
name,
onRemoveComponentClick,
showDownIcon,
showUpIcon,
onMoveComponent,
}) => {
const [isOpen, setIsOpen] = useState(true);
const { formatMessage } = useIntl();
@ -103,69 +109,85 @@ const DynamicZoneComponent = ({
setIsOpen((s) => !s);
};
const [{ handlerId, isDragging, handleKeyDown }, boxRef, dropRef, dragRef, dragPreviewRef] =
useDragAndDrop(isFieldAllowed, {
type: ItemTypes.DYNAMIC_ZONE,
index,
item: {
displayedValue: `${friendlyName}${mainValue}`,
icon,
},
onMoveItem: onMoveComponent,
});
useEffect(() => {
dragPreviewRef(getEmptyImage(), { captureDraggingState: false });
}, [dragPreviewRef]);
const composedBoxRefs = composeRefs(boxRef, dropRef);
return (
<Box>
<Flex justifyContent="center">
<Rectangle background="neutral200" />
</Flex>
<StyledBox hasRadius>
<Accordion expanded={isOpen} onToggle={handleToggle} size="S" error={errorMessage}>
<AccordionToggle
startIcon={<FontAwesomeIcon icon={icon} />}
action={
<Stack horizontal spacing={0} expanded={isOpen}>
{showDownIcon && (
<IconButtonCustom
noBorder
label={formatMessage({
id: getTrad('components.DynamicZone.move-down-label'),
defaultMessage: 'Move component down',
})}
onClick={onMoveComponentDownClick}
icon={<ArrowDown />}
/>
)}
{showUpIcon && (
<IconButtonCustom
noBorder
label={formatMessage({
id: getTrad('components.DynamicZone.move-up-label'),
defaultMessage: 'Move component up',
})}
onClick={onMoveComponentUpClick}
icon={<ArrowUp />}
/>
)}
{isFieldAllowed && (
<IconButtonCustom
noBorder
label={formatMessage(
{
id: getTrad('components.DynamicZone.delete-label'),
defaultMessage: 'Delete {name}',
},
{ name: friendlyName }
)}
onClick={onRemoveComponentClick}
icon={<Trash />}
/>
)}
</Stack>
}
title={`${friendlyName}${mainValue}`}
togglePosition="left"
/>
<AccordionContent>
<AccordionContentRadius background="neutral0">
<FieldComponent
componentUid={componentUid}
icon={icon}
name={`${name}.${index}`}
isFromDynamicZone
/>
</AccordionContentRadius>
</AccordionContent>
</Accordion>
<StyledBox ref={composedBoxRefs} hasRadius>
{isDragging ? (
<Preview ref={dragPreviewRef} padding={6} background="primary100" />
) : (
<Accordion expanded={isOpen} onToggle={handleToggle} size="S" error={errorMessage}>
<AccordionToggle
startIcon={<FontAwesomeIcon icon={icon} />}
action={
isFieldAllowed ? (
<Stack horizontal spacing={0} expanded={isOpen}>
<IconButtonCustom
noBorder
label={formatMessage(
{
id: getTrad('components.DynamicZone.delete-label'),
defaultMessage: 'Delete {name}',
},
{ name: friendlyName }
)}
onClick={onRemoveComponentClick}
>
<Trash />
</IconButtonCustom>
<IconButton
forwardedAs="div"
role="button"
noBorder
tabIndex={0}
onClick={(e) => e.stopPropagation()}
data-handler-id={handlerId}
ref={dragRef}
label={formatMessage({
id: getTrad('components.DragHandle-label'),
defaultMessage: 'Drag',
})}
onKeyDown={handleKeyDown}
>
<Drag />
</IconButton>
</Stack>
) : null
}
title={`${friendlyName}${mainValue}`}
togglePosition="left"
/>
<AccordionContent>
<AccordionContentRadius background="neutral0">
<FieldComponent
componentUid={componentUid}
icon={icon}
name={`${name}.${index}`}
isFromDynamicZone
/>
</AccordionContentRadius>
</AccordionContent>
</Accordion>
)}
</StyledBox>
</Box>
);
@ -175,8 +197,6 @@ DynamicZoneComponent.defaultProps = {
formErrors: {},
index: 0,
isFieldAllowed: true,
showDownIcon: true,
showUpIcon: true,
};
DynamicZoneComponent.propTypes = {
@ -185,11 +205,8 @@ DynamicZoneComponent.propTypes = {
index: PropTypes.number,
isFieldAllowed: PropTypes.bool,
name: PropTypes.string.isRequired,
onMoveComponentDownClick: PropTypes.func.isRequired,
onMoveComponentUpClick: PropTypes.func.isRequired,
onMoveComponent: PropTypes.func.isRequired,
onRemoveComponentClick: PropTypes.func.isRequired,
showDownIcon: PropTypes.bool,
showUpIcon: PropTypes.bool,
};
export default DynamicZoneComponent;

View File

@ -27,8 +27,7 @@ const DynamicZone = ({
isFieldAllowed,
isFieldReadable,
labelAction,
moveComponentUp,
moveComponentDown,
moveComponentField,
removeComponentFromDynamicZone,
dynamicDisplayedComponents,
fieldSchema,
@ -82,12 +81,12 @@ const DynamicZone = ({
}
};
const handleMoveComponentDown = (name, componentIndex) => () => {
moveComponentDown(name, componentIndex);
};
const handleMoveComponentUp = (name, componentIndex) => () => {
moveComponentUp(name, componentIndex);
const handleMoveComponent = (newIndex, currentIndex) => {
moveComponentField({
name,
newIndex,
currentIndex,
});
};
const handleRemoveComponent = (name, currentIndex) => () => {
@ -117,27 +116,19 @@ const DynamicZone = ({
numberOfComponents={dynamicDisplayedComponentsLength}
required={fieldSchema.required || false}
/>
{dynamicDisplayedComponents.map((componentUid, index) => {
const showDownIcon = isFieldAllowed && index < dynamicDisplayedComponentsLength - 1;
const showUpIcon = isFieldAllowed && index > 0;
return (
<DynamicZoneComponent
componentUid={componentUid}
formErrors={formErrors}
// eslint-disable-next-line react/no-array-index-key
key={index}
index={index}
isFieldAllowed={isFieldAllowed}
onMoveComponentDownClick={handleMoveComponentDown(name, index)}
onMoveComponentUpClick={handleMoveComponentUp(name, index)}
name={name}
onRemoveComponentClick={handleRemoveComponent(name, index)}
showDownIcon={showDownIcon}
showUpIcon={showUpIcon}
/>
);
})}
{dynamicDisplayedComponents.map((componentUid, index) => (
<DynamicZoneComponent
componentUid={componentUid}
formErrors={formErrors}
// eslint-disable-next-line react/no-array-index-key
key={`${componentUid}-${index}`}
index={index}
isFieldAllowed={isFieldAllowed}
name={name}
onMoveComponent={handleMoveComponent}
onRemoveComponentClick={handleRemoveComponent(name, index)}
/>
))}
</Box>
)}
@ -188,8 +179,7 @@ DynamicZone.propTypes = {
description: PropTypes.string,
label: PropTypes.string,
}).isRequired,
moveComponentUp: PropTypes.func.isRequired,
moveComponentDown: PropTypes.func.isRequired,
moveComponentField: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
removeComponentFromDynamicZone: PropTypes.func.isRequired,
};

View File

@ -51,8 +51,7 @@ describe('DynamicZone', () => {
label: 'dynamic zone',
description: 'dynamic description',
},
moveComponentDown: jest.fn(),
moveComponentUp: jest.fn(),
moveComponentField: jest.fn(),
name: 'DynamicZoneComponent',
removeComponentFromDynamicZone: jest.fn(),
};

View File

@ -9,8 +9,7 @@ function useSelect(name) {
isCreatingEntry,
formErrors,
modifiedData,
moveComponentUp,
moveComponentDown,
moveComponentField,
removeComponentFromDynamicZone,
readActionAllowedFields,
updateActionAllowedFields,
@ -39,8 +38,7 @@ function useSelect(name) {
isCreatingEntry,
isFieldAllowed,
isFieldReadable,
moveComponentUp,
moveComponentDown,
moveComponentField,
removeComponentFromDynamicZone,
dynamicDisplayedComponents,
};

View File

@ -597,8 +597,14 @@ const EditViewDataManagerProvider = ({
status,
layout: currentContentTypeLayout,
modifiedData,
moveComponentDown,
moveComponentField,
/**
* @deprecated use `moveComponentField` instead. This will be removed in v5.
*/
moveComponentDown,
/**
* @deprecated use `moveComponentField` instead. This will be removed in v5.
*/
moveComponentUp,
onChange: handleChange,
onPublish: handlePublish,

View File

@ -147,7 +147,6 @@ const DraggedItem = ({
})}
icon={<Trash />}
/>
{/* react-dnd is broken in firefox with our IconButton, maybe a ref issue */}
<IconButton
className="drag-handle"
ref={composedAccordionRefs}

View File

@ -6,3 +6,5 @@ export { default as usePluginsQueryParams } from './usePluginsQueryParams';
export { default as useSyncRbac } from './useSyncRbac';
export { default as useWysiwyg } from './useWysiwyg';
export { usePrev } from './usePrev';
export { useDragAndDrop } from './useDragAndDrop';
export { useKeyboardDragAndDrop } from './useKeyboardDragAndDrop';

View File

@ -1,7 +1,6 @@
export default {
COMPONENT: 'component',
EDIT_FIELD: 'editField',
EDIT_RELATION: 'editRelation',
FIELD: 'field',
RELATION: 'relation',
DYNAMIC_ZONE: 'dynamicZone',
};