XieYongHong 3 лет назад
Родитель
Сommit
d9eacd6b9e

+ 1 - 0
config/config.ts

@@ -91,5 +91,6 @@ export default defineConfig({
         languages: ['json', 'javascript', 'typescript', 'sql'],
       }),
     );
+    memo.module.rule('mjs-rule').test(/.m?js/).resolve.set('fullySpecified', false);
   },
 });

+ 2 - 0
package.json

@@ -97,6 +97,8 @@
     "react-beautiful-dnd": "^13.1.0",
     "react-custom-scrollbars": "^4.2.1",
     "react-dev-inspector": "^1.1.1",
+    "react-dnd": "^16.0.1",
+    "react-dnd-html5-backend": "^16.0.1",
     "react-dom": "^17.0.0",
     "react-helmet-async": "^1.0.4",
     "react-json-view": "^1.21.3",

+ 14 - 0
src/pages/system/Menu/Setting/DragItem.css

@@ -0,0 +1,14 @@
+.menu-setting-drag-tree .ant-tree-treenode.ant-tree-treenode-draggable {
+  width: 100%;
+}
+.menu-setting-drag-tree .ant-tree-node-content-wrapper {
+  flex: 1;
+}
+.menu-setting-drag-tree .tree-drag-item {
+  display: flex;
+  align-items: center;
+}
+.menu-setting-drag-tree .tree-drag-item > span:first-child {
+  flex: 1;
+  padding-right: 4px;
+}

+ 20 - 0
src/pages/system/Menu/Setting/DragItem.less

@@ -0,0 +1,20 @@
+
+.menu-setting-drag-tree {
+  .ant-tree-treenode.ant-tree-treenode-draggable {
+    width: 100%;
+  }
+
+  .ant-tree-node-content-wrapper {
+    flex: 1;
+  }
+
+  .tree-drag-item {
+    display: flex;
+    align-items: center;
+
+    & >span:first-child {
+      flex: 1;
+      padding-right: 4px;
+    }
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 1397 - 42
src/pages/system/Menu/Setting/baseMenu.ts


+ 13 - 0
src/pages/system/Menu/Setting/dnd.tsx

@@ -0,0 +1,13 @@
+import { HTML5Backend } from 'react-dnd-html5-backend';
+import { DndProvider } from 'react-dnd';
+import { Tree } from 'antd';
+
+const Dnd = () => {
+  return (
+    <DndProvider backend={HTML5Backend}>
+      <Tree />
+    </DndProvider>
+  );
+};
+
+export default Dnd;

+ 88 - 12
src/pages/system/Menu/Setting/dragItem.tsx

@@ -1,23 +1,99 @@
-import { Draggable } from 'react-beautiful-dnd';
+import {useDrag, useDrop} from 'react-dnd';
+import {useEffect, useRef, useState} from "react";
+import { CloseOutlined } from '@ant-design/icons';
+import classNames from 'classnames';
+import { DragType } from './tree';
+import { Popconfirm, Tooltip } from 'antd';
 
 interface DragItemProps {
   data: any;
   type: string;
+  canDrag: boolean;
+  isSearch?: boolean;
+  onDrop?: (item: any,dropIndex: string, type?: 'source' | 'menu') => void
+  removeDragItem?: (id: string | number) => void
 }
 
 const DragItem = (props: DragItemProps) => {
+
+  const ref = useRef<HTMLDivElement>(null)
+  const [isDrag, setIsDrag] = useState(props.type === 'source')
+
+  useEffect(() => {
+    setIsDrag(props.type === 'source' && props.canDrag)
+  }, [props.canDrag])
+
+  const [, drop] = useDrop(() => ({
+    accept: DragType,
+    drop(item: any) {
+      props.onDrop?.(item.data, props.data.code, item.type)
+      return undefined
+    },
+    collect: (monitor) => {
+      const { index } = monitor.getItem() || {};
+      if (index === props.data.code) {
+        return {}
+      }
+      return {
+        isOver: monitor.isOver(),
+        canDrop: monitor.canDrop(),
+      }
+    },
+  }))
+
+  const [, drag] = useDrag(() => (
+    {
+      type: DragType,
+      item: {
+        index: props.data.code,
+        data: props.data,
+        type: props.type
+      },
+      collect: (monitor) => {
+        return {
+          isDragging: monitor.isDragging(),
+        }
+      },
+      canDrag: isDrag
+    }
+  ), [isDrag, props.data]);
+
+
+  drag(drop(ref))
+
+  const style = props.type === 'source' ? isDrag ? {
+    border: '1px solid #E0E0E0',
+    padding: '0px 4px',
+    backgroundColor: '#F6F6F6',
+    borderRadius: 4,
+    color: '#2F54EB',
+    width: 'fit-content'
+  } : {
+    color: '#666666',
+    cursor: 'not-allowed',
+    width: 'fit-content'
+  } : {}
+
   return (
-    <Draggable
-      draggableId={props.type + '&' + props.data.id}
-      index={props.type + '&' + props.data.id}
-      isCombineEnabled={true}
-    >
-      {(provided) => (
-        <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
-          {props.data.name}
-        </div>
-      )}
-    </Draggable>
+    <div ref={ref} style={style} className={classNames({'tree-drag-item': props.type === 'menu'})}>
+      <span style={props.isSearch ? { color: '#f50' }: {}}>
+       {props.data.name}
+      </span>
+      {
+        props.type === 'menu' && 
+        <Popconfirm
+          title='确认删除?'
+          onConfirm={(e: any) => {
+            e?.stopPropagation()
+            props.removeDragItem?.(props.data.code)
+          }}
+        >
+          <Tooltip title='删除' >
+            <CloseOutlined />
+          </Tooltip>
+        </Popconfirm>
+      }
+    </div>
   );
 };
 

+ 0 - 0
src/pages/system/Menu/Setting/index.css


+ 2 - 1
src/pages/system/Menu/Setting/index.less

@@ -34,7 +34,8 @@
         font-weight: 400;
         font-size: 16px;
         background-color: #f3f4f4;
-
+        align-items: center;
+        
         > span {
           margin-left: 16px;
         }

+ 187 - 104
src/pages/system/Menu/Setting/index.tsx

@@ -1,5 +1,5 @@
-import { PageContainer } from '@ant-design/pro-layout';
-import { useDomFullHeight } from '@/hooks';
+import {PageContainer} from '@ant-design/pro-layout';
+import {useDomFullHeight} from '@/hooks';
 import {
   ExclamationCircleOutlined,
   QuestionCircleOutlined,
@@ -7,162 +7,245 @@ import {
 } from '@ant-design/icons';
 import Tree from './tree';
 import './index.less';
-import { Button, Tooltip } from 'antd';
+import {Button, Tooltip} from 'antd';
 import BaseTreeData from './baseMenu';
-import { useCallback, useEffect, useState } from 'react';
-import { DragDropContext } from 'react-beautiful-dnd';
-
-export default () => {
-  const { minHeight } = useDomFullHeight(`.menu-setting-warp`);
-  const [menuData, setMenuData] = useState<any[]>([]);
-  const [baseMenu, setBaseMenu] = useState<any[]>([]);
-
-  // const removeItem = (data: any[], id: string): any[] => {
-  //   return data.filter(item => {
-  //     if (item.id === id) {
-  //       return false
-  //     }
-  //
-  //     if (item.children) {
-  //       item.children = removeItem(item.children, id)
-  //     }
-  //     return true
-  //   })
-  // }
-
-  const findItem = (data: any[], id: string) => {
-    let object = null;
-    data.some((item) => {
-      if (item.id === id) {
-        object = item;
-        return true;
+import { useEffect, useState} from 'react';
+import {HTML5Backend} from 'react-dnd-html5-backend';
+import {DndProvider} from 'react-dnd';
+import {cloneDeep} from 'lodash'
+import { observable } from '@formily/reactive';
+import { Observer, observer } from '@formily/react'
+import { service } from '../index'
+import type {TreeProps, DataNode} from 'antd/es/tree'
+
+type MenuSettingModelType = {
+  menuData: any[]
+  notDragKeys: (string | number)[]
+}
+
+export const MenuSettingModel = observable<MenuSettingModelType>({
+  menuData: [],
+  notDragKeys: []
+})
+
+export default observer(() => {
+  const {minHeight} = useDomFullHeight(`.menu-setting-warp`);
+  const [baseMenu, setBaseMenu] = useState<any[]>(BaseTreeData);
+  const [loading, setLoading] = useState(false)
+
+  const finedObject = (data: any[], code: string | number, callback: (index: number, arr: any[], item: any) => void) => {
+
+    data.forEach((item, index) => {
+      if (item.code === code) {
+        return callback(index, data, item)
       }
 
       if (item.children) {
-        object = findItem(item.children, id);
-        return !!object;
+        finedObject(item.children, code, callback);
       }
-
-      return false;
     });
-    return object;
   };
 
-  const finedIndex = (data: any[], id: string): { index: number; menus: any[] } => {
-    let object = {
-      index: data.length,
-      menus: data,
-    };
-    data.some((item, index) => {
-      if (item.id === id) {
-        object = {
-          index,
-          menus: data,
-        };
-        return true;
-      }
+  const onTreeDrop: TreeProps['onDrop'] = (info) => {
+    const dropKey = info.node.key;
+    const dragKey = info.dragNode.key;
+    const dropPos = info.node.pos.split('-');
+    const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
 
-      if (item.children) {
-        object = finedIndex(item.children, id);
-        return !!object;
-      }
+    const data = [...MenuSettingModel.menuData];
+    let dragObj: DataNode & {parentId: string};
 
-      return false;
+    finedObject(data, dragKey, (index, arr, item) => {
+      arr.splice(index, 1);
+      dragObj = item;
+      dragObj.parentId = '';
     });
 
-    return object;
-  };
+    if (!info.dropToGap) {
+      finedObject(data, dropKey, (_i, _arr, item) => {
+        item.children = item.children || [];
+        dragObj.parentId = item.id
+        
+        item.children.unshift(dragObj);
+      });
+    } else if (
+      ((info.node as any).props.children || []).length > 0 && 
+      (info.node as any).props.expanded && 
+      dropPosition === 1 
+    ) {
+      finedObject(data, dropKey, (_i, _arr, item) => {
+        item.children = item.children || [];
+        dragObj.parentId = item.id 
+      
+        item.children.unshift(dragObj);
+      });
+    } else {
+      let ar: DataNode[] = [];
+      let i: number;
+      finedObject(data, dropKey, (index, arr, item) => {
+        ar = arr;
+        i = index;
+        dragObj.parentId = item.parentId;
+      });
+      if (dropPosition === -1) {
+        ar.splice(i!, 0, dragObj!);
+      } else {
+        ar.splice(i! + 1, 0, dragObj!);
+      }
+    }
+    MenuSettingModel.menuData = [...data]
+  }
+
+  const getKeys = (data: any[]): (string|number)[] => {
+    return data.reduce((pre: (string|number)[] , next: any) => {
+      const childrenKeys = next.children ? getKeys(next.children) : []
+      return [...pre, next.code, ...childrenKeys]
+    }, [])
+  }
 
-  const onDragEnd = useCallback(
-    (result: any) => {
-      console.log(result);
-      if (result.source.droppableId.includes('source')) {
-        if (result.combine && result.combine.droppableId.includes('menu')) {
-          const sourceIndex = result.source.index.replace(/(source|menu)&/, '');
-          const draggableIdIndex = result.combine?.draggableId.replace(/(source|menu)&/, '');
-          const sourceItem = findItem(baseMenu, sourceIndex);
-          const newMenus = [...menuData];
-          const { index, menus } = finedIndex(newMenus, draggableIdIndex);
-          console.log(index, menus);
-          menus.splice(index + 1, 0, sourceItem);
-          console.log(newMenus);
-          setMenuData([...newMenus]);
-        } else if (result.destination && result.destination.includes('menu')) {
-          const sourceIndex = result.source.index.replace(/(source|menu)&/, '');
-          const sourceItem = findItem(baseMenu, sourceIndex);
-          const newMenus = [...menuData];
-          if (sourceItem) {
-            if (newMenus.length) {
-              const destinationIndex = result.destination?.index.replace(/(source|menu)&/, '');
-              // 获取右侧menu的位置
-              const { index, menus } = finedIndex(newMenus, destinationIndex);
-              console.log(index, menus);
-              menus.splice(index + 1, 0, sourceItem);
-            } else {
-              newMenus.push(sourceItem);
-            }
-            console.log(newMenus);
-            setMenuData([...newMenus]);
-          }
+  const filterItem = (data: any[], dragKeys: (string | number)[]) => {
+    return data.filter(item => {
+      if (dragKeys.includes(item.code)) {
+        return false
+      } else if (item.children) {
+        item.children = filterItem(item.children, dragKeys)
+      }
+      return true
+    })
+  }
+
+  const onDrop = (item: any, dropIndex: string, type?: string) => {
+      const newItem = cloneDeep(item);
+      
+      if (dropIndex) { // 切换层级或者挪动
+        // const newMenus = cloneDeep(menuData);
+        if (type === 'source') {
+          finedObject(MenuSettingModel.menuData, dropIndex, (index, arr) => {
+            arr.splice(index + 1, 0, newItem);
+          });
+          MenuSettingModel.menuData = [...MenuSettingModel.menuData];
+        }
+      } else { // 新增
+        newItem.parentId = ''
+        // 过滤掉内部已选择的节点
+        if (newItem.children && MenuSettingModel.notDragKeys.length) {
+          newItem.children = filterItem(newItem.children, MenuSettingModel.notDragKeys)
         }
+        MenuSettingModel.menuData.push(newItem)
+        MenuSettingModel.notDragKeys = getKeys(MenuSettingModel.menuData)
       }
-    },
-    [menuData, baseMenu],
-  );
+    }
+
+  const removeDragItem = (id: string | number) => {
+    finedObject(MenuSettingModel.menuData, id, (index, arr) => {
+      arr.splice(index, 1);
+    });
+    MenuSettingModel.menuData = [...MenuSettingModel.menuData]
+    MenuSettingModel.notDragKeys = getKeys(MenuSettingModel.menuData)
+  }
+
+  const getSystemMenu = () => {
+    service.queryMenuThree({ paging: false }).then(res => {
+      if (res.status === 200) {
+        MenuSettingModel.menuData = [...res.result]
+        MenuSettingModel.notDragKeys = getKeys(res.result)
+      }
+    })
+  }
 
   useEffect(() => {
+    getSystemMenu()
     setBaseMenu(BaseTreeData);
   }, []);
 
+  const updateMenu = () => {
+    setLoading(true)
+    service.updateMenus(MenuSettingModel.menuData).then(res => {
+      setLoading(false)
+      if (res.status === 200) {
+
+      }
+    })
+  }
+
   return (
     <PageContainer>
-      <div className={'menu-setting-warp'} style={{ minHeight }}>
+      <div className={'menu-setting-warp'} style={{minHeight}}>
         <div className={'menu-setting-tip'}>
-          <ExclamationCircleOutlined />
-          基于系统源代码中的菜单数据,配置系统菜单。
+          <ExclamationCircleOutlined/>
+          <span style={{paddingLeft: 12}}>
+            基于系统源代码中的菜单数据,配置系统菜单。
+          </span>
         </div>
         <div className={'menu-tree-content'}>
-          <DragDropContext onDragEnd={onDragEnd}>
+          <DndProvider backend={HTML5Backend}>
             <div className={'menu-tree left-tree'}>
-              <div className={'menu-tree-title'}>
+              <div className={'menu-tree-title'} style={{padding: '10px 16px'}}>
                 <div>
+                  <span style={{paddingRight: 12}}>
                   源菜单
+                  </span>
                   <Tooltip title={'根据系统代码自动读取的菜单数据'}>
-                    <QuestionCircleOutlined />
+                    <QuestionCircleOutlined/>
                   </Tooltip>
                 </div>
-                <Button type={'primary'} ghost>
+                <Button type={'primary'} ghost onClick={() => {
+                  MenuSettingModel.menuData = cloneDeep(baseMenu)
+                }}>
                   一键拷贝
                 </Button>
               </div>
-              <Tree treeData={baseMenu} droppableId={'source'} />
+              <Observer>
+                {() => (
+                  <Tree
+                    onDrop={onDrop}
+                    treeData={baseMenu}
+                    droppableId={'source'}
+                    notDragKeys={MenuSettingModel.notDragKeys}
+                  />
+                )}
+              </Observer>
             </div>
-            <div style={{ display: 'flex', alignItems: 'center' }}>
+            <div style={{display: 'flex', alignItems: 'center'}}>
               <div className={'menu-tree-drag-btn'}>
                 请拖动至右侧
-                <RightOutlined />
+                <RightOutlined/>
               </div>
             </div>
 
             <div className={'menu-tree right-tree'}>
               <div className={'menu-tree-title'}>
                 <div>
+                  <span style={{paddingRight: 12}}>
                   系统菜单
+                  </span>
                   <Tooltip title={'菜单管理页面配置的菜单数据'}>
-                    <QuestionCircleOutlined />
+                    <QuestionCircleOutlined/>
                   </Tooltip>
                 </div>
               </div>
-              <Tree treeData={menuData} droppableId={'menu'} />
+              <Observer>
+                {() => (
+                  <>
+                  <Tree
+                    onTreeDrop={onTreeDrop}
+                    onDrop={onDrop}
+                    treeData={MenuSettingModel.menuData}
+                    droppableId={'menu'}
+                    removeDragItem={removeDragItem}
+                  />
+                  </>
+                )}
+              </Observer>
             </div>
-          </DragDropContext>
+          </DndProvider>
         </div>
         <div>
-          <Button type={'primary'} style={{ marginTop: 24 }}>
+          <Button type={'primary'} style={{ marginTop: 24 }} onClick={updateMenu} loading={loading}>
             保存
           </Button>
         </div>
       </div>
     </PageContainer>
   );
-};
+});

+ 120 - 25
src/pages/system/Menu/Setting/tree.tsx

@@ -1,58 +1,153 @@
 import { Input, Tree } from 'antd';
 import { SearchOutlined } from '@ant-design/icons';
 import DragItem from '@/pages/system/Menu/Setting/dragItem';
-import { Droppable } from 'react-beautiful-dnd';
+import { useDrop } from 'react-dnd';
+import {useEffect, useState} from 'react'
+import type {TreeProps} from 'antd'
+import { cloneDeep, debounce } from 'lodash'
+import './DragItem.less'
 
 interface TreeBodyProps {
   treeData: any[];
   droppableId: string;
+  notDragKeys?: (string| number)[]
+  onDrop?: (item: any,dropIndex: string, type?: string) => void
+  onTreeDrop?: TreeProps['onDrop']
+  removeDragItem?: (id: string | number) => void
+  className?: string
 }
 
+export const DragType = 'DragBox'
+
 const { TreeNode } = Tree;
 
 export default (props: TreeBodyProps) => {
+  const [newData, setNewData] = useState(props.treeData)
+  const [searchKeys, setSearchKeys] = useState<(string| number)[]>([])
+  const [expandedKeys, setExpandedKeys] = useState<(string| number)[]>([])
+  
+  useEffect(() => {
+    setNewData(cloneDeep(props.treeData))
+  }, [props.treeData])
+
+  const [, drop] = useDrop(() => ({
+    accept: DragType,
+    drop(item: any, monitor) {
+      const result = monitor.getDropResult()
+      if (!result && props.onDrop) {
+        props.onDrop(item.data, '')
+      }
+      return undefined
+    },
+    collect: (monitor) => {
+      return {
+        isOver: monitor.isOver(),
+        canDrop: monitor.canDrop(),
+        draggingColor: monitor.getItemType(),
+      }
+    },
+  }))
+
   const createTreeNode = (data: any[], type: string): React.ReactNode => {
     return data.map((item: any) => {
+      const isCanDrag = !props.notDragKeys?.includes(item.code)
+
       if (item.children) {
         return (
-          <TreeNode title={<DragItem data={item} type={type} />}>
+          <TreeNode
+          selectable={false}
+          key={item.code}
+          title={
+          <DragItem 
+            onDrop={props.onDrop}
+            data={item}
+            type={type}
+            canDrag={isCanDrag}
+            removeDragItem={props.removeDragItem}
+            isSearch={searchKeys.includes(item.code)}
+            />
+           }>
             {createTreeNode(item.children, type)}
           </TreeNode>
         );
       }
-      return <TreeNode title={<DragItem data={item} type={type} />}></TreeNode>;
+      return <TreeNode
+      selectable={false}
+      key={item.code}
+      title={
+      <DragItem 
+        onDrop={props.onDrop}
+        data={item}
+        type={type}
+        canDrag={isCanDrag}
+        removeDragItem={props.removeDragItem}
+        isSearch={searchKeys.includes(item.code)}
+        />
+     } />;
     });
-  };
+  }
+
+  const findAllItem = (data: any[], value: string): string[] => {
+    return data.reduce((pre, next) => {
+      const childrenKeys = next.children ? findAllItem(next.children, value) : []
+      return next.name.includes(value) ? [...pre, next.code, ...childrenKeys] : [...pre, ...childrenKeys]
+    }, [])
+
+  }
+
+  const searchValue = (e: any) => {
+    const value = e.target.value
+
+    if (value) {
+      const sKeys = findAllItem(props.treeData, value)
+      setSearchKeys(sKeys)
+      setExpandedKeys(sKeys)
+    } else {
+      setSearchKeys([])
+    }
+  }
+
 
   return (
     <div className={'tree-content'}>
       <div style={{ width: '75%' }}>
         <Input
+          allowClear
           prefix={<SearchOutlined style={{ color: '#B3B3B3' }} />}
           placeholder={'请输入菜单名称'}
+          onChange={debounce(searchValue, 500)}
         />
       </div>
-      <div className={'tree-body'}>
-        <Droppable
-          droppableId={props.droppableId}
-          direction="horizontal"
-          type="COLUMN"
-          isCombineEnabled={true}
-        >
-          {(provided) => (
-            <div
-              className="columns"
-              {...provided.droppableProps}
-              ref={provided.innerRef}
-              style={{ height: '100%' }}
-            >
-              <Tree draggable={props.droppableId === 'menu'}>
-                {createTreeNode(props.treeData, props.droppableId)}
-              </Tree>
-            </div>
-          )}
-        </Droppable>
-      </div>
+      {
+        props.droppableId === 'source' ?
+        <div className={'tree-body'}>
+          <Tree
+            expandedKeys={expandedKeys}
+            onExpand={(_expandedKeys) => {
+              setExpandedKeys(_expandedKeys)
+            }}
+          >
+            {createTreeNode(newData, props.droppableId)}
+          </Tree>
+        </div>
+          :
+        <div className={'tree-body'} ref={drop} >
+          <Tree
+            expandedKeys={expandedKeys}
+            onExpand={(_expandedKeys) => {
+              setExpandedKeys(_expandedKeys)
+            }}
+            draggable={{
+              icon: false
+            }}
+            onDrop={props.onTreeDrop}
+            className='menu-setting-drag-tree'
+          >
+            {createTreeNode(props.treeData, props.droppableId)}
+          </Tree>
+        </div>
+
+      }
     </div>
   );
 };

+ 3 - 0
src/pages/system/Menu/service.ts

@@ -27,6 +27,9 @@ class Service extends BaseService<MenuItem> {
 
   // 资产类型
   queryAssetsType = () => request(`${SystemConst.API_BASE}/asset/types`, { method: 'GET' });
+
+  // 更新全部菜单
+  updateMenus = (data: any) => request(`${this.uri}`, {method: 'PATCH', data})
 }
 
 export default Service;

+ 43 - 1
yarn.lock

@@ -3622,6 +3622,21 @@
   resolved "https://registry.yarnpkg.com/@qixian.cs/path-to-regexp/-/path-to-regexp-6.1.0.tgz#6b84ad01596332aba95fa29d2e70104698cd5c45"
   integrity sha512-2jIiLiVZB1jnY7IIRQKtoV8Gnr7XIhk4mC88ONGunZE3hYt5IHUG4BE/6+JiTBjjEWQLBeWnZB8hGpppkufiVw==
 
+"@react-dnd/asap@^5.0.1":
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488"
+  integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==
+
+"@react-dnd/invariant@^4.0.1":
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df"
+  integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==
+
+"@react-dnd/shallowequal@^4.0.1":
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4"
+  integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==
+
 "@sindresorhus/is@^0.14.0":
   version "0.14.0"
   resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@@ -8448,6 +8463,15 @@ discontinuous-range@1.0.0:
   resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
   integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=
 
+dnd-core@^16.0.1:
+  version "16.0.1"
+  resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19"
+  integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==
+  dependencies:
+    "@react-dnd/asap" "^5.0.1"
+    "@react-dnd/invariant" "^4.0.1"
+    redux "^4.2.0"
+
 doctrine@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -16919,6 +16943,24 @@ react-dev-utils@^11.0.0:
     strip-ansi "6.0.0"
     text-table "0.2.0"
 
+react-dnd-html5-backend@^16.0.1:
+  version "16.0.1"
+  resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6"
+  integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==
+  dependencies:
+    dnd-core "^16.0.1"
+
+react-dnd@^16.0.1:
+  version "16.0.1"
+  resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37"
+  integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==
+  dependencies:
+    "@react-dnd/invariant" "^4.0.1"
+    "@react-dnd/shallowequal" "^4.0.1"
+    dnd-core "^16.0.1"
+    fast-deep-equal "^3.1.3"
+    hoist-non-react-statics "^3.3.2"
+
 react-docgen-typescript-dumi-tmp@^1.22.1-0:
   version "1.22.1-0"
   resolved "https://registry.yarnpkg.com/react-docgen-typescript-dumi-tmp/-/react-docgen-typescript-dumi-tmp-1.22.1-0.tgz#6f452de05c5c114a6e1dd60b34930afaa7ae39a0"
@@ -17468,7 +17510,7 @@ redux@^4.0.0, redux@^4.0.1:
   dependencies:
     "@babel/runtime" "^7.9.2"
 
-redux@^4.0.4:
+redux@^4.0.4, redux@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
   integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==