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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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