mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 16:29:34 +00:00
feat: add visual re-ordering with mouse
This commit is contained in:
parent
6b6c3948aa
commit
eef9fdb463
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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,21 +73,34 @@ 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}>
|
||||||
|
{isDragging ? (
|
||||||
|
<Box
|
||||||
|
ref={dragPreviewRef}
|
||||||
|
paddingTop={2}
|
||||||
|
paddingBottom={2}
|
||||||
|
paddingLeft={4}
|
||||||
|
paddingRight={4}
|
||||||
|
hasRadius
|
||||||
|
borderStyle="dashed"
|
||||||
|
borderColor="primary600"
|
||||||
|
borderWidth="1px"
|
||||||
|
background="primary100"
|
||||||
|
height="100%"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Flex
|
<Flex
|
||||||
draggable={canDrag}
|
draggable={canDrag}
|
||||||
paddingTop={2}
|
paddingTop={2}
|
||||||
@ -111,15 +113,17 @@ export const RelationItem = ({
|
|||||||
borderColor="neutral200"
|
borderColor="neutral200"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
ref={composedRefs}
|
ref={composedRefs}
|
||||||
style={{ opacity }}
|
|
||||||
data-handler-id={handlerId}
|
data-handler-id={handlerId}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{/* TODO: swap this out for using children when DS is updated */}
|
{/* TODO: swap this out for using children when DS is updated */}
|
||||||
{canDrag ? <IconButton marginRight={1} aria-label="Drag" noBorder icon={<Drag />} /> : null}
|
{canDrag ? (
|
||||||
|
<IconButton marginRight={1} aria-label="Drag" noBorder icon={<Drag />} />
|
||||||
|
) : null}
|
||||||
<ChildrenWrapper justifyContent="space-between">{children}</ChildrenWrapper>
|
<ChildrenWrapper justifyContent="space-between">{children}</ChildrenWrapper>
|
||||||
{endAction && <Box paddingLeft={4}>{endAction}</Box>}
|
{endAction && <Box paddingLeft={4}>{endAction}</Box>}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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');
|
|
||||||
});
|
|
@ -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;
|
|
||||||
};
|
|
Loading…
x
Reference in New Issue
Block a user