feat: add visual re-ordering with mouse

This commit is contained in:
Josh 2022-11-07 10:52:14 +00:00
parent 6b6c3948aa
commit eef9fdb463
6 changed files with 159 additions and 136 deletions

View File

@ -187,13 +187,9 @@ const reducer = (state, action) =>
const newRelations = [...modifiedDataRelations]; const newRelations = [...modifiedDataRelations];
console.log(oldIndex, newIndex);
newRelations.splice(oldIndex, 1); newRelations.splice(oldIndex, 1);
newRelations.splice(newIndex, 0, currentItem); newRelations.splice(newIndex, 0, currentItem);
console.log(newRelations);
set(draftState, path, newRelations); set(draftState, path, newRelations);
break; break;

View File

@ -293,64 +293,19 @@ const RelationInput = ({
outerRef={outerListRef} outerRef={outerListRef}
itemCount={totalNumberOfRelations} itemCount={totalNumberOfRelations}
itemSize={RELATION_ITEM_HEIGHT + RELATION_GUTTER} itemSize={RELATION_ITEM_HEIGHT + RELATION_GUTTER}
itemData={relations} itemData={{
itemKey={(index, listData) => `${listData[index].id}-${listData[index].name}`} disabled,
labelDisconnectRelation,
onRelationDisconnect,
publicationStateTranslations,
relations,
totalNumberOfRelations,
updatePositionOfRelation: handleUpdatePositionOfRelation,
}}
itemKey={(index, { relations: relationsItems }) => relationsItems[index].id}
innerElementType="ol" innerElementType="ol"
> >
{({ data, index, style }) => { {ListItem}
const { publicationState, href, mainField, id } = data[index];
const statusColor = publicationState === 'draft' ? 'secondary' : 'success';
const canDrag = totalNumberOfRelations > 1;
return (
<RelationItem
disabled={disabled}
key={`relation-${name}-${id}`}
canDrag={canDrag}
id={id}
index={index}
updatePositionOfRelation={handleUpdatePositionOfRelation}
endAction={
<DisconnectButton
data-testid={`remove-relation-${id}`}
disabled={disabled}
type="button"
onClick={() => onRelationDisconnect(data[index])}
aria-label={labelDisconnectRelation}
>
<Icon width="12px" as={Cross} />
</DisconnectButton>
}
style={{
...style,
bottom: style.bottom + RELATION_GUTTER,
height: style.height - RELATION_GUTTER,
}}
>
<BoxEllipsis minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
<Tooltip description={mainField ?? `${id}`}>
{href ? (
<LinkEllipsis to={href} disabled={disabled}>
{mainField ?? id}
</LinkEllipsis>
) : (
<Typography textColor={disabled ? 'neutral600' : 'primary600'} ellipsis>
{mainField ?? id}
</Typography>
)}
</Tooltip>
</BoxEllipsis>
{publicationState && (
<Status variant={statusColor} showBullet={false} size="S">
<Typography fontWeight="bold" textColor={`${statusColor}700`}>
{publicationStateTranslations[publicationState]}
</Typography>
</Status>
)}
</RelationItem>
);
}}
</List> </List>
</RelationList> </RelationList>
{(description || error) && ( {(description || error) && (
@ -434,4 +389,99 @@ RelationInput.propTypes = {
relations: RelationsResult, relations: RelationsResult,
}; };
/**
* This is in a seperate component to enforce passing all the props the component requires to react-window
* to ensure drag & drop correctly works.
*/
const ListItem = ({ data, index, style }) => {
const {
disabled,
labelDisconnectRelation,
onRelationDisconnect,
publicationStateTranslations,
relations,
totalNumberOfRelations,
updatePositionOfRelation,
} = data;
const { publicationState, href, mainField, id } = relations[index];
const statusColor = publicationState === 'draft' ? 'secondary' : 'success';
const canDrag = totalNumberOfRelations > 1;
return (
<RelationItem
disabled={disabled}
canDrag={canDrag}
id={id}
index={index}
updatePositionOfRelation={updatePositionOfRelation}
endAction={
<DisconnectButton
data-testid={`remove-relation-${id}`}
disabled={disabled}
type="button"
onClick={() => onRelationDisconnect(data[index])}
aria-label={labelDisconnectRelation}
>
<Icon width="12px" as={Cross} />
</DisconnectButton>
}
style={{
...style,
bottom: style.bottom + RELATION_GUTTER,
height: style.height - RELATION_GUTTER,
}}
>
<BoxEllipsis minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
<Tooltip description={mainField ?? `${id}`}>
{href ? (
<LinkEllipsis to={href} disabled={disabled}>
{mainField ?? id}
</LinkEllipsis>
) : (
<Typography textColor={disabled ? 'neutral600' : 'primary600'} ellipsis>
{mainField ?? id}
</Typography>
)}
</Tooltip>
</BoxEllipsis>
{publicationState && (
<Status variant={statusColor} showBullet={false} size="S">
<Typography fontWeight="bold" textColor={`${statusColor}700`}>
{publicationStateTranslations[publicationState]}
</Typography>
</Status>
)}
</RelationItem>
);
};
ListItem.defaultProps = {
data: {},
};
ListItem.propTypes = {
data: PropTypes.shape({
disabled: PropTypes.bool.isRequired,
labelDisconnectRelation: PropTypes.string.isRequired,
onRelationDisconnect: PropTypes.func.isRequired,
publicationStateTranslations: PropTypes.shape({
draft: PropTypes.string.isRequired,
published: PropTypes.string.isRequired,
}).isRequired,
relations: PropTypes.arrayOf(
PropTypes.shape({
href: PropTypes.string,
id: PropTypes.number.isRequired,
publicationState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
mainField: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})
),
totalNumberOfRelations: PropTypes.number.isRequired,
updatePositionOfRelation: PropTypes.func.isRequired,
}),
index: PropTypes.number.isRequired,
style: PropTypes.object.isRequired,
};
export default RelationInput; export default RelationInput;

View File

@ -51,22 +51,11 @@ export const RelationItem = ({
return; return;
} }
// Determine rectangle on screen
const hoverBoundingRect = relationRef.current.getBoundingClientRect(); const hoverBoundingRect = relationRef.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 < currentIndex && hoverClientY < hoverMiddleY) { if (dragIndex < currentIndex && hoverClientY < hoverMiddleY) {
return; return;
@ -84,42 +73,57 @@ export const RelationItem = ({
}, },
}); });
const [{ isDragging }, dragRef] = useDrag(() => ({ const [{ isDragging }, dragRef, dragPreviewRef] = useDrag({
type: RELATION_ITEM_DRAG_TYPE, type: RELATION_ITEM_DRAG_TYPE,
item: { index }, item: { index, id },
canDrag, canDrag,
collect: (monitor) => ({ collect: (monitor) => ({
isDragging: monitor.isDragging(), isDragging: monitor.isDragging(),
}), }),
})); });
const composedRefs = composeRefs(relationRef, dropRef, dragRef); const composedRefs = composeRefs(relationRef, dragRef);
const opacity = isDragging ? 0 : 1;
return ( return (
<Box style={style} as="li"> <Box style={style} as="li" ref={dropRef}>
<Flex {isDragging ? (
draggable={canDrag} <Box
paddingTop={2} ref={dragPreviewRef}
paddingBottom={2} paddingTop={2}
paddingLeft={canDrag ? 2 : 4} paddingBottom={2}
paddingRight={4} paddingLeft={4}
hasRadius paddingRight={4}
borderSize={1} hasRadius
background={disabled ? 'neutral150' : 'neutral0'} borderStyle="dashed"
borderColor="neutral200" borderColor="primary600"
justifyContent="space-between" borderWidth="1px"
ref={composedRefs} background="primary100"
style={{ opacity }} height="100%"
data-handler-id={handlerId} />
{...props} ) : (
> <Flex
{/* TODO: swap this out for using children when DS is updated */} draggable={canDrag}
{canDrag ? <IconButton marginRight={1} aria-label="Drag" noBorder icon={<Drag />} /> : null} paddingTop={2}
<ChildrenWrapper justifyContent="space-between">{children}</ChildrenWrapper> paddingBottom={2}
{endAction && <Box paddingLeft={4}>{endAction}</Box>} paddingLeft={canDrag ? 2 : 4}
</Flex> paddingRight={4}
hasRadius
borderSize={1}
background={disabled ? 'neutral150' : 'neutral0'}
borderColor="neutral200"
justifyContent="space-between"
ref={composedRefs}
data-handler-id={handlerId}
{...props}
>
{/* TODO: swap this out for using children when DS is updated */}
{canDrag ? (
<IconButton marginRight={1} aria-label="Drag" noBorder icon={<Drag />} />
) : null}
<ChildrenWrapper justifyContent="space-between">{children}</ChildrenWrapper>
{endAction && <Box paddingLeft={4}>{endAction}</Box>}
</Flex>
)}
</Box> </Box>
); );
}; };

View File

@ -10,7 +10,6 @@ import { useCMEditViewDataManager, NotAllowedInput } from '@strapi/helper-plugin
import { RelationInput } from '../RelationInput'; import { RelationInput } from '../RelationInput';
import { useRelation } from '../../hooks/useRelation'; import { useRelation } from '../../hooks/useRelation';
import { useModifiedDataSelector } from '../../hooks/useModifiedDataSelector';
import { getTrad } from '../../utils'; import { getTrad } from '../../utils';
@ -38,10 +37,17 @@ export const RelationInputDataManager = ({
targetModel, targetModel,
}) => { }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { connectRelation, disconnectRelation, loadRelation, slug, initialData, reorderRelation } = const {
useCMEditViewDataManager(); connectRelation,
disconnectRelation,
loadRelation,
slug,
initialData,
modifiedData,
reorderRelation,
} = useCMEditViewDataManager();
const relationsFromModifiedData = useModifiedDataSelector(name, []); const relationsFromModifiedData = get(modifiedData, name);
const currentLastPage = Math.ceil(relationsFromModifiedData.length / RELATIONS_TO_DISPLAY); const currentLastPage = Math.ceil(relationsFromModifiedData.length / RELATIONS_TO_DISPLAY);

View File

@ -1,13 +0,0 @@
describe('useModifiedDataSelector', () => {
it.todo('should not re render when the same primitive value is returned from the modifiedData');
it.todo('should not re render when the same object value is returned from the modifiedData');
it.todo('should not re render when the same array value is returned from the modifiedData');
it.todo('should re render when a different primitive value is returned from the modifiedData');
it.todo('should re render when a different object value is returned from the modifiedData');
it.todo('should re render when a different array value is returned from the modifiedData');
});

View File

@ -1,20 +0,0 @@
import { useEffect, useState } from 'react';
import get from 'lodash/get';
import isEqual from 'react-fast-compare';
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
export const useModifiedDataSelector = (path, defaultValue) => {
const { modifiedData } = useCMEditViewDataManager();
const [value, setValue] = useState(get(modifiedData, path, defaultValue));
useEffect(() => {
const newValue = get(modifiedData, path, defaultValue);
if (!isEqual(newValue, value)) {
setValue(newValue);
}
}, [modifiedData, path, defaultValue, value]);
return value;
};