mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 08:19:07 +00:00
refactor: repeatableComponents (make it simpler)
This commit is contained in:
parent
eef9fdb463
commit
ab5dac4ca2
@ -498,12 +498,12 @@ const EditViewDataManagerProvider = ({
|
||||
[shouldCheckDZErrors]
|
||||
);
|
||||
|
||||
const moveComponentField = useCallback((pathToComponent, dragIndex, hoverIndex) => {
|
||||
const moveComponentField = useCallback(({ name, newIndex, currentIndex }) => {
|
||||
dispatch({
|
||||
type: 'MOVE_COMPONENT_FIELD',
|
||||
pathToComponent,
|
||||
dragIndex,
|
||||
hoverIndex,
|
||||
keys: name.split('.'),
|
||||
newIndex,
|
||||
oldIndex: currentIndex,
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
@ -178,6 +178,7 @@ const reducer = (state, action) =>
|
||||
|
||||
break;
|
||||
}
|
||||
case 'MOVE_COMPONENT_FIELD':
|
||||
case 'REORDER_RELATION': {
|
||||
const { oldIndex, newIndex, keys } = action;
|
||||
const path = ['modifiedData', ...keys];
|
||||
@ -265,25 +266,6 @@ const reducer = (state, action) =>
|
||||
draftState.shouldCheckErrors = false;
|
||||
break;
|
||||
}
|
||||
case 'MOVE_COMPONENT_FIELD': {
|
||||
const currentValue = get(state, ['modifiedData', ...action.pathToComponent]);
|
||||
const valueToInsert = get(state, [
|
||||
'modifiedData',
|
||||
...action.pathToComponent,
|
||||
action.dragIndex,
|
||||
]);
|
||||
|
||||
const updatedValue = moveFields(
|
||||
currentValue,
|
||||
action.dragIndex,
|
||||
action.hoverIndex,
|
||||
valueToInsert
|
||||
);
|
||||
|
||||
set(draftState, ['modifiedData', ...action.pathToComponent], updatedValue);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'MOVE_COMPONENT_UP':
|
||||
case 'MOVE_COMPONENT_DOWN': {
|
||||
const { currentIndex, dynamicZoneName, shouldCheckErrors } = action;
|
||||
|
@ -1,72 +0,0 @@
|
||||
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 { Typography } from '@strapi/design-system/Typography';
|
||||
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 spacing={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}>
|
||||
<Typography fontWeight="bold" textColor="neutral700">
|
||||
{displayedValue}
|
||||
</Typography>
|
||||
</ToggleButton>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal spacing={0}>
|
||||
<CustomIconButtonSibling noBorder onClick={() => {}} icon={<Trash />} />
|
||||
<CustomIconButtonSibling icon={<Drag />} noBorder />
|
||||
</Stack>
|
||||
</SiblingWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
DraggingSibling.propTypes = {
|
||||
displayedValue: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DraggingSibling;
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { forwardRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledSpan = styled.span`
|
||||
@ -9,8 +9,8 @@ const StyledSpan = styled.span`
|
||||
padding: ${({ theme }) => theme.spaces[6]};
|
||||
`;
|
||||
|
||||
const Preview = () => {
|
||||
return <StyledSpan padding={6} background="primary100" />;
|
||||
};
|
||||
const Preview = forwardRef((_, ref) => {
|
||||
return <StyledSpan ref={ref} padding={6} background="primary100" />;
|
||||
});
|
||||
|
||||
export default Preview;
|
||||
|
@ -1,24 +1,27 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import React, { memo, useEffect, useRef, useState } from 'react';
|
||||
import React, { memo, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import styled from 'styled-components';
|
||||
import { useIntl } from 'react-intl';
|
||||
import toString from 'lodash/toString';
|
||||
|
||||
import { Accordion, AccordionToggle, AccordionContent } from '@strapi/design-system/Accordion';
|
||||
import { Grid, GridItem } from '@strapi/design-system/Grid';
|
||||
import { Stack } from '@strapi/design-system/Stack';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Tooltip } from '@strapi/design-system/Tooltip';
|
||||
|
||||
import Trash from '@strapi/icons/Trash';
|
||||
import Drag from '@strapi/icons/Drag';
|
||||
import ItemTypes from '../../../utils/ItemTypes';
|
||||
import getTrad from '../../../utils/getTrad';
|
||||
|
||||
import { composeRefs, getTrad, ItemTypes } from '../../../utils';
|
||||
|
||||
import Inputs from '../../Inputs';
|
||||
import FieldComponent from '../../FieldComponent';
|
||||
|
||||
import Preview from './Preview';
|
||||
import DraggingSibling from './DraggingSibling';
|
||||
import { CustomIconButton } from './IconButtonCustoms';
|
||||
import { connect, select } from './utils';
|
||||
|
||||
@ -48,7 +51,7 @@ const DraggedItem = ({
|
||||
// Errors are retrieved from the AccordionGroupCustom cloneElement
|
||||
hasErrorMessage,
|
||||
hasErrors,
|
||||
isDraggingSibling,
|
||||
index,
|
||||
isOpen,
|
||||
isReadOnly,
|
||||
onClickToggle,
|
||||
@ -57,90 +60,70 @@ const DraggedItem = ({
|
||||
// Retrieved from the select function
|
||||
moveComponentField,
|
||||
removeRepeatableField,
|
||||
setIsDraggingSibling,
|
||||
triggerFormValidation,
|
||||
// checkFormErrors,
|
||||
displayedValue,
|
||||
}) => {
|
||||
const dragRef = useRef(null);
|
||||
const dropRef = useRef(null);
|
||||
const [, forceRerenderAfterDnd] = useState(false);
|
||||
const accordionRef = useRef(null);
|
||||
const boxRef = useRef(null);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const fields = schema.layouts.edit;
|
||||
|
||||
const [, drop] = useDrop({
|
||||
const [{ handlerId }, dropRef] = useDrop({
|
||||
accept: ItemTypes.COMPONENT,
|
||||
canDrop() {
|
||||
return false;
|
||||
collect(monitor) {
|
||||
return {
|
||||
handlerId: monitor.getHandlerId(),
|
||||
};
|
||||
},
|
||||
hover(item, monitor) {
|
||||
if (!dropRef.current) {
|
||||
if (!boxRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dragPath = item.originalPath;
|
||||
const hoverPath = componentFieldName;
|
||||
const fullPathToComponentArray = dragPath.split('.');
|
||||
const dragIndexString = fullPathToComponentArray.slice().splice(-1).join('');
|
||||
const hoverIndexString = hoverPath.split('.').splice(-1).join('');
|
||||
const pathToComponentArray = fullPathToComponentArray.slice(
|
||||
0,
|
||||
fullPathToComponentArray.length - 1
|
||||
);
|
||||
const dragIndex = parseInt(dragIndexString, 10);
|
||||
const hoverIndex = parseInt(hoverIndexString, 10);
|
||||
const dragIndex = item.index;
|
||||
const currentIndex = index;
|
||||
|
||||
// Don't replace items with themselves
|
||||
if (dragIndex === hoverIndex) {
|
||||
if (dragIndex === currentIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine rectangle on screen
|
||||
const hoverBoundingRect = dropRef.current.getBoundingClientRect();
|
||||
// Get vertical middle
|
||||
const hoverBoundingRect = boxRef.current.getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
// Determine mouse position
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
// Get pixels to the top
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
// Only perform the move when the mouse has crossed half of the items height
|
||||
// When dragging downwards, only move when the cursor is below 50%
|
||||
// When dragging upwards, only move when the cursor is above 50%
|
||||
// Dragging downwards
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||
if (dragIndex < currentIndex && hoverClientY < hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
// Dragging upwards
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
// If They are not in the same level, should not move
|
||||
if (dragPath.split('.').length !== hoverPath.split('.').length) {
|
||||
return;
|
||||
}
|
||||
// Time to actually perform the action in the data
|
||||
moveComponentField(pathToComponentArray, dragIndex, hoverIndex);
|
||||
|
||||
item.originalPath = hoverPath;
|
||||
// Dragging upwards
|
||||
if (dragIndex > currentIndex && hoverClientY > hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Time to actually perform the action
|
||||
moveComponentField(dragIndex, currentIndex);
|
||||
|
||||
item.index = currentIndex;
|
||||
},
|
||||
});
|
||||
const [{ isDragging }, drag, preview] = useDrag({
|
||||
const [{ isDragging }, dragRef, previewRef] = useDrag({
|
||||
type: ItemTypes.COMPONENT,
|
||||
item() {
|
||||
// Close all collapses
|
||||
toggleCollapses(-1);
|
||||
|
||||
return {
|
||||
displayedValue,
|
||||
originalPath: componentFieldName,
|
||||
index,
|
||||
};
|
||||
},
|
||||
end() {
|
||||
// Update the errors
|
||||
triggerFormValidation();
|
||||
setIsDraggingSibling(false);
|
||||
// setIsDraggingSibling(false);
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
@ -148,43 +131,20 @@ const DraggedItem = ({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
preview(getEmptyImage(), { captureDraggingState: false });
|
||||
}, [preview]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
setIsDraggingSibling(true);
|
||||
}
|
||||
}, [isDragging, setIsDraggingSibling]);
|
||||
|
||||
// Effect in order to force a rerender after reordering the components
|
||||
// Since we are removing the Accordion when doing the DnD we are losing the dragRef, therefore the replaced element cannot be dragged
|
||||
// anymore, this hack forces a rerender in order to apply the dragRef
|
||||
useEffect(() => {
|
||||
if (!isDraggingSibling) {
|
||||
forceRerenderAfterDnd((prev) => !prev);
|
||||
}
|
||||
}, [isDraggingSibling]);
|
||||
|
||||
// Create the refs
|
||||
// We need 1 for the drop target
|
||||
// 1 for the drag target
|
||||
const refs = {
|
||||
dragRef: drag(dragRef),
|
||||
dropRef: drop(dropRef),
|
||||
};
|
||||
previewRef(getEmptyImage(), { captureDraggingState: false });
|
||||
}, [previewRef]);
|
||||
|
||||
const accordionTitle = toString(displayedValue);
|
||||
const accordionHasError = hasErrors ? 'error' : undefined;
|
||||
|
||||
return (
|
||||
<Box ref={refs ? refs.dropRef : null}>
|
||||
{isDragging && <Preview />}
|
||||
{!isDragging && isDraggingSibling && (
|
||||
<DraggingSibling displayedValue={accordionTitle} componentFieldName={componentFieldName} />
|
||||
)}
|
||||
const composedAccordionRefs = composeRefs(accordionRef, dragRef);
|
||||
const composedBoxRefs = composeRefs(boxRef, dropRef);
|
||||
|
||||
{!isDragging && !isDraggingSibling && (
|
||||
return (
|
||||
<Box ref={composedBoxRefs} style={{ border: '1px solid red' }}>
|
||||
{isDragging ? (
|
||||
<Preview ref={previewRef} />
|
||||
) : (
|
||||
<Accordion
|
||||
error={accordionHasError}
|
||||
hasErrorMessage={hasErrorMessage}
|
||||
@ -220,8 +180,9 @@ const DraggedItem = ({
|
||||
<DragButton
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
ref={refs.dragRef}
|
||||
ref={composedAccordionRefs}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
data-handler-id={handlerId}
|
||||
>
|
||||
<Drag />
|
||||
</DragButton>
|
||||
@ -290,9 +251,7 @@ const DraggedItem = ({
|
||||
|
||||
DraggedItem.defaultProps = {
|
||||
componentUid: undefined,
|
||||
isDraggingSibling: false,
|
||||
isOpen: false,
|
||||
setIsDraggingSibling() {},
|
||||
toggleCollapses() {},
|
||||
};
|
||||
|
||||
@ -301,7 +260,7 @@ DraggedItem.propTypes = {
|
||||
componentUid: PropTypes.string,
|
||||
hasErrorMessage: PropTypes.bool.isRequired,
|
||||
hasErrors: PropTypes.bool.isRequired,
|
||||
isDraggingSibling: PropTypes.bool,
|
||||
index: PropTypes.number.isRequired,
|
||||
isOpen: PropTypes.bool,
|
||||
isReadOnly: PropTypes.bool.isRequired,
|
||||
onClickToggle: PropTypes.func.isRequired,
|
||||
@ -309,9 +268,7 @@ DraggedItem.propTypes = {
|
||||
toggleCollapses: PropTypes.func,
|
||||
moveComponentField: PropTypes.func.isRequired,
|
||||
removeRepeatableField: PropTypes.func.isRequired,
|
||||
setIsDraggingSibling: PropTypes.func,
|
||||
triggerFormValidation: PropTypes.func.isRequired,
|
||||
// checkFormErrors: PropTypes.func.isRequired,
|
||||
displayedValue: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
@ -3,13 +3,7 @@ import { get, toString } from 'lodash';
|
||||
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
||||
|
||||
function useSelect({ schema, componentFieldName }) {
|
||||
const {
|
||||
checkFormErrors,
|
||||
modifiedData,
|
||||
moveComponentField,
|
||||
removeRepeatableField,
|
||||
triggerFormValidation,
|
||||
} = useCMEditViewDataManager();
|
||||
const { modifiedData, removeRepeatableField, triggerFormValidation } = useCMEditViewDataManager();
|
||||
|
||||
const mainField = useMemo(() => get(schema, ['settings', 'mainField'], 'id'), [schema]);
|
||||
const displayedValue = toString(
|
||||
@ -19,8 +13,6 @@ function useSelect({ schema, componentFieldName }) {
|
||||
return {
|
||||
displayedValue,
|
||||
mainField,
|
||||
checkFormErrors,
|
||||
moveComponentField,
|
||||
removeRepeatableField,
|
||||
schema,
|
||||
triggerFormValidation,
|
||||
|
@ -1,25 +1,26 @@
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { useDrop } from 'react-dnd';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import get from 'lodash/get';
|
||||
import { useNotification } from '@strapi/helper-plugin';
|
||||
|
||||
import { useNotification, useCMEditViewDataManager } 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 { getMaxTempKey, getTrad } from '../../utils';
|
||||
import { useContentTypeLayout } from '../../hooks';
|
||||
import ItemTypes from '../../utils/ItemTypes';
|
||||
|
||||
import ComponentInitializer from '../ComponentInitializer';
|
||||
import connect from './utils/connect';
|
||||
import select from './utils/select';
|
||||
import getComponentErrorKeys from './utils/getComponentErrorKeys';
|
||||
import DraggedItem from './DraggedItem';
|
||||
import AccordionGroupCustom from './AccordionGroupCustom';
|
||||
|
||||
import getComponentErrorKeys from './utils/getComponentErrorKeys';
|
||||
|
||||
const TextButtonCustom = styled(TextButton)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@ -33,8 +34,6 @@ const TextButtonCustom = styled(TextButton)`
|
||||
`;
|
||||
|
||||
const RepeatableComponent = ({
|
||||
addRepeatableComponentToField,
|
||||
formErrors,
|
||||
componentUid,
|
||||
componentValue,
|
||||
componentValueLength,
|
||||
@ -43,11 +42,11 @@ const RepeatableComponent = ({
|
||||
min,
|
||||
name,
|
||||
}) => {
|
||||
const { addRepeatableComponentToField, formErrors, moveComponentField } =
|
||||
useCMEditViewDataManager();
|
||||
const toggleNotification = useNotification();
|
||||
const { formatMessage } = useIntl();
|
||||
const [collapseToOpen, setCollapseToOpen] = useState('');
|
||||
const [isDraggingSibling, setIsDraggingSibling] = useState(false);
|
||||
const [, drop] = useDrop({ accept: ItemTypes.COMPONENT });
|
||||
const { getComponentLayout, components } = useContentTypeLayout();
|
||||
const componentLayoutData = useMemo(
|
||||
() => getComponentLayout(componentUid),
|
||||
@ -124,8 +123,16 @@ const RepeatableComponent = ({
|
||||
};
|
||||
}
|
||||
|
||||
const handleMoveComponentField = (newIndex, currentIndex) => {
|
||||
moveComponentField({
|
||||
name,
|
||||
newIndex,
|
||||
currentIndex,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box hasRadius ref={drop}>
|
||||
<Box hasRadius>
|
||||
<AccordionGroupCustom
|
||||
error={errorMessage}
|
||||
footer={
|
||||
@ -151,7 +158,6 @@ const RepeatableComponent = ({
|
||||
componentUid={componentUid}
|
||||
hasErrors={hasErrors}
|
||||
hasMinError={hasMinError}
|
||||
isDraggingSibling={isDraggingSibling}
|
||||
isOpen={isOpen}
|
||||
isReadOnly={isReadOnly}
|
||||
key={key}
|
||||
@ -162,10 +168,11 @@ const RepeatableComponent = ({
|
||||
setCollapseToOpen(key);
|
||||
}
|
||||
}}
|
||||
moveComponentField={handleMoveComponentField}
|
||||
parentName={name}
|
||||
schema={componentLayoutData}
|
||||
setIsDraggingSibling={setIsDraggingSibling}
|
||||
toggleCollapses={toggleCollapses}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -177,25 +184,20 @@ const RepeatableComponent = ({
|
||||
RepeatableComponent.defaultProps = {
|
||||
componentValue: null,
|
||||
componentValueLength: 0,
|
||||
formErrors: {},
|
||||
max: Infinity,
|
||||
min: 0,
|
||||
};
|
||||
|
||||
RepeatableComponent.propTypes = {
|
||||
addRepeatableComponentToField: PropTypes.func.isRequired,
|
||||
componentUid: PropTypes.string.isRequired,
|
||||
componentValue: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
componentValueLength: PropTypes.number,
|
||||
formErrors: PropTypes.object,
|
||||
isReadOnly: PropTypes.bool.isRequired,
|
||||
max: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const Memoized = memo(RepeatableComponent);
|
||||
|
||||
export default connect(Memoized, select);
|
||||
export default memo(RepeatableComponent);
|
||||
|
||||
export { RepeatableComponent };
|
||||
|
@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
function connect(WrappedComponent, select) {
|
||||
return (props) => {
|
||||
const selectors = select(props);
|
||||
|
||||
return <WrappedComponent {...props} {...selectors} />;
|
||||
};
|
||||
}
|
||||
|
||||
export default connect;
|
@ -1,12 +0,0 @@
|
||||
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
||||
|
||||
function useSelect() {
|
||||
const { addRepeatableComponentToField, formErrors } = useCMEditViewDataManager();
|
||||
|
||||
return {
|
||||
addRepeatableComponentToField,
|
||||
formErrors,
|
||||
};
|
||||
}
|
||||
|
||||
export default useSelect;
|
Loading…
x
Reference in New Issue
Block a user