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] [shouldCheckDZErrors]
); );
const moveComponentField = useCallback((pathToComponent, dragIndex, hoverIndex) => { const moveComponentField = useCallback(({ name, newIndex, currentIndex }) => {
dispatch({ dispatch({
type: 'MOVE_COMPONENT_FIELD', type: 'MOVE_COMPONENT_FIELD',
pathToComponent, keys: name.split('.'),
dragIndex, newIndex,
hoverIndex, oldIndex: currentIndex,
}); });
}, []); }, []);

View File

@ -178,6 +178,7 @@ const reducer = (state, action) =>
break; break;
} }
case 'MOVE_COMPONENT_FIELD':
case 'REORDER_RELATION': { case 'REORDER_RELATION': {
const { oldIndex, newIndex, keys } = action; const { oldIndex, newIndex, keys } = action;
const path = ['modifiedData', ...keys]; const path = ['modifiedData', ...keys];
@ -265,25 +266,6 @@ const reducer = (state, action) =>
draftState.shouldCheckErrors = false; draftState.shouldCheckErrors = false;
break; 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_UP':
case 'MOVE_COMPONENT_DOWN': { case 'MOVE_COMPONENT_DOWN': {
const { currentIndex, dynamicZoneName, shouldCheckErrors } = action; 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'; import styled from 'styled-components';
const StyledSpan = styled.span` const StyledSpan = styled.span`
@ -9,8 +9,8 @@ const StyledSpan = styled.span`
padding: ${({ theme }) => theme.spaces[6]}; padding: ${({ theme }) => theme.spaces[6]};
`; `;
const Preview = () => { const Preview = forwardRef((_, ref) => {
return <StyledSpan padding={6} background="primary100" />; return <StyledSpan ref={ref} padding={6} background="primary100" />;
}; });
export default Preview; export default Preview;

View File

@ -1,24 +1,27 @@
/* eslint-disable import/no-cycle */ /* 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 PropTypes from 'prop-types';
import { useDrag, useDrop } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd';
import { getEmptyImage } from 'react-dnd-html5-backend'; import { getEmptyImage } from 'react-dnd-html5-backend';
import styled from 'styled-components'; import styled from 'styled-components';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import toString from 'lodash/toString'; import toString from 'lodash/toString';
import { Accordion, AccordionToggle, AccordionContent } from '@strapi/design-system/Accordion'; import { Accordion, AccordionToggle, AccordionContent } from '@strapi/design-system/Accordion';
import { Grid, GridItem } from '@strapi/design-system/Grid'; import { Grid, GridItem } from '@strapi/design-system/Grid';
import { Stack } from '@strapi/design-system/Stack'; import { Stack } from '@strapi/design-system/Stack';
import { Box } from '@strapi/design-system/Box'; import { Box } from '@strapi/design-system/Box';
import { Tooltip } from '@strapi/design-system/Tooltip'; import { Tooltip } from '@strapi/design-system/Tooltip';
import Trash from '@strapi/icons/Trash'; import Trash from '@strapi/icons/Trash';
import Drag from '@strapi/icons/Drag'; 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 Inputs from '../../Inputs';
import FieldComponent from '../../FieldComponent'; import FieldComponent from '../../FieldComponent';
import Preview from './Preview'; import Preview from './Preview';
import DraggingSibling from './DraggingSibling';
import { CustomIconButton } from './IconButtonCustoms'; import { CustomIconButton } from './IconButtonCustoms';
import { connect, select } from './utils'; import { connect, select } from './utils';
@ -48,7 +51,7 @@ const DraggedItem = ({
// Errors are retrieved from the AccordionGroupCustom cloneElement // Errors are retrieved from the AccordionGroupCustom cloneElement
hasErrorMessage, hasErrorMessage,
hasErrors, hasErrors,
isDraggingSibling, index,
isOpen, isOpen,
isReadOnly, isReadOnly,
onClickToggle, onClickToggle,
@ -57,90 +60,70 @@ const DraggedItem = ({
// Retrieved from the select function // Retrieved from the select function
moveComponentField, moveComponentField,
removeRepeatableField, removeRepeatableField,
setIsDraggingSibling,
triggerFormValidation, triggerFormValidation,
// checkFormErrors,
displayedValue, displayedValue,
}) => { }) => {
const dragRef = useRef(null); const accordionRef = useRef(null);
const dropRef = useRef(null); const boxRef = useRef(null);
const [, forceRerenderAfterDnd] = useState(false);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const fields = schema.layouts.edit; const fields = schema.layouts.edit;
const [, drop] = useDrop({ const [{ handlerId }, dropRef] = useDrop({
accept: ItemTypes.COMPONENT, accept: ItemTypes.COMPONENT,
canDrop() { collect(monitor) {
return false; return {
handlerId: monitor.getHandlerId(),
};
}, },
hover(item, monitor) { hover(item, monitor) {
if (!dropRef.current) { if (!boxRef.current) {
return; return;
} }
const dragPath = item.originalPath; const dragIndex = item.index;
const hoverPath = componentFieldName; const currentIndex = index;
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);
// Don't replace items with themselves // Don't replace items with themselves
if (dragIndex === hoverIndex) { if (dragIndex === currentIndex) {
return; return;
} }
// Determine rectangle on screen const hoverBoundingRect = boxRef.current.getBoundingClientRect();
const hoverBoundingRect = dropRef.current.getBoundingClientRect();
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset(); const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.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 // Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { if (dragIndex < currentIndex && hoverClientY < hoverMiddleY) {
return; 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, type: ItemTypes.COMPONENT,
item() { item() {
// Close all collapses // Close all collapses
toggleCollapses(-1); toggleCollapses(-1);
return { return {
displayedValue, index,
originalPath: componentFieldName,
}; };
}, },
end() { end() {
// Update the errors // Update the errors
triggerFormValidation(); triggerFormValidation();
setIsDraggingSibling(false); // setIsDraggingSibling(false);
}, },
collect: (monitor) => ({ collect: (monitor) => ({
isDragging: monitor.isDragging(), isDragging: monitor.isDragging(),
@ -148,43 +131,20 @@ const DraggedItem = ({
}); });
useEffect(() => { useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: false }); previewRef(getEmptyImage(), { captureDraggingState: false });
}, [preview]); }, [previewRef]);
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),
};
const accordionTitle = toString(displayedValue); const accordionTitle = toString(displayedValue);
const accordionHasError = hasErrors ? 'error' : undefined; const accordionHasError = hasErrors ? 'error' : undefined;
return ( const composedAccordionRefs = composeRefs(accordionRef, dragRef);
<Box ref={refs ? refs.dropRef : null}> const composedBoxRefs = composeRefs(boxRef, dropRef);
{isDragging && <Preview />}
{!isDragging && isDraggingSibling && (
<DraggingSibling displayedValue={accordionTitle} componentFieldName={componentFieldName} />
)}
{!isDragging && !isDraggingSibling && ( return (
<Box ref={composedBoxRefs} style={{ border: '1px solid red' }}>
{isDragging ? (
<Preview ref={previewRef} />
) : (
<Accordion <Accordion
error={accordionHasError} error={accordionHasError}
hasErrorMessage={hasErrorMessage} hasErrorMessage={hasErrorMessage}
@ -220,8 +180,9 @@ const DraggedItem = ({
<DragButton <DragButton
role="button" role="button"
tabIndex={-1} tabIndex={-1}
ref={refs.dragRef} ref={composedAccordionRefs}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
data-handler-id={handlerId}
> >
<Drag /> <Drag />
</DragButton> </DragButton>
@ -290,9 +251,7 @@ const DraggedItem = ({
DraggedItem.defaultProps = { DraggedItem.defaultProps = {
componentUid: undefined, componentUid: undefined,
isDraggingSibling: false,
isOpen: false, isOpen: false,
setIsDraggingSibling() {},
toggleCollapses() {}, toggleCollapses() {},
}; };
@ -301,7 +260,7 @@ DraggedItem.propTypes = {
componentUid: PropTypes.string, componentUid: PropTypes.string,
hasErrorMessage: PropTypes.bool.isRequired, hasErrorMessage: PropTypes.bool.isRequired,
hasErrors: PropTypes.bool.isRequired, hasErrors: PropTypes.bool.isRequired,
isDraggingSibling: PropTypes.bool, index: PropTypes.number.isRequired,
isOpen: PropTypes.bool, isOpen: PropTypes.bool,
isReadOnly: PropTypes.bool.isRequired, isReadOnly: PropTypes.bool.isRequired,
onClickToggle: PropTypes.func.isRequired, onClickToggle: PropTypes.func.isRequired,
@ -309,9 +268,7 @@ DraggedItem.propTypes = {
toggleCollapses: PropTypes.func, toggleCollapses: PropTypes.func,
moveComponentField: PropTypes.func.isRequired, moveComponentField: PropTypes.func.isRequired,
removeRepeatableField: PropTypes.func.isRequired, removeRepeatableField: PropTypes.func.isRequired,
setIsDraggingSibling: PropTypes.func,
triggerFormValidation: PropTypes.func.isRequired, triggerFormValidation: PropTypes.func.isRequired,
// checkFormErrors: PropTypes.func.isRequired,
displayedValue: PropTypes.string.isRequired, displayedValue: PropTypes.string.isRequired,
}; };

View File

@ -3,13 +3,7 @@ import { get, toString } from 'lodash';
import { useCMEditViewDataManager } from '@strapi/helper-plugin'; import { useCMEditViewDataManager } from '@strapi/helper-plugin';
function useSelect({ schema, componentFieldName }) { function useSelect({ schema, componentFieldName }) {
const { const { modifiedData, removeRepeatableField, triggerFormValidation } = useCMEditViewDataManager();
checkFormErrors,
modifiedData,
moveComponentField,
removeRepeatableField,
triggerFormValidation,
} = useCMEditViewDataManager();
const mainField = useMemo(() => get(schema, ['settings', 'mainField'], 'id'), [schema]); const mainField = useMemo(() => get(schema, ['settings', 'mainField'], 'id'), [schema]);
const displayedValue = toString( const displayedValue = toString(
@ -19,8 +13,6 @@ function useSelect({ schema, componentFieldName }) {
return { return {
displayedValue, displayedValue,
mainField, mainField,
checkFormErrors,
moveComponentField,
removeRepeatableField, removeRepeatableField,
schema, schema,
triggerFormValidation, triggerFormValidation,

View File

@ -1,25 +1,26 @@
import React, { memo, useCallback, useMemo, useState } from 'react';
/* eslint-disable import/no-cycle */ /* eslint-disable import/no-cycle */
import { useDrop } from 'react-dnd'; import React, { memo, useCallback, useMemo, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import styled from 'styled-components'; import styled from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import get from 'lodash/get'; 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 { Box } from '@strapi/design-system/Box';
import { Flex } from '@strapi/design-system/Flex'; import { Flex } from '@strapi/design-system/Flex';
import { TextButton } from '@strapi/design-system/TextButton'; import { TextButton } from '@strapi/design-system/TextButton';
import Plus from '@strapi/icons/Plus'; import Plus from '@strapi/icons/Plus';
import { getMaxTempKey, getTrad } from '../../utils'; import { getMaxTempKey, getTrad } from '../../utils';
import { useContentTypeLayout } from '../../hooks'; import { useContentTypeLayout } from '../../hooks';
import ItemTypes from '../../utils/ItemTypes';
import ComponentInitializer from '../ComponentInitializer'; import ComponentInitializer from '../ComponentInitializer';
import connect from './utils/connect';
import select from './utils/select';
import getComponentErrorKeys from './utils/getComponentErrorKeys';
import DraggedItem from './DraggedItem'; import DraggedItem from './DraggedItem';
import AccordionGroupCustom from './AccordionGroupCustom'; import AccordionGroupCustom from './AccordionGroupCustom';
import getComponentErrorKeys from './utils/getComponentErrorKeys';
const TextButtonCustom = styled(TextButton)` const TextButtonCustom = styled(TextButton)`
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -33,8 +34,6 @@ const TextButtonCustom = styled(TextButton)`
`; `;
const RepeatableComponent = ({ const RepeatableComponent = ({
addRepeatableComponentToField,
formErrors,
componentUid, componentUid,
componentValue, componentValue,
componentValueLength, componentValueLength,
@ -43,11 +42,11 @@ const RepeatableComponent = ({
min, min,
name, name,
}) => { }) => {
const { addRepeatableComponentToField, formErrors, moveComponentField } =
useCMEditViewDataManager();
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [collapseToOpen, setCollapseToOpen] = useState(''); const [collapseToOpen, setCollapseToOpen] = useState('');
const [isDraggingSibling, setIsDraggingSibling] = useState(false);
const [, drop] = useDrop({ accept: ItemTypes.COMPONENT });
const { getComponentLayout, components } = useContentTypeLayout(); const { getComponentLayout, components } = useContentTypeLayout();
const componentLayoutData = useMemo( const componentLayoutData = useMemo(
() => getComponentLayout(componentUid), () => getComponentLayout(componentUid),
@ -124,8 +123,16 @@ const RepeatableComponent = ({
}; };
} }
const handleMoveComponentField = (newIndex, currentIndex) => {
moveComponentField({
name,
newIndex,
currentIndex,
});
};
return ( return (
<Box hasRadius ref={drop}> <Box hasRadius>
<AccordionGroupCustom <AccordionGroupCustom
error={errorMessage} error={errorMessage}
footer={ footer={
@ -151,7 +158,6 @@ const RepeatableComponent = ({
componentUid={componentUid} componentUid={componentUid}
hasErrors={hasErrors} hasErrors={hasErrors}
hasMinError={hasMinError} hasMinError={hasMinError}
isDraggingSibling={isDraggingSibling}
isOpen={isOpen} isOpen={isOpen}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
key={key} key={key}
@ -162,10 +168,11 @@ const RepeatableComponent = ({
setCollapseToOpen(key); setCollapseToOpen(key);
} }
}} }}
moveComponentField={handleMoveComponentField}
parentName={name} parentName={name}
schema={componentLayoutData} schema={componentLayoutData}
setIsDraggingSibling={setIsDraggingSibling}
toggleCollapses={toggleCollapses} toggleCollapses={toggleCollapses}
index={index}
/> />
); );
})} })}
@ -177,25 +184,20 @@ const RepeatableComponent = ({
RepeatableComponent.defaultProps = { RepeatableComponent.defaultProps = {
componentValue: null, componentValue: null,
componentValueLength: 0, componentValueLength: 0,
formErrors: {},
max: Infinity, max: Infinity,
min: 0, min: 0,
}; };
RepeatableComponent.propTypes = { RepeatableComponent.propTypes = {
addRepeatableComponentToField: PropTypes.func.isRequired,
componentUid: PropTypes.string.isRequired, componentUid: PropTypes.string.isRequired,
componentValue: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), componentValue: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
componentValueLength: PropTypes.number, componentValueLength: PropTypes.number,
formErrors: PropTypes.object,
isReadOnly: PropTypes.bool.isRequired, isReadOnly: PropTypes.bool.isRequired,
max: PropTypes.number, max: PropTypes.number,
min: PropTypes.number, min: PropTypes.number,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
}; };
const Memoized = memo(RepeatableComponent); export default memo(RepeatableComponent);
export default connect(Memoized, select);
export { 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;