mirror of
https://github.com/strapi/strapi.git
synced 2025-10-06 22:01:59 +00:00
feat(wip): add dnd to dynamic zones
This commit is contained in:
parent
23bf8c5dac
commit
f0b851a7b9
@ -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",
|
||||
|
@ -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;
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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(),
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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';
|
||||
|
@ -1,7 +1,6 @@
|
||||
export default {
|
||||
COMPONENT: 'component',
|
||||
EDIT_FIELD: 'editField',
|
||||
EDIT_RELATION: 'editRelation',
|
||||
FIELD: 'field',
|
||||
RELATION: 'relation',
|
||||
DYNAMIC_ZONE: 'dynamicZone',
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user