mirror of
https://github.com/strapi/strapi.git
synced 2025-12-14 16:51:55 +00:00
feat(wip): add visual reordering of relations
This commit is contained in:
parent
a11193d13e
commit
6b6c3948aa
@ -515,6 +515,25 @@ const EditViewDataManagerProvider = ({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef Payload
|
||||||
|
* @type {object}
|
||||||
|
* @property {string} name - The name of the field in `modifiedData`
|
||||||
|
* @property {number} oldIndex - The relation's current index
|
||||||
|
* @property {number} newIndex - The relation's new index
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @type {(payload: Payload) => void}
|
||||||
|
*/
|
||||||
|
const reorderRelation = useCallback(({ name, oldIndex, newIndex }) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'REORDER_RELATION',
|
||||||
|
keys: name.split('.'),
|
||||||
|
oldIndex,
|
||||||
|
newIndex,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const removeComponentFromDynamicZone = useCallback(
|
const removeComponentFromDynamicZone = useCallback(
|
||||||
(dynamicZoneName, index) => {
|
(dynamicZoneName, index) => {
|
||||||
trackUsageRef.current('removeComponentFromDynamicZone');
|
trackUsageRef.current('removeComponentFromDynamicZone');
|
||||||
@ -592,6 +611,7 @@ const EditViewDataManagerProvider = ({
|
|||||||
removeComponentFromDynamicZone,
|
removeComponentFromDynamicZone,
|
||||||
removeComponentFromField,
|
removeComponentFromField,
|
||||||
removeRepeatableField,
|
removeRepeatableField,
|
||||||
|
reorderRelation,
|
||||||
slug,
|
slug,
|
||||||
triggerFormValidation,
|
triggerFormValidation,
|
||||||
updateActionAllowedFields,
|
updateActionAllowedFields,
|
||||||
|
|||||||
@ -172,15 +172,32 @@ const reducer = (state, action) =>
|
|||||||
const { id } = action;
|
const { id } = action;
|
||||||
const modifiedDataRelation = get(state, [...path]);
|
const modifiedDataRelation = get(state, [...path]);
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: before merge make this performant (e.g. 1000 relations === long time)
|
|
||||||
*/
|
|
||||||
const newRelations = modifiedDataRelation.filter((rel) => rel.id !== id);
|
const newRelations = modifiedDataRelation.filter((rel) => rel.id !== id);
|
||||||
|
|
||||||
set(draftState, path, newRelations);
|
set(draftState, path, newRelations);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'REORDER_RELATION': {
|
||||||
|
const { oldIndex, newIndex, keys } = action;
|
||||||
|
const path = ['modifiedData', ...keys];
|
||||||
|
const modifiedDataRelations = get(state, [...path]);
|
||||||
|
|
||||||
|
const currentItem = modifiedDataRelations[oldIndex];
|
||||||
|
|
||||||
|
const newRelations = [...modifiedDataRelations];
|
||||||
|
|
||||||
|
console.log(oldIndex, newIndex);
|
||||||
|
|
||||||
|
newRelations.splice(oldIndex, 1);
|
||||||
|
newRelations.splice(newIndex, 0, currentItem);
|
||||||
|
|
||||||
|
console.log(newRelations);
|
||||||
|
|
||||||
|
set(draftState, path, newRelations);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This action will be called when you open your entry (first load)
|
* This action will be called when you open your entry (first load)
|
||||||
* but also every time you press publish.
|
* but also every time you press publish.
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { Relation } from './components/Relation';
|
|||||||
import { RelationItem } from './components/RelationItem';
|
import { RelationItem } from './components/RelationItem';
|
||||||
import { RelationList } from './components/RelationList';
|
import { RelationList } from './components/RelationList';
|
||||||
import { Option } from './components/Option';
|
import { Option } from './components/Option';
|
||||||
import { RELATION_ITEM_HEIGHT } from './constants';
|
import { RELATION_GUTTER, RELATION_ITEM_HEIGHT } from './constants';
|
||||||
|
|
||||||
const LinkEllipsis = styled(Link)`
|
const LinkEllipsis = styled(Link)`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -65,6 +65,7 @@ const RelationInput = ({
|
|||||||
onRelationConnect,
|
onRelationConnect,
|
||||||
onRelationLoadMore,
|
onRelationLoadMore,
|
||||||
onRelationDisconnect,
|
onRelationDisconnect,
|
||||||
|
onRelationReorder,
|
||||||
onSearchNextPage,
|
onSearchNextPage,
|
||||||
onSearch,
|
onSearch,
|
||||||
placeholder,
|
placeholder,
|
||||||
@ -87,9 +88,11 @@ const RelationInput = ({
|
|||||||
const dynamicListHeight = useMemo(
|
const dynamicListHeight = useMemo(
|
||||||
() =>
|
() =>
|
||||||
totalNumberOfRelations > numberOfRelationsToDisplay
|
totalNumberOfRelations > numberOfRelationsToDisplay
|
||||||
? Math.min(totalNumberOfRelations, numberOfRelationsToDisplay) * RELATION_ITEM_HEIGHT +
|
? Math.min(totalNumberOfRelations, numberOfRelationsToDisplay) *
|
||||||
|
(RELATION_ITEM_HEIGHT + RELATION_GUTTER) +
|
||||||
RELATION_ITEM_HEIGHT / 2
|
RELATION_ITEM_HEIGHT / 2
|
||||||
: Math.min(totalNumberOfRelations, numberOfRelationsToDisplay) * RELATION_ITEM_HEIGHT,
|
: Math.min(totalNumberOfRelations, numberOfRelationsToDisplay) *
|
||||||
|
(RELATION_ITEM_HEIGHT + RELATION_GUTTER),
|
||||||
[totalNumberOfRelations, numberOfRelationsToDisplay]
|
[totalNumberOfRelations, numberOfRelationsToDisplay]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -143,6 +146,9 @@ const RelationInput = ({
|
|||||||
};
|
};
|
||||||
}, [paginatedRelations, relations, numberOfRelationsToDisplay, totalNumberOfRelations]);
|
}, [paginatedRelations, relations, numberOfRelationsToDisplay, totalNumberOfRelations]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --- ReactSelect Workaround START ---
|
||||||
|
*/
|
||||||
/**
|
/**
|
||||||
* This code is being isolated because it's a hack to fix a placement bug in
|
* This code is being isolated because it's a hack to fix a placement bug in
|
||||||
* `react-select` where when the options prop is updated the position of the
|
* `react-select` where when the options prop is updated the position of the
|
||||||
@ -198,12 +204,21 @@ const RelationInput = ({
|
|||||||
const handleMenuClose = () => {
|
const handleMenuClose = () => {
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* --- ReactSelect Workaround END ---
|
||||||
|
*/
|
||||||
|
|
||||||
const handleMenuOpen = () => {
|
const handleMenuOpen = () => {
|
||||||
setIsMenuOpen(true);
|
setIsMenuOpen(true);
|
||||||
onSearch();
|
onSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdatePositionOfRelation = (newIndex, currentIndex) => {
|
||||||
|
if (onRelationReorder) {
|
||||||
|
onRelationReorder(currentIndex, newIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field error={error} name={name} hint={description} id={id}>
|
<Field error={error} name={name} hint={description} id={id}>
|
||||||
<Relation
|
<Relation
|
||||||
@ -277,18 +292,24 @@ const RelationInput = ({
|
|||||||
ref={listRef}
|
ref={listRef}
|
||||||
outerRef={outerListRef}
|
outerRef={outerListRef}
|
||||||
itemCount={totalNumberOfRelations}
|
itemCount={totalNumberOfRelations}
|
||||||
itemSize={RELATION_ITEM_HEIGHT}
|
itemSize={RELATION_ITEM_HEIGHT + RELATION_GUTTER}
|
||||||
itemData={relations}
|
itemData={relations}
|
||||||
|
itemKey={(index, listData) => `${listData[index].id}-${listData[index].name}`}
|
||||||
innerElementType="ol"
|
innerElementType="ol"
|
||||||
>
|
>
|
||||||
{({ data, index, style }) => {
|
{({ data, index, style }) => {
|
||||||
const { publicationState, href, mainField, id } = data[index];
|
const { publicationState, href, mainField, id } = data[index];
|
||||||
const statusColor = publicationState === 'draft' ? 'secondary' : 'success';
|
const statusColor = publicationState === 'draft' ? 'secondary' : 'success';
|
||||||
|
const canDrag = totalNumberOfRelations > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RelationItem
|
<RelationItem
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
key={`relation-${name}-${id}`}
|
key={`relation-${name}-${id}`}
|
||||||
|
canDrag={canDrag}
|
||||||
|
id={id}
|
||||||
|
index={index}
|
||||||
|
updatePositionOfRelation={handleUpdatePositionOfRelation}
|
||||||
endAction={
|
endAction={
|
||||||
<DisconnectButton
|
<DisconnectButton
|
||||||
data-testid={`remove-relation-${id}`}
|
data-testid={`remove-relation-${id}`}
|
||||||
@ -300,7 +321,11 @@ const RelationInput = ({
|
|||||||
<Icon width="12px" as={Cross} />
|
<Icon width="12px" as={Cross} />
|
||||||
</DisconnectButton>
|
</DisconnectButton>
|
||||||
}
|
}
|
||||||
style={style}
|
style={{
|
||||||
|
...style,
|
||||||
|
bottom: style.bottom + RELATION_GUTTER,
|
||||||
|
height: style.height - RELATION_GUTTER,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<BoxEllipsis minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
|
<BoxEllipsis minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
|
||||||
<Tooltip description={mainField ?? `${id}`}>
|
<Tooltip description={mainField ?? `${id}`}>
|
||||||
@ -395,6 +420,7 @@ RelationInput.propTypes = {
|
|||||||
onRelationConnect: PropTypes.func.isRequired,
|
onRelationConnect: PropTypes.func.isRequired,
|
||||||
onRelationDisconnect: PropTypes.func.isRequired,
|
onRelationDisconnect: PropTypes.func.isRequired,
|
||||||
onRelationLoadMore: PropTypes.func.isRequired,
|
onRelationLoadMore: PropTypes.func.isRequired,
|
||||||
|
onRelationReorder: PropTypes.func.isRequired,
|
||||||
onSearch: PropTypes.func.isRequired,
|
onSearch: PropTypes.func.isRequired,
|
||||||
onSearchNextPage: PropTypes.func.isRequired,
|
onSearchNextPage: PropTypes.func.isRequired,
|
||||||
placeholder: PropTypes.string.isRequired,
|
placeholder: PropTypes.string.isRequired,
|
||||||
|
|||||||
@ -1,8 +1,15 @@
|
|||||||
import React from 'react';
|
import React, { useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
|
|
||||||
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 { IconButton } from '@strapi/design-system/IconButton';
|
||||||
|
|
||||||
|
import Drag from '@strapi/icons/Drag';
|
||||||
|
|
||||||
|
import { composeRefs } from '../../../utils';
|
||||||
|
|
||||||
const ChildrenWrapper = styled(Flex)`
|
const ChildrenWrapper = styled(Flex)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -10,21 +17,106 @@ const ChildrenWrapper = styled(Flex)`
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RelationItem = ({ children, disabled, endAction, style, ...props }) => {
|
const RELATION_ITEM_DRAG_TYPE = 'RelationItem';
|
||||||
|
|
||||||
|
export const RelationItem = ({
|
||||||
|
children,
|
||||||
|
canDrag,
|
||||||
|
disabled,
|
||||||
|
endAction,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
updatePositionOfRelation,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const relationRef = useRef(null);
|
||||||
|
|
||||||
|
const [{ handlerId }, dropRef] = useDrop({
|
||||||
|
accept: RELATION_ITEM_DRAG_TYPE,
|
||||||
|
collect(monitor) {
|
||||||
|
return {
|
||||||
|
handlerId: monitor.getHandlerId(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hover(item, monitor) {
|
||||||
|
if (!relationRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dragIndex = item.index;
|
||||||
|
const currentIndex = index;
|
||||||
|
|
||||||
|
// Don't replace items with themselves
|
||||||
|
if (dragIndex === currentIndex) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dragging upwards
|
||||||
|
if (dragIndex > currentIndex && hoverClientY > hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time to actually perform the action
|
||||||
|
updatePositionOfRelation(dragIndex, currentIndex);
|
||||||
|
|
||||||
|
item.index = currentIndex;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [{ isDragging }, dragRef] = useDrag(() => ({
|
||||||
|
type: RELATION_ITEM_DRAG_TYPE,
|
||||||
|
item: { index },
|
||||||
|
canDrag,
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const composedRefs = composeRefs(relationRef, dropRef, dragRef);
|
||||||
|
|
||||||
|
const opacity = isDragging ? 0 : 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={style} as="li">
|
<Box style={style} as="li">
|
||||||
<Flex
|
<Flex
|
||||||
|
draggable={canDrag}
|
||||||
paddingTop={2}
|
paddingTop={2}
|
||||||
paddingBottom={2}
|
paddingBottom={2}
|
||||||
paddingLeft={4}
|
paddingLeft={canDrag ? 2 : 4}
|
||||||
paddingRight={4}
|
paddingRight={4}
|
||||||
hasRadius
|
hasRadius
|
||||||
borderSize={1}
|
borderSize={1}
|
||||||
background={disabled ? 'neutral150' : 'neutral0'}
|
background={disabled ? 'neutral150' : 'neutral0'}
|
||||||
borderColor="neutral200"
|
borderColor="neutral200"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
|
ref={composedRefs}
|
||||||
|
style={{ opacity }}
|
||||||
|
data-handler-id={handlerId}
|
||||||
{...props}
|
{...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>
|
<ChildrenWrapper justifyContent="space-between">{children}</ChildrenWrapper>
|
||||||
{endAction && <Box paddingLeft={4}>{endAction}</Box>}
|
{endAction && <Box paddingLeft={4}>{endAction}</Box>}
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -33,15 +125,19 @@ export const RelationItem = ({ children, disabled, endAction, style, ...props })
|
|||||||
};
|
};
|
||||||
|
|
||||||
RelationItem.defaultProps = {
|
RelationItem.defaultProps = {
|
||||||
|
canDrag: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
endAction: undefined,
|
endAction: undefined,
|
||||||
style: undefined,
|
style: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
RelationItem.propTypes = {
|
RelationItem.propTypes = {
|
||||||
|
canDrag: PropTypes.bool,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
endAction: PropTypes.node,
|
endAction: PropTypes.node,
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
style: PropTypes.shape({
|
style: PropTypes.shape({
|
||||||
height: PropTypes.number,
|
height: PropTypes.number,
|
||||||
left: PropTypes.number,
|
left: PropTypes.number,
|
||||||
@ -49,4 +145,5 @@ RelationItem.propTypes = {
|
|||||||
right: PropTypes.number,
|
right: PropTypes.number,
|
||||||
width: PropTypes.string,
|
width: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
|
updatePositionOfRelation: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Box } from '@strapi/design-system/Box';
|
import { Box } from '@strapi/design-system/Box';
|
||||||
import { Stack } from '@strapi/design-system/Stack';
|
|
||||||
|
|
||||||
const ShadowBox = styled(Box)`
|
const ShadowBox = styled(Box)`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -37,7 +36,7 @@ const ShadowBox = styled(Box)`
|
|||||||
export const RelationList = ({ children, overflow, ...props }) => {
|
export const RelationList = ({ children, overflow, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<ShadowBox overflowDirection={overflow} {...props}>
|
<ShadowBox overflowDirection={overflow} {...props}>
|
||||||
<Stack spacing={1}>{children}</Stack>
|
{children}
|
||||||
</ShadowBox>
|
</ShadowBox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export const RELATION_ITEM_HEIGHT = 50;
|
export const RELATION_ITEM_HEIGHT = 50;
|
||||||
|
export const RELATION_GUTTER = 4;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ 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';
|
||||||
|
|
||||||
@ -37,10 +38,10 @@ export const RelationInputDataManager = ({
|
|||||||
targetModel,
|
targetModel,
|
||||||
}) => {
|
}) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { connectRelation, disconnectRelation, loadRelation, modifiedData, slug, initialData } =
|
const { connectRelation, disconnectRelation, loadRelation, slug, initialData, reorderRelation } =
|
||||||
useCMEditViewDataManager();
|
useCMEditViewDataManager();
|
||||||
|
|
||||||
const relationsFromModifiedData = get(modifiedData, name) ?? [];
|
const relationsFromModifiedData = useModifiedDataSelector(name, []);
|
||||||
|
|
||||||
const currentLastPage = Math.ceil(relationsFromModifiedData.length / RELATIONS_TO_DISPLAY);
|
const currentLastPage = Math.ceil(relationsFromModifiedData.length / RELATIONS_TO_DISPLAY);
|
||||||
|
|
||||||
@ -131,6 +132,19 @@ export const RelationInputDataManager = ({
|
|||||||
search.fetchNextPage();
|
search.fetchNextPage();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} currentIndex
|
||||||
|
* @param {number} oldIndex
|
||||||
|
*/
|
||||||
|
const handleRelationReorder = (oldIndex, newIndex) => {
|
||||||
|
reorderRelation({
|
||||||
|
name,
|
||||||
|
newIndex,
|
||||||
|
oldIndex,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!isFieldAllowed && isCreatingEntry) ||
|
(!isFieldAllowed && isCreatingEntry) ||
|
||||||
(!isCreatingEntry && !isFieldAllowed && !isFieldReadable)
|
(!isCreatingEntry && !isFieldAllowed && !isFieldReadable)
|
||||||
@ -194,9 +208,10 @@ export const RelationInputDataManager = ({
|
|||||||
defaultMessage: 'No relations available',
|
defaultMessage: 'No relations available',
|
||||||
})}
|
})}
|
||||||
numberOfRelationsToDisplay={RELATIONS_TO_DISPLAY}
|
numberOfRelationsToDisplay={RELATIONS_TO_DISPLAY}
|
||||||
onRelationConnect={(relation) => handleRelationConnect(relation)}
|
onRelationConnect={handleRelationConnect}
|
||||||
onRelationDisconnect={(relation) => handleRelationDisconnect(relation)}
|
onRelationDisconnect={handleRelationDisconnect}
|
||||||
onRelationLoadMore={() => handleRelationLoadMore()}
|
onRelationLoadMore={handleRelationLoadMore}
|
||||||
|
onRelationReorder={handleRelationReorder}
|
||||||
onSearch={(term) => handleSearch(term)}
|
onSearch={(term) => handleSearch(term)}
|
||||||
onSearchNextPage={() => handleSearchMore()}
|
onSearchNextPage={() => handleSearchMore()}
|
||||||
placeholder={formatMessage(
|
placeholder={formatMessage(
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
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');
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* @typedef PossibleRef<T>
|
||||||
|
* @type {React.Ref<T> | undefined;}
|
||||||
|
*
|
||||||
|
* @typedef setRef
|
||||||
|
* @type {<T>(ref: PossibleRef<T>, value: T) => React.RefCallback<T>}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {setRef}
|
||||||
|
*/
|
||||||
|
const setRef = (ref, value) => {
|
||||||
|
if (typeof ref === 'function') {
|
||||||
|
ref(value);
|
||||||
|
} else if (ref !== null && ref !== undefined) {
|
||||||
|
ref.current = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility to compose multiple refs together
|
||||||
|
* Accepts callback refs and RefObject(s)
|
||||||
|
*
|
||||||
|
* @type {<T>(...refs: PossibleRef<T>[]) => (node: T) => void}
|
||||||
|
*/
|
||||||
|
export const composeRefs = (...refs) => {
|
||||||
|
return (node) => refs.forEach((ref) => setRef(ref, node));
|
||||||
|
};
|
||||||
@ -1,13 +1,20 @@
|
|||||||
export { default as arrayMoveItem } from './arrayMoveItem';
|
export { default as arrayMoveItem } from './arrayMoveItem';
|
||||||
|
|
||||||
export { default as checkIfAttributeIsDisplayable } from './checkIfAttributeIsDisplayable';
|
export { default as checkIfAttributeIsDisplayable } from './checkIfAttributeIsDisplayable';
|
||||||
|
export { composeRefs } from './composeRefs';
|
||||||
export { default as createDefaultForm } from './createDefaultForm';
|
export { default as createDefaultForm } from './createDefaultForm';
|
||||||
|
|
||||||
export { default as formatLayoutToApi } from './formatLayoutToApi';
|
export { default as formatLayoutToApi } from './formatLayoutToApi';
|
||||||
|
|
||||||
export { default as generatePermissionsObject } from './generatePermissionsObject';
|
export { default as generatePermissionsObject } from './generatePermissionsObject';
|
||||||
export { default as getFieldName } from './getFieldName';
|
export { default as getFieldName } from './getFieldName';
|
||||||
export { default as getMaxTempKey } from './getMaxTempKey';
|
export { default as getMaxTempKey } from './getMaxTempKey';
|
||||||
export { default as getRequestUrl } from './getRequestUrl';
|
export { default as getRequestUrl } from './getRequestUrl';
|
||||||
export { default as getTrad } from './getTrad';
|
export { default as getTrad } from './getTrad';
|
||||||
|
|
||||||
export { default as ItemTypes } from './ItemTypes';
|
export { default as ItemTypes } from './ItemTypes';
|
||||||
|
|
||||||
export { default as mergeMetasWithSchema } from './mergeMetasWithSchema';
|
export { default as mergeMetasWithSchema } from './mergeMetasWithSchema';
|
||||||
|
|
||||||
export { default as removeKeyInObject } from './removeKeyInObject';
|
export { default as removeKeyInObject } from './removeKeyInObject';
|
||||||
export { default as removePasswordFieldsFromData } from './removePasswordFieldsFromData';
|
export { default as removePasswordFieldsFromData } from './removePasswordFieldsFromData';
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { composeRefs } from '../composeRefs';
|
||||||
|
|
||||||
|
describe('composeRefs', () => {
|
||||||
|
it('given the ref is a function it should call those functions with the node value', () => {
|
||||||
|
const ref1 = jest.fn();
|
||||||
|
const ref2 = jest.fn();
|
||||||
|
const ref3 = jest.fn();
|
||||||
|
const node = 'I am a node';
|
||||||
|
|
||||||
|
const composedRefs = composeRefs(ref1, ref2, ref3);
|
||||||
|
|
||||||
|
composedRefs(node);
|
||||||
|
|
||||||
|
expect(ref1).toHaveBeenCalledWith(node);
|
||||||
|
expect(ref2).toHaveBeenCalledWith(node);
|
||||||
|
expect(ref3).toHaveBeenCalledWith(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is difficult because you need to be able to access the
|
||||||
|
* ref.current value from outside the component.
|
||||||
|
*/
|
||||||
|
it.todo('refs as React.useRef');
|
||||||
|
});
|
||||||
@ -108,8 +108,8 @@
|
|||||||
"qs": "6.10.1",
|
"qs": "6.10.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dnd": "^14.0.2",
|
"react-dnd": "15.1.2",
|
||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "15.1.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-error-boundary": "3.1.1",
|
"react-error-boundary": "3.1.1",
|
||||||
"react-fast-compare": "^3.2.0",
|
"react-fast-compare": "^3.2.0",
|
||||||
|
|||||||
77
yarn.lock
77
yarn.lock
@ -3747,20 +3747,30 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||||
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
||||||
|
|
||||||
"@react-dnd/asap@^4.0.0":
|
"@react-dnd/asap@4.0.0":
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651"
|
||||||
|
integrity sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==
|
||||||
|
|
||||||
|
"@react-dnd/asap@4.0.1":
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
|
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
|
||||||
integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==
|
integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==
|
||||||
|
|
||||||
"@react-dnd/invariant@^2.0.0":
|
"@react-dnd/invariant@3.0.0":
|
||||||
version "2.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e"
|
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-3.0.0.tgz#ea55db612b8be3284e87b67f1a1567595cd4c386"
|
||||||
integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==
|
integrity sha512-keberJRIqPX15IK3SWS/iO1t/kGETiL1oczKrDitAaMnQ+kpHf81l3MrRmFjvfqcnApE+izEvwM6GsyoIcpsVA==
|
||||||
|
|
||||||
"@react-dnd/shallowequal@^2.0.0":
|
"@react-dnd/invariant@3.0.1":
|
||||||
version "2.0.0"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
|
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-3.0.1.tgz#7e70be19ea21b539e8bf1da28466f4f05df2a4cc"
|
||||||
integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
|
integrity sha512-blqduwV86oiKw2Gr44wbe3pj3Z/OsXirc7ybCv9F/pLAR+Aih8F3rjeJzK0ANgtYKv5lCpkGVoZAeKitKDaD/g==
|
||||||
|
|
||||||
|
"@react-dnd/shallowequal@3.0.1":
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-3.0.1.tgz#8056fe046a8d10a275e321ec0557ae652d7a4d06"
|
||||||
|
integrity sha512-XjDVbs3ZU16CO1h5Q3Ew2RPJqmZBDE/EVf1LYp6ePEffs3V/MX9ZbL5bJr8qiK5SbGmUMuDoaFgyKacYz8prRA==
|
||||||
|
|
||||||
"@rushstack/ts-command-line@^4.7.7":
|
"@rushstack/ts-command-line@^4.7.7":
|
||||||
version "4.12.1"
|
version "4.12.1"
|
||||||
@ -10123,15 +10133,24 @@ dkim-signer@0.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
libmime "^2.0.3"
|
libmime "^2.0.3"
|
||||||
|
|
||||||
dnd-core@14.0.1:
|
dnd-core@15.1.1:
|
||||||
version "14.0.1"
|
version "15.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e"
|
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-15.1.1.tgz#b4dce2d892be2a7c9ca32ffdd545350be8d52f4f"
|
||||||
integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==
|
integrity sha512-Mtj/Sltcx7stVXzeDg4g7roTe/AmzRuIf/FYOxX6F8gULbY54w066BlErBOzQfn9RIJ3gAYLGX7wvVvoBSq7ig==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@react-dnd/asap" "^4.0.0"
|
"@react-dnd/asap" "4.0.0"
|
||||||
"@react-dnd/invariant" "^2.0.0"
|
"@react-dnd/invariant" "3.0.0"
|
||||||
redux "^4.1.1"
|
redux "^4.1.1"
|
||||||
|
|
||||||
|
dnd-core@15.1.2:
|
||||||
|
version "15.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-15.1.2.tgz#0983bce555c4985f58b731ffe1faed31e1ea7f6f"
|
||||||
|
integrity sha512-EOec1LyJUuGRFg0LDa55rSRAUe97uNVKVkUo8iyvzQlcECYTuPblVQfRWXWj1OyPseFIeebWpNmKFy0h6BcF1A==
|
||||||
|
dependencies:
|
||||||
|
"@react-dnd/asap" "4.0.1"
|
||||||
|
"@react-dnd/invariant" "3.0.1"
|
||||||
|
redux "^4.1.2"
|
||||||
|
|
||||||
dns-equal@^1.0.0:
|
dns-equal@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
|
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
|
||||||
@ -17837,6 +17856,8 @@ path-case@^2.1.0:
|
|||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5"
|
resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5"
|
||||||
integrity sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU=
|
integrity sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU=
|
||||||
|
dependencies:
|
||||||
|
no-case "^2.2.0"
|
||||||
|
|
||||||
path-dirname@^1.0.0:
|
path-dirname@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
@ -18790,21 +18811,21 @@ react-copy-to-clipboard@^5.1.0:
|
|||||||
copy-to-clipboard "^3.3.1"
|
copy-to-clipboard "^3.3.1"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
react-dnd-html5-backend@^14.0.0:
|
react-dnd-html5-backend@15.1.2:
|
||||||
version "14.1.0"
|
version "15.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f"
|
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.2.tgz#85e2c5ad57e87190495756f68f44fd89299062fb"
|
||||||
integrity sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==
|
integrity sha512-mem9QbutUF+aA2YC1y47G3ECjnYV/sCYKSnu5Jd7cbg3fLMPAwbnTf/JayYdnCH5l3eg9akD9dQt+cD0UdF8QQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
dnd-core "14.0.1"
|
dnd-core "15.1.1"
|
||||||
|
|
||||||
react-dnd@^14.0.2:
|
react-dnd@15.1.2:
|
||||||
version "14.0.5"
|
version "15.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.5.tgz#ecf264e220ae62e35634d9b941502f3fca0185ed"
|
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-15.1.2.tgz#211b30fd842326209c63f26f1bdf1bc52eef4f64"
|
||||||
integrity sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==
|
integrity sha512-EaSbMD9iFJDY/o48T3c8wn3uWU+2uxfFojhesZN3LhigJoAIvH2iOjxofSA9KbqhAKP6V9P853G6XG8JngKVtA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@react-dnd/invariant" "^2.0.0"
|
"@react-dnd/invariant" "3.0.1"
|
||||||
"@react-dnd/shallowequal" "^2.0.0"
|
"@react-dnd/shallowequal" "3.0.1"
|
||||||
dnd-core "14.0.1"
|
dnd-core "15.1.2"
|
||||||
fast-deep-equal "^3.1.3"
|
fast-deep-equal "^3.1.3"
|
||||||
hoist-non-react-statics "^3.3.2"
|
hoist-non-react-statics "^3.3.2"
|
||||||
|
|
||||||
@ -19338,7 +19359,7 @@ redent@^3.0.0:
|
|||||||
indent-string "^4.0.0"
|
indent-string "^4.0.0"
|
||||||
strip-indent "^3.0.0"
|
strip-indent "^3.0.0"
|
||||||
|
|
||||||
redux@^4.0.0, redux@^4.0.1, redux@^4.1.1:
|
redux@^4.0.0, redux@^4.0.1, redux@^4.1.1, redux@^4.1.2:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
|
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
|
||||||
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
|
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user