refactor: repeatableComponents (make it simpler)

This commit is contained in:
Josh 2022-11-07 11:29:03 +00:00
parent eef9fdb463
commit ab5dac4ca2
9 changed files with 77 additions and 239 deletions

View File

@ -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,
});
}, []);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,
};

View File

@ -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,

View File

@ -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 };

View File

@ -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;

View File

@ -1,12 +0,0 @@
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
function useSelect() {
const { addRepeatableComponentToField, formErrors } = useCMEditViewDataManager();
return {
addRepeatableComponentToField,
formErrors,
};
}
export default useSelect;