mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 08:19:07 +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];
|
||||
|
||||
console.log(oldIndex, newIndex);
|
||||
|
||||
newRelations.splice(oldIndex, 1);
|
||||
newRelations.splice(newIndex, 0, currentItem);
|
||||
|
||||
console.log(newRelations);
|
||||
|
||||
set(draftState, path, newRelations);
|
||||
|
||||
break;
|
||||
|
@ -293,64 +293,19 @@ const RelationInput = ({
|
||||
outerRef={outerListRef}
|
||||
itemCount={totalNumberOfRelations}
|
||||
itemSize={RELATION_ITEM_HEIGHT + RELATION_GUTTER}
|
||||
itemData={relations}
|
||||
itemKey={(index, listData) => `${listData[index].id}-${listData[index].name}`}
|
||||
itemData={{
|
||||
disabled,
|
||||
labelDisconnectRelation,
|
||||
onRelationDisconnect,
|
||||
publicationStateTranslations,
|
||||
relations,
|
||||
totalNumberOfRelations,
|
||||
updatePositionOfRelation: handleUpdatePositionOfRelation,
|
||||
}}
|
||||
itemKey={(index, { relations: relationsItems }) => relationsItems[index].id}
|
||||
innerElementType="ol"
|
||||
>
|
||||
{({ data, index, style }) => {
|
||||
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>
|
||||
);
|
||||
}}
|
||||
{ListItem}
|
||||
</List>
|
||||
</RelationList>
|
||||
{(description || error) && (
|
||||
@ -434,4 +389,99 @@ RelationInput.propTypes = {
|
||||
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;
|
||||
|
@ -51,22 +51,11 @@ export const RelationItem = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine rectangle on screen
|
||||
const hoverBoundingRect = relationRef.current.getBoundingClientRect();
|
||||
|
||||
// Get vertical middle
|
||||
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 < currentIndex && hoverClientY < hoverMiddleY) {
|
||||
return;
|
||||
@ -84,42 +73,57 @@ export const RelationItem = ({
|
||||
},
|
||||
});
|
||||
|
||||
const [{ isDragging }, dragRef] = useDrag(() => ({
|
||||
const [{ isDragging }, dragRef, dragPreviewRef] = useDrag({
|
||||
type: RELATION_ITEM_DRAG_TYPE,
|
||||
item: { index },
|
||||
item: { index, id },
|
||||
canDrag,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
const composedRefs = composeRefs(relationRef, dropRef, dragRef);
|
||||
|
||||
const opacity = isDragging ? 0 : 1;
|
||||
const composedRefs = composeRefs(relationRef, dragRef);
|
||||
|
||||
return (
|
||||
<Box style={style} as="li">
|
||||
<Flex
|
||||
draggable={canDrag}
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
paddingLeft={canDrag ? 2 : 4}
|
||||
paddingRight={4}
|
||||
hasRadius
|
||||
borderSize={1}
|
||||
background={disabled ? 'neutral150' : 'neutral0'}
|
||||
borderColor="neutral200"
|
||||
justifyContent="space-between"
|
||||
ref={composedRefs}
|
||||
style={{ opacity }}
|
||||
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 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
|
||||
draggable={canDrag}
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
paddingLeft={canDrag ? 2 : 4}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -10,7 +10,6 @@ import { useCMEditViewDataManager, NotAllowedInput } from '@strapi/helper-plugin
|
||||
import { RelationInput } from '../RelationInput';
|
||||
|
||||
import { useRelation } from '../../hooks/useRelation';
|
||||
import { useModifiedDataSelector } from '../../hooks/useModifiedDataSelector';
|
||||
|
||||
import { getTrad } from '../../utils';
|
||||
|
||||
@ -38,10 +37,17 @@ export const RelationInputDataManager = ({
|
||||
targetModel,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { connectRelation, disconnectRelation, loadRelation, slug, initialData, reorderRelation } =
|
||||
useCMEditViewDataManager();
|
||||
const {
|
||||
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);
|
||||
|
||||
|
@ -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