feat(wip): add visual reordering of relations

This commit is contained in:
Josh 2022-11-04 17:36:35 +00:00
parent a11193d13e
commit 6b6c3948aa
14 changed files with 336 additions and 48 deletions

View File

@ -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(
(dynamicZoneName, index) => {
trackUsageRef.current('removeComponentFromDynamicZone');
@ -592,6 +611,7 @@ const EditViewDataManagerProvider = ({
removeComponentFromDynamicZone,
removeComponentFromField,
removeRepeatableField,
reorderRelation,
slug,
triggerFormValidation,
updateActionAllowedFields,

View File

@ -172,15 +172,32 @@ const reducer = (state, action) =>
const { id } = action;
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);
set(draftState, path, newRelations);
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)
* but also every time you press publish.

View File

@ -20,7 +20,7 @@ import { Relation } from './components/Relation';
import { RelationItem } from './components/RelationItem';
import { RelationList } from './components/RelationList';
import { Option } from './components/Option';
import { RELATION_ITEM_HEIGHT } from './constants';
import { RELATION_GUTTER, RELATION_ITEM_HEIGHT } from './constants';
const LinkEllipsis = styled(Link)`
white-space: nowrap;
@ -65,6 +65,7 @@ const RelationInput = ({
onRelationConnect,
onRelationLoadMore,
onRelationDisconnect,
onRelationReorder,
onSearchNextPage,
onSearch,
placeholder,
@ -87,9 +88,11 @@ const RelationInput = ({
const dynamicListHeight = useMemo(
() =>
totalNumberOfRelations > numberOfRelationsToDisplay
? Math.min(totalNumberOfRelations, numberOfRelationsToDisplay) * RELATION_ITEM_HEIGHT +
? Math.min(totalNumberOfRelations, numberOfRelationsToDisplay) *
(RELATION_ITEM_HEIGHT + RELATION_GUTTER) +
RELATION_ITEM_HEIGHT / 2
: Math.min(totalNumberOfRelations, numberOfRelationsToDisplay) * RELATION_ITEM_HEIGHT,
: Math.min(totalNumberOfRelations, numberOfRelationsToDisplay) *
(RELATION_ITEM_HEIGHT + RELATION_GUTTER),
[totalNumberOfRelations, numberOfRelationsToDisplay]
);
@ -143,6 +146,9 @@ const RelationInput = ({
};
}, [paginatedRelations, relations, numberOfRelationsToDisplay, totalNumberOfRelations]);
/**
* --- ReactSelect Workaround START ---
*/
/**
* 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
@ -198,12 +204,21 @@ const RelationInput = ({
const handleMenuClose = () => {
setIsMenuOpen(false);
};
/**
* --- ReactSelect Workaround END ---
*/
const handleMenuOpen = () => {
setIsMenuOpen(true);
onSearch();
};
const handleUpdatePositionOfRelation = (newIndex, currentIndex) => {
if (onRelationReorder) {
onRelationReorder(currentIndex, newIndex);
}
};
return (
<Field error={error} name={name} hint={description} id={id}>
<Relation
@ -277,18 +292,24 @@ const RelationInput = ({
ref={listRef}
outerRef={outerListRef}
itemCount={totalNumberOfRelations}
itemSize={RELATION_ITEM_HEIGHT}
itemSize={RELATION_ITEM_HEIGHT + RELATION_GUTTER}
itemData={relations}
itemKey={(index, listData) => `${listData[index].id}-${listData[index].name}`}
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}`}
@ -300,7 +321,11 @@ const RelationInput = ({
<Icon width="12px" as={Cross} />
</DisconnectButton>
}
style={style}
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}`}>
@ -395,6 +420,7 @@ RelationInput.propTypes = {
onRelationConnect: PropTypes.func.isRequired,
onRelationDisconnect: PropTypes.func.isRequired,
onRelationLoadMore: PropTypes.func.isRequired,
onRelationReorder: PropTypes.func.isRequired,
onSearch: PropTypes.func.isRequired,
onSearchNextPage: PropTypes.func.isRequired,
placeholder: PropTypes.string.isRequired,

View File

@ -1,8 +1,15 @@
import React from 'react';
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { useDrag, useDrop } from 'react-dnd';
import { Box } from '@strapi/design-system/Box';
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)`
width: 100%;
@ -10,21 +17,106 @@ const ChildrenWrapper = styled(Flex)`
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 (
<Box style={style} as="li">
<Flex
draggable={canDrag}
paddingTop={2}
paddingBottom={2}
paddingLeft={4}
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>
@ -33,15 +125,19 @@ export const RelationItem = ({ children, disabled, endAction, style, ...props })
};
RelationItem.defaultProps = {
canDrag: false,
disabled: false,
endAction: undefined,
style: undefined,
};
RelationItem.propTypes = {
canDrag: PropTypes.bool,
children: PropTypes.node.isRequired,
disabled: PropTypes.bool,
endAction: PropTypes.node,
id: PropTypes.number.isRequired,
index: PropTypes.number.isRequired,
style: PropTypes.shape({
height: PropTypes.number,
left: PropTypes.number,
@ -49,4 +145,5 @@ RelationItem.propTypes = {
right: PropTypes.number,
width: PropTypes.string,
}),
updatePositionOfRelation: PropTypes.func.isRequired,
};

View File

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Box } from '@strapi/design-system/Box';
import { Stack } from '@strapi/design-system/Stack';
const ShadowBox = styled(Box)`
position: relative;
@ -37,7 +36,7 @@ const ShadowBox = styled(Box)`
export const RelationList = ({ children, overflow, ...props }) => {
return (
<ShadowBox overflowDirection={overflow} {...props}>
<Stack spacing={1}>{children}</Stack>
{children}
</ShadowBox>
);
};

View File

@ -1 +1,2 @@
export const RELATION_ITEM_HEIGHT = 50;
export const RELATION_GUTTER = 4;

View File

@ -10,6 +10,7 @@ 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';
@ -37,10 +38,10 @@ export const RelationInputDataManager = ({
targetModel,
}) => {
const { formatMessage } = useIntl();
const { connectRelation, disconnectRelation, loadRelation, modifiedData, slug, initialData } =
const { connectRelation, disconnectRelation, loadRelation, slug, initialData, reorderRelation } =
useCMEditViewDataManager();
const relationsFromModifiedData = get(modifiedData, name) ?? [];
const relationsFromModifiedData = useModifiedDataSelector(name, []);
const currentLastPage = Math.ceil(relationsFromModifiedData.length / RELATIONS_TO_DISPLAY);
@ -131,6 +132,19 @@ export const RelationInputDataManager = ({
search.fetchNextPage();
};
/**
*
* @param {number} currentIndex
* @param {number} oldIndex
*/
const handleRelationReorder = (oldIndex, newIndex) => {
reorderRelation({
name,
newIndex,
oldIndex,
});
};
if (
(!isFieldAllowed && isCreatingEntry) ||
(!isCreatingEntry && !isFieldAllowed && !isFieldReadable)
@ -194,9 +208,10 @@ export const RelationInputDataManager = ({
defaultMessage: 'No relations available',
})}
numberOfRelationsToDisplay={RELATIONS_TO_DISPLAY}
onRelationConnect={(relation) => handleRelationConnect(relation)}
onRelationDisconnect={(relation) => handleRelationDisconnect(relation)}
onRelationLoadMore={() => handleRelationLoadMore()}
onRelationConnect={handleRelationConnect}
onRelationDisconnect={handleRelationDisconnect}
onRelationLoadMore={handleRelationLoadMore}
onRelationReorder={handleRelationReorder}
onSearch={(term) => handleSearch(term)}
onSearchNextPage={() => handleSearchMore()}
placeholder={formatMessage(

View File

@ -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');
});

View File

@ -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;
};

View File

@ -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));
};

View File

@ -1,13 +1,20 @@
export { default as arrayMoveItem } from './arrayMoveItem';
export { default as checkIfAttributeIsDisplayable } from './checkIfAttributeIsDisplayable';
export { composeRefs } from './composeRefs';
export { default as createDefaultForm } from './createDefaultForm';
export { default as formatLayoutToApi } from './formatLayoutToApi';
export { default as generatePermissionsObject } from './generatePermissionsObject';
export { default as getFieldName } from './getFieldName';
export { default as getMaxTempKey } from './getMaxTempKey';
export { default as getRequestUrl } from './getRequestUrl';
export { default as getTrad } from './getTrad';
export { default as ItemTypes } from './ItemTypes';
export { default as mergeMetasWithSchema } from './mergeMetasWithSchema';
export { default as removeKeyInObject } from './removeKeyInObject';
export { default as removePasswordFieldsFromData } from './removePasswordFieldsFromData';

View File

@ -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');
});

View File

@ -108,8 +108,8 @@
"qs": "6.10.1",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.1.0",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dnd": "15.1.2",
"react-dnd-html5-backend": "15.1.2",
"react-dom": "^17.0.2",
"react-error-boundary": "3.1.1",
"react-fast-compare": "^3.2.0",

View File

@ -3747,20 +3747,30 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
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"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==
"@react-dnd/invariant@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e"
integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==
"@react-dnd/invariant@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-3.0.0.tgz#ea55db612b8be3284e87b67f1a1567595cd4c386"
integrity sha512-keberJRIqPX15IK3SWS/iO1t/kGETiL1oczKrDitAaMnQ+kpHf81l3MrRmFjvfqcnApE+izEvwM6GsyoIcpsVA==
"@react-dnd/shallowequal@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
"@react-dnd/invariant@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-3.0.1.tgz#7e70be19ea21b539e8bf1da28466f4f05df2a4cc"
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":
version "4.12.1"
@ -10123,15 +10133,24 @@ dkim-signer@0.2.2:
dependencies:
libmime "^2.0.3"
dnd-core@14.0.1:
version "14.0.1"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e"
integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==
dnd-core@15.1.1:
version "15.1.1"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-15.1.1.tgz#b4dce2d892be2a7c9ca32ffdd545350be8d52f4f"
integrity sha512-Mtj/Sltcx7stVXzeDg4g7roTe/AmzRuIf/FYOxX6F8gULbY54w066BlErBOzQfn9RIJ3gAYLGX7wvVvoBSq7ig==
dependencies:
"@react-dnd/asap" "^4.0.0"
"@react-dnd/invariant" "^2.0.0"
"@react-dnd/asap" "4.0.0"
"@react-dnd/invariant" "3.0.0"
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:
version "1.0.0"
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"
resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5"
integrity sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU=
dependencies:
no-case "^2.2.0"
path-dirname@^1.0.0:
version "1.0.2"
@ -18790,21 +18811,21 @@ react-copy-to-clipboard@^5.1.0:
copy-to-clipboard "^3.3.1"
prop-types "^15.8.1"
react-dnd-html5-backend@^14.0.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f"
integrity sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==
react-dnd-html5-backend@15.1.2:
version "15.1.2"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.2.tgz#85e2c5ad57e87190495756f68f44fd89299062fb"
integrity sha512-mem9QbutUF+aA2YC1y47G3ECjnYV/sCYKSnu5Jd7cbg3fLMPAwbnTf/JayYdnCH5l3eg9akD9dQt+cD0UdF8QQ==
dependencies:
dnd-core "14.0.1"
dnd-core "15.1.1"
react-dnd@^14.0.2:
version "14.0.5"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.5.tgz#ecf264e220ae62e35634d9b941502f3fca0185ed"
integrity sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==
react-dnd@15.1.2:
version "15.1.2"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-15.1.2.tgz#211b30fd842326209c63f26f1bdf1bc52eef4f64"
integrity sha512-EaSbMD9iFJDY/o48T3c8wn3uWU+2uxfFojhesZN3LhigJoAIvH2iOjxofSA9KbqhAKP6V9P853G6XG8JngKVtA==
dependencies:
"@react-dnd/invariant" "^2.0.0"
"@react-dnd/shallowequal" "^2.0.0"
dnd-core "14.0.1"
"@react-dnd/invariant" "3.0.1"
"@react-dnd/shallowequal" "3.0.1"
dnd-core "15.1.2"
fast-deep-equal "^3.1.3"
hoist-non-react-statics "^3.3.2"
@ -19338,7 +19359,7 @@ redent@^3.0.0:
indent-string "^4.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"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==