diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7b90428f3d..693a9f22f1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -46,8 +46,8 @@ "react-app-polyfill": "^2.0.0", "react-chartjs-2": "5.3.0", "react-cookies": "^0.1.0", - "react-dnd": "^2.6.0", - "react-dnd-html5-backend": "^2.6.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.3.1", "react-i18next": "^10.12.2", "react-mentions": "4.4.10", @@ -3105,19 +3105,6 @@ } } }, - "node_modules/@emotion/react/node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/@emotion/react/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/@emotion/serialize": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", @@ -5390,6 +5377,21 @@ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "node_modules/@replit/codemirror-lang-csharp": { "version": "6.2.0", "resolved": "https://registry.npmmirror.com/@replit/codemirror-lang-csharp/-/codemirror-lang-csharp-6.2.0.tgz", @@ -8290,6 +8292,7 @@ "version": "2.0.6", "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, "license": "MIT" }, "node_modules/asn1.js": { @@ -11436,12 +11439,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/disposables": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/disposables/-/disposables-1.0.2.tgz", - "integrity": "sha512-q1XTvs/XGdfubRSemB2+QRhJjIX4PerKkSom+i8Nkw3hCv6xISNrgaN442n2BunyBI4x77Om4ZAzSlqmhM9pwA==", - "license": "Apache-2.0" - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", @@ -11450,15 +11447,13 @@ "license": "MIT" }, "node_modules/dnd-core": { - "version": "2.6.0", - "resolved": "https://registry.npmmirror.com/dnd-core/-/dnd-core-2.6.0.tgz", - "integrity": "sha512-5BfQHIp0XVd4ioF0q4GyUeHQQNCbqP+0SnUiP9TssoQ50wrP1NgSzDqZkjD5pFngsVz9txGin6rvTQD7w0qC3w==", - "license": "BSD-3-Clause", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", "dependencies": { - "asap": "^2.0.6", - "invariant": "^2.0.0", - "lodash": "^4.2.0", - "redux": "^3.7.1" + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" } }, "node_modules/dns-packet": { @@ -13247,7 +13242,6 @@ "version": "3.1.3", "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -14913,10 +14907,17 @@ } }, "node_modules/hoist-non-react-statics": { - "version": "2.5.5", - "resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", - "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==", - "license": "BSD-3-Clause" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/hoopy": { "version": "0.1.4", @@ -23210,29 +23211,40 @@ } }, "node_modules/react-dnd": { - "version": "2.6.0", - "resolved": "https://registry.npmmirror.com/react-dnd/-/react-dnd-2.6.0.tgz", - "integrity": "sha512-2KHNpeg2SyaxXYq+xO1TM+tOtN9hViI41otJuiYiu6DRYGw+WMvDFDMP4aw7zIKRRm1xd0gizXuKWhb8iJYHBw==", - "license": "BSD-3-Clause", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", "dependencies": { - "disposables": "^1.0.1", - "dnd-core": "^2.6.0", - "hoist-non-react-statics": "^2.1.0", - "invariant": "^2.1.0", - "lodash": "^4.2.0", - "prop-types": "^15.5.10" + "@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" }, "peerDependencies": { - "react": "*" + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } } }, "node_modules/react-dnd-html5-backend": { - "version": "2.6.0", - "resolved": "https://registry.npmmirror.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.6.0.tgz", - "integrity": "sha512-8gOfBfqFikWmXvAGSZz1mgoctwkcsKdUC9POt/WGnMoZwGB4ivB0Ex5D6pwHTNjvAs0ixqqWdJKy57CzjDg5Sg==", - "license": "BSD-3-Clause", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", "dependencies": { - "lodash": "^4.2.0" + "dnd-core": "^16.0.1" } }, "node_modules/react-dom": { @@ -23577,15 +23589,11 @@ } }, "node_modules/redux": { - "version": "3.7.2", - "resolved": "https://registry.npmmirror.com/redux/-/redux-3.7.2.tgz", - "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", - "license": "MIT", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "dependencies": { - "lodash": "^4.2.1", - "lodash-es": "^4.2.1", - "loose-envify": "^1.1.0", - "symbol-observable": "^1.0.3" + "@babel/runtime": "^7.9.2" } }, "node_modules/reflect.getprototypeof": { @@ -27108,15 +27116,6 @@ "node": ">=4" } }, - "node_modules/symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9b809772fb..35b164bd52 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,8 +41,8 @@ "react-app-polyfill": "^2.0.0", "react-chartjs-2": "5.3.0", "react-cookies": "^0.1.0", - "react-dnd": "^2.6.0", - "react-dnd-html5-backend": "^2.6.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.3.1", "react-i18next": "^10.12.2", "react-mentions": "4.4.10", diff --git a/frontend/src/metadata/components/popover/groupbys-popover/groupbys/groupby-item.js b/frontend/src/metadata/components/popover/groupbys-popover/groupbys/groupby-item.js index d2a91a1f8a..a059630371 100644 --- a/frontend/src/metadata/components/popover/groupbys-popover/groupbys/groupby-item.js +++ b/frontend/src/metadata/components/popover/groupbys-popover/groupbys/groupby-item.js @@ -1,7 +1,7 @@ -import React, { Fragment, useCallback, useMemo } from 'react'; +import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { DragSource, DropTarget } from 'react-dnd'; +import { useDrag, useDrop } from 'react-dnd'; import CustomizeSelect from '../../../../../components/customize-select'; import Icon from '../../../../../components/icon'; import { gettext } from '../../../../../utils/constants'; @@ -9,49 +9,6 @@ import { getColumnByKey } from '../../../../utils/column'; import { COLUMNS_ICON_CONFIG, SORT_TYPE, SORT_COLUMN_OPTIONS } from '../../../../constants'; import { getGroupbyGranularityByColumn, isShowGroupCountType, getSelectedCountType, getDefaultCountType } from '../../../../utils/group'; -const dragSource = { - beginDrag: props => { - return { idx: props.index, data: props.groupby, mode: 'sfMetadataGroupbyItem' }; - }, - endDrag(props, monitor) { - const groupSource = monitor.getItem(); - const didDrop = monitor.didDrop(); - let groupTarget = {}; - if (!didDrop) { - return { groupSource, groupTarget }; - } - }, - isDragging(props) { - const { index, dragged } = props; - const { idx } = dragged; - return idx > index; - } -}; - -const dropTarget = { - drop(props, monitor) { - const groupSource = monitor.getItem(); - const { index: targetIdx } = props; - if (targetIdx !== groupSource.idx) { - let groupTarget = { idx: targetIdx, data: props.groupby }; - props.onMove(groupSource, groupTarget); - } - } -}; - -const dragCollect = (connect, monitor) => ({ - connectDragSource: connect.dragSource(), - connectDragPreview: connect.dragPreview(), - isDragging: monitor.isDragging(), -}); - -const dropCollect = (connect, monitor) => ({ - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - dragged: monitor.getItem(), -}); - /* groupby: { column_key: 'xxx', @@ -59,10 +16,46 @@ const dropCollect = (connect, monitor) => ({ sort_type: 'xxx', } */ -const GroupbyItem = ({ - isOver, isDragging, canDrop, connectDragSource, connectDragPreview, connectDropTarget, - showDragBtn, index, readOnly, groupby, columns, onDelete, onUpdate -}) => { +const GroupbyItem = ({ showDragBtn, index, readOnly, groupby, columns, onDelete, onUpdate, onMove }) => { + const ref = useRef(null); + + const [dropPosition, setDropPosition] = useState(null); + + const [, drag, preview] = useDrag({ + type: 'sfMetadataGroupbyItem', + item: () => ({ + idx: index, + data: groupby, + }), + }); + + const [{ isOver, canDrop }, drop] = useDrop({ + accept: 'sfMetadataGroupbyItem', + hover: (item, monitor) => { + if (!ref.current) return; + + const hoverBoundingRect = ref.current.getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + const newPosition = hoverClientY < hoverMiddleY ? 'top' : 'bottom'; + setDropPosition(newPosition); + }, + drop: (item) => { + if (item.idx === index) return; + if (item.idx === index - 1 && dropPosition === 'top') return; + if (item.idx === index + 1 && dropPosition === 'bottom') return; + onMove(item, { idx: index, data: groupby }); + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }) + }); + + drop(preview(ref)); + const column = useMemo(() => { return getColumnByKey(columns, groupby.column_key); }, [groupby, columns]); @@ -165,59 +158,58 @@ const GroupbyItem = ({ onUpdate(newGroupby, index); }, [groupby, index, onUpdate]); - return connectDropTarget( - connectDragPreview( -
- {!readOnly && ( -
- -
- )} -
-
+ return ( +
+ {!readOnly && ( +
+ +
+ )} +
+
+ +
+ {isShowGroupCountType(column) && ( +
- {isShowGroupCountType(column) && ( -
- -
- )} -
- {(!column.key || SORT_COLUMN_OPTIONS.includes(column.type)) && ( - - )} -
-
- {!readOnly && showDragBtn && connectDragSource( -
- -
)} +
+ {(!column.key || SORT_COLUMN_OPTIONS.includes(column.type)) && ( + + )} +
- ) + {!readOnly && showDragBtn && ( +
+ +
+ )} +
); }; @@ -229,16 +221,7 @@ GroupbyItem.propTypes = { columns: PropTypes.array, onDelete: PropTypes.func, onUpdate: PropTypes.func, - - // drag - isDragging: PropTypes.bool, - isOver: PropTypes.bool, - canDrop: PropTypes.bool, - connectDropTarget: PropTypes.func, - connectDragSource: PropTypes.func, - connectDragPreview: PropTypes.func, + onMove: PropTypes.func, }; -export default DropTarget('sfMetadataGroupbyItem', dropTarget, dropCollect)( - DragSource('sfMetadataGroupbyItem', dragSource, dragCollect)(GroupbyItem) -); +export default GroupbyItem; diff --git a/frontend/src/metadata/components/popover/groupbys-popover/groupbys/index.js b/frontend/src/metadata/components/popover/groupbys-popover/groupbys/index.js index 8cd808133a..b546d95fb6 100644 --- a/frontend/src/metadata/components/popover/groupbys-popover/groupbys/index.js +++ b/frontend/src/metadata/components/popover/groupbys-popover/groupbys/index.js @@ -1,8 +1,6 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { DropTarget } from 'react-dnd'; -import html5DragDropContext from '../../../../../pages/wiki2/wiki-nav/html5DragDropContext'; import GroupbyItem from './groupby-item'; import { gettext } from '../../../../../utils/constants'; @@ -47,8 +45,4 @@ Groupbys.propTypes = { onMove: PropTypes.func, }; -const DndGroupbysContainer = DropTarget('sfMetadataGroupbyItem', {}, connect => ({ - connectDropTarget: connect.dropTarget() -}))(Groupbys); - -export default html5DragDropContext(DndGroupbysContainer); +export default Groupbys; diff --git a/frontend/src/metadata/components/popover/groupbys-popover/index.css b/frontend/src/metadata/components/popover/groupbys-popover/index.css index 2defd503ee..65a5b45dfc 100644 --- a/frontend/src/metadata/components/popover/groupbys-popover/index.css +++ b/frontend/src/metadata/components/popover/groupbys-popover/index.css @@ -27,19 +27,28 @@ position: relative; } -.sf-metadata-groupbys-popover .groupbys-list .groupby-item.group-can-drop { - border-bottom: 1px solid #666; -} - .sf-metadata-groupbys-popover .groupbys-list .groupby-item.group-can-drop-top::before { content: ''; - width: 100%; - height: 1px; - background: #666; - display: block; position: absolute; top: 0; left: 0; + width: 100%; + height: 2px; + background-color: rgb(200, 220, 240); + border-radius: 2px; + z-index: 1; +} + +.sf-metadata-groupbys-popover .groupbys-list .groupby-item.group-can-drop-bottom::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background-color: rgb(200, 220, 240); + border-radius: 2px; + z-index: 1; } .sf-metadata-groupbys-popover .groupby-item .delete-groupby { diff --git a/frontend/src/metadata/components/popover/options-popover/option/index.css b/frontend/src/metadata/components/popover/options-popover/option/index.css index 310cf6f8f7..5f83cb5f26 100644 --- a/frontend/src/metadata/components/popover/options-popover/option/index.css +++ b/frontend/src/metadata/components/popover/options-popover/option/index.css @@ -49,19 +49,28 @@ fill: #212529; } -.sf-metadata-edit-option-container.sf-metadata-edit-option-can-drop { - border-bottom: 1px solid #666; -} - -.sf-metadata-edit-option-container.sf-metadata-edit-option-can-drop-top::before { +.sf-metadata-edit-option-container.sf-metadata-edit-option-drop-over-top::before { content: ''; - width: 100%; - height: 1px; - background: #666; - display: block; position: absolute; top: 0; left: 0; + width: 100%; + height: 2px; + background-color: rgb(200, 220, 240); + border-radius: 2px; + z-index: 1; +} + +.sf-metadata-edit-option-container.sf-metadata-edit-option-drop-over-bottom::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background-color: rgb(200, 220, 240); + border-radius: 2px; + z-index: 1; } .sf-metadata-edit-option-container.sf-metadata-edit-deleting-option, diff --git a/frontend/src/metadata/components/popover/options-popover/option/index.js b/frontend/src/metadata/components/popover/options-popover/option/index.js index 6f0d36ec61..ecdb5ae916 100644 --- a/frontend/src/metadata/components/popover/options-popover/option/index.js +++ b/frontend/src/metadata/components/popover/options-popover/option/index.js @@ -1,6 +1,6 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import PropTypes from 'prop-types'; -import { DragSource, DropTarget } from 'react-dnd'; +import { useDrag, useDrop } from 'react-dnd'; import classnames from 'classnames'; import Icon from '../../../../../components/icon'; import IconBtn from '../../../../../components/icon-btn'; @@ -9,56 +9,49 @@ import Name from './name'; import './index.css'; -const dragSource = { - beginDrag: props => { - return { idx: props.index, data: props.option, mode: 'sfMetadataSingleSelectOption' }; - }, - endDrag(props, monitor) { - const optionSource = monitor.getItem(); - const didDrop = monitor.didDrop(); - let optionTarget = {}; - if (!didDrop) { - return { optionSource, optionTarget }; - } - }, - isDragging(props, monitor) { - const { index, dragged } = props; - const { idx } = dragged; - return idx > index; - } -}; - -const dragCollect = (connect, monitor) => ({ - connectDragSource: connect.dragSource(), - connectDragPreview: connect.dragPreview(), - isDragging: monitor.isDragging() -}); - -const dropTarget = { - drop(props, monitor) { - const optionSource = monitor.getItem(); - const { index: targetIdx } = props; - if (targetIdx !== optionSource.idx) { - const optionTarget = { idx: targetIdx, data: props.option }; - props.onMove(optionSource, optionTarget); - } - } -}; - -const dropCollect = (connect, monitor) => ({ - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - dragged: monitor.getItem() -}); - const Option = ({ - isOver, isDragging, canDrop, connectDragSource, connectDragPreview, connectDropTarget, isViewing, isDeleting, isEditing, isPredefined, - option, - onDelete: propsDelete, onUpdate, + option, index, + onDelete: propsDelete, onUpdate, onMove, onMouseLeave, onMouseEnter: propsMouseEnter, onToggleFreeze, onOpenNameEditor, onCloseNameEditor, }) => { + const ref = useRef(null); + const [dropPosition, setDropPosition] = useState(null); + + const [, drag, preview] = useDrag({ + type: 'sfMetadataSingleSelectOption', + item: () => ({ + idx: index, + data: option, + }), + }); + + const [{ isOver, canDrop }, drop] = useDrop({ + accept: 'sfMetadataSingleSelectOption', + hover: (item, monitor) => { + if (!ref.current) return; + + const hoverBoundingRect = ref.current.getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + const newPosition = hoverClientY < hoverMiddleY ? 'top' : 'bottom'; + setDropPosition(newPosition); + }, + drop: (item) => { + if (item.idx === index) return; + if (item.idx === index - 1 && dropPosition === 'top') return; + if (item.idx === index + 1 && dropPosition === 'bottom') return; + onMove(item, { idx: index, data: option }); + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }) + }); + + drop(preview(ref)); const onDelete = useCallback((event) => { event.nativeEvent.stopImmediatePropagation(); @@ -69,49 +62,45 @@ const Option = ({ propsMouseEnter(option.id); }, [option, propsMouseEnter]); - return connectDropTarget( - connectDragPreview( -
onMouseEnter()} - onMouseLeave={onMouseLeave} - > - {connectDragSource( -
- -
- )} -
- - -
-
- {(isViewing || isDeleting) && ( - - )} -
+ return ( +
onMouseEnter()} + onMouseLeave={onMouseLeave} + > +
+
- ) +
+ + +
+
+ {(isViewing || isDeleting) && ( + + )} +
+
); }; Option.propTypes = { - // normal option: PropTypes.object, index: PropTypes.number, isPredefined: PropTypes.bool, @@ -126,16 +115,6 @@ Option.propTypes = { onToggleFreeze: PropTypes.func, onOpenNameEditor: PropTypes.func.isRequired, onCloseNameEditor: PropTypes.func.isRequired, - - // drag - isOver: PropTypes.bool, - canDrop: PropTypes.bool, - dragged: PropTypes.object, - connectDragSource: PropTypes.func.isRequired, - connectDropTarget: PropTypes.func.isRequired, - connectDragPreview: PropTypes.func.isRequired, }; -export default DropTarget('sfMetadataSingleSelectOption', dropTarget, dropCollect)( - DragSource('sfMetadataSingleSelectOption', dragSource, dragCollect)(Option) -); +export default Option; diff --git a/frontend/src/metadata/components/popover/options-popover/options-container.js b/frontend/src/metadata/components/popover/options-popover/options-container.js index 04b9db553d..44d2c2768a 100644 --- a/frontend/src/metadata/components/popover/options-popover/options-container.js +++ b/frontend/src/metadata/components/popover/options-popover/options-container.js @@ -1,7 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { DropTarget } from 'react-dnd'; -import html5DragDropContext from '../../../../pages/wiki2/wiki-nav/html5DragDropContext'; const OptionsContainer = ({ inputRef, options }) => { if (!Array.isArray(options) || options.length === 0) return null; @@ -17,8 +15,4 @@ OptionsContainer.propTypes = { options: PropTypes.array }; -const DndOptionsContainer = DropTarget('sfMetadataSingleSelectOption', {}, connect => ({ - connectDropTarget: connect.dropTarget() -}))(OptionsContainer); - -export default html5DragDropContext(DndOptionsContainer); +export default OptionsContainer; diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index 39087421b2..e8fe4833d7 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -7,6 +7,8 @@ import classnames from 'classnames'; import MediaQuery from 'react-responsive'; import { Modal } from 'reactstrap'; import { navigate } from '@gatsbyjs/reach-router'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; import { gettext, siteRoot, username } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; @@ -2352,89 +2354,90 @@ class LibContentView extends React.Component { } const detailDirent = currentDirent || currentNode?.object || null; return ( - - - - -
-
- {this.state.currentRepoInfo.status === 'read-only' && + + + + + +
+
+ {this.state.currentRepoInfo.status === 'read-only' &&
{gettext('This library has been set to read-only by admin and cannot be updated.')}
- } -
-
- {isDirentSelected ? ( - currentMode === TAGS_MODE || currentMode === METADATA_MODE ? ( - + } +
+
+ {isDirentSelected ? ( + currentMode === TAGS_MODE || currentMode === METADATA_MODE ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} -
- {isDesktop && + )} +
+ {isDesktop &&
- } -
-
- {this.state.pathExist ? - - : -
{gettext('Folder does not exist.')}
- } - {!isCustomPermission && this.state.isDirentDetailShow && ( - - )} + } +
+
+ {this.state.pathExist ? + + : +
{gettext('Folder does not exist.')}
+ } + {!isCustomPermission && this.state.isDirentDetailShow && ( + + )} +
+ {canUpload && this.state.pathExist && !this.state.isViewFile && ![METADATA_MODE, TAGS_MODE].includes(this.state.currentMode) && ( + this.uploader = uploader} + dragAndDrop={true} + path={this.state.path} + repoID={this.props.repoID} + direntList={this.state.direntList} + onFileUploadSuccess={this.onFileUploadSuccess} + isCustomPermission={isCustomPermission} + /> + )}
- {canUpload && this.state.pathExist && !this.state.isViewFile && ![METADATA_MODE, TAGS_MODE].includes(this.state.currentMode) && ( - this.uploader = uploader} - dragAndDrop={true} - path={this.state.path} - repoID={this.props.repoID} - direntList={this.state.direntList} - onFileUploadSuccess={this.onFileUploadSuccess} - isCustomPermission={isCustomPermission} + {isCopyMoveProgressDialogShow && ( + )} -
- {isCopyMoveProgressDialogShow && ( - - )} - {isDeleteFolderDialogOpen && ( - - )} - - - -
-
-
-
+ {isDeleteFolderDialogOpen && ( + + )} + + + + + + + +
); } } diff --git a/frontend/src/pages/wiki2/wiki-nav/html5DragDropContext.js b/frontend/src/pages/wiki2/wiki-nav/html5DragDropContext.js deleted file mode 100644 index a9708ef978..0000000000 --- a/frontend/src/pages/wiki2/wiki-nav/html5DragDropContext.js +++ /dev/null @@ -1,4 +0,0 @@ -import { DragDropContext } from 'react-dnd'; -import HTML5Backend from 'react-dnd-html5-backend'; - -export default DragDropContext(HTML5Backend); diff --git a/frontend/src/pages/wiki2/wiki-nav/pages/dragged-page-item.js b/frontend/src/pages/wiki2/wiki-nav/pages/dragged-page-item.js deleted file mode 100644 index 7410a37358..0000000000 --- a/frontend/src/pages/wiki2/wiki-nav/pages/dragged-page-item.js +++ /dev/null @@ -1,82 +0,0 @@ -import { DragSource, DropTarget } from 'react-dnd'; -import PageItem from './page-item'; -import wikiAPI from '../../../../utils/wiki-api'; -import { wikiId, gettext } from '../../../../utils/constants'; -import { Utils } from '../../../../utils/utils'; -import toaster from '../../../../components/toast'; - -const dragSource = { - beginDrag: props => { - return { - idx: props.pageIndex, - data: { ...props.page, index: props.pageIndex }, - mode: 'wiki-page', - }; - }, - endDrag(props, monitor) { - const viewSource = monitor.getItem(); - const didDrop = monitor.didDrop(); - let viewTarget = {}; - if (!didDrop) { - return { viewSource, viewTarget }; - } - }, - isDragging(props) { - const { draggedPage, pageIndex: targetIndex } = props; - const { idx } = draggedPage; - return idx > targetIndex; - } -}; - -const dropTarget = { - drop(props, monitor) { - const dragSource = monitor.getItem(); - const className = props.getClassName(); - let move_position; - if (className.includes('page-can-drop-bottom')) { - move_position = 'move_below'; - } else if (className.includes('page-can-drop-top')) { - move_position = 'move_above'; - } else if (className.includes('dragged-page-over')) { - move_position = 'move_into'; - } - if (!move_position) return; - if (dragSource.mode === 'wiki-page') { - const targetPage = props.page; - const draggedPage = dragSource.data; - const moved_page_id = draggedPage.id; - const target_page_id = targetPage.id; - if (moved_page_id === target_page_id) { - return; - } - if (targetPage._path && targetPage._path.includes(moved_page_id)) { - toaster.danger(gettext('Cannot move parent page to child page')); - return; - } - wikiAPI.moveWiki2Page(wikiId, moved_page_id, target_page_id, move_position).then(res => { - props.onMovePage({ moved_page_id, target_page_id, move_position }); - }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - return; - } -}; - -const dragCollect = (connect, monitor) => ({ - connectDragSource: connect.dragSource(), - connectDragPreview: connect.dragPreview(), - isDragging: monitor.isDragging() -}); - -const dropCollect = (connect, monitor) => ({ - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - draggedPage: monitor.getItem() -}); - -export default DropTarget('WikiNav', dropTarget, dropCollect)( - DragSource('WikiNav', dragSource, dragCollect)(PageItem) -); diff --git a/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js b/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js index 05d830cc5e..3d1e46f84d 100644 --- a/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js +++ b/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js @@ -1,354 +1,413 @@ -import React, { Component } from 'react'; +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import NameEditPopover from '../../common/name-edit-popover'; import NavItemIcon from '../../common/nav-item-icon'; import PageDropdownMenu from './page-dropdownmenu'; -import { gettext, wikiPermission } from '../../../../utils/constants'; +import { gettext, wikiId, wikiPermission } from '../../../../utils/constants'; import AddNewPageDialog from '../add-new-page-dialog'; import Icon from '../../../../components/icon'; -import DraggedPageItem from './dragged-page-item'; import CustomIcon from '../../custom-icon'; import { eventBus } from '../../../../components/common/event-bus'; import { INSERT_POSITION } from '../constants'; +import toaster from '../../../../components/toast'; +import wikiAPI from '../../../../utils/wiki-api'; +import { Utils } from '../../../../utils/utils'; -class PageItem extends Component { +const PageItem = ({ + page, + pageIndex, + pages, + pagesLength, + parentPageId, + duplicatePage, + setCurrentPage, + onUpdatePage, + onDeletePage, + onMovePage, + isOnlyOnePage, + pathStr, + toggleExpand, + getCurrentPageId, + addSiblingPage, + addPageInside, + getFoldState, + updateWikiConfig, + getClassName, + setClassName, +}) => { + const [isShowNameEditor, setIsShowNameEditor] = useState(false); + const [isShowOperationDropdown, setIsShowOperationDropdown] = useState(false); + const [isShowInsertPage, setIsShowInsertPage] = useState(false); + const [isShowAddSiblingPage, setIsShowAddSiblingPage] = useState(false); + const [insertPosition, setInsertPosition] = useState(''); + const [dropPosition, setDropPosition] = useState(null); + const [pageName, setPageName] = useState(page.name || ''); + const [isSelected, setIsSelected] = useState(page.id === getCurrentPageId()); + const [isMouseEntered, setIsMouseEntered] = useState(false); - constructor(props) { - super(props); - this.state = { - isShowNameEditor: false, - isShowOperationDropdown: false, - isShowInsertPage: false, - isShowAddSiblingPage: false, - insertPosition: '', - pageName: props.page.name || '', - isSelected: props.getCurrentPageId() === props.page.id, - isMouseEnter: false, - }; - this.pageItemRef = React.createRef(); - } + const ref = useRef(null); - componentDidMount() { - this.unsubscribeEvent = eventBus.subscribe('update-wiki-current-page', this.updateSelected); - } + const [, drag] = useDrag(() => ({ + type: 'wiki-page', + item: () => ({ + idx: pageIndex, + data: { ...page, index: pageIndex }, + }), + })); - componentWillUnmount() { - this.unsubscribeEvent(); - this.setState = () => { - return; - }; - } + const [{ isOver, canDrop }, drop] = useDrop(() => ({ + accept: 'wiki-page', + hover: (item, monitor) => { + if (!ref.current) return; + const hoverBoundingRect = ref.current.getBoundingClientRect(); + const height = hoverBoundingRect.bottom - hoverBoundingRect.top; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + if (hoverClientY < 10) { + setDropPosition('top'); + item.dropPosition = 'move_above'; + } else if (hoverClientY > height - 10) { + setDropPosition('bottom'); + item.dropPosition = 'move_below'; + } else { + setDropPosition(null); + item.dropPosition = 'move_into'; + } + }, + drop: (item, monitor) => { + const target = page; + const source = item.data; + const moved_page_id = source.id; + const target_page_id = target.id; - updateSelected = () => { - const isSelected = this.props.getCurrentPageId() === this.props.page.id; - if (isSelected !== this.state.isSelected) { - this.setState({ isSelected }); + if (moved_page_id === target_page_id) return; + + if (target._path && target._path.includes(moved_page_id)) { + toaster.danger(gettext('Cannot move parent page to child page')); + return; + } + + const move_position = item.dropPosition || 'move_into'; + wikiAPI.moveWiki2Page(wikiId, moved_page_id, target_page_id, move_position).then(res => { + onMovePage({ moved_page_id, target_page_id, move_position }); + setDropPosition(null); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop() + }) + })); + + drag(drop(ref)); + + const onMouseEnter = () => { + setIsMouseEntered(true); + }; + + const onMouseMove = () => { + if (!isMouseEntered) { + setIsMouseEntered(true); } }; - onMouseEnter = () => { - this.setState({ isMouseEnter: true }); - if (this.state.isSelected) return; + const onMouseLeave = () => { + setIsMouseEntered(false); }; - onMouseMove = () => { - if (!this.state.isMouseEnter) this.setState({ isMouseEnter: true }); + const onClickPageItem = () => { + if (!isShowNameEditor) { + setCurrentPage(page.id); + } }; - onMouseLeave = () => { - this.setState({ isMouseEnter: false }); - if (this.state.isSelected) return; + const onChangeName = (name) => { + setPageName(name); }; - toggleNameEditor = (e) => { - if (e) e.stopPropagation(); - this.setState({ isShowNameEditor: !this.state.isShowNameEditor }, () => { - if (!this.state.isShowNameEditor) { - this.savePageProperties(); + const savePageProperties = useCallback(() => { + const { name, id } = page; + const currentPageName = pageName.trim(); + if (currentPageName !== name) { + const isUpdateBySide = true; + onUpdatePage(id, { name: currentPageName }, isUpdateBySide); + } + }, [page, pageName, onUpdatePage]); + + const toggleNameEditor = useCallback((e) => { + if (e) { + e.stopPropagation(); + } + setIsShowNameEditor(!isShowNameEditor, () => { + if (!isShowNameEditor) { + savePageProperties(); } }); + }, [isShowNameEditor, savePageProperties]); + + const changeItemFreeze = (isFreeze) => { + if (isFreeze) { + ref.current.classList.add('wiki-page-freezed'); + } else { + ref.current.classList.remove('wiki-page-freezed'); + } }; - toggleInsertPage = () => { - this.setState({ isShowInsertPage: !this.state.isShowInsertPage }); - }; + const toggleDropdown = useCallback(() => { + const isShow = !isShowOperationDropdown; + setIsShowOperationDropdown(isShow); + changeItemFreeze(isShow); + }, [isShowOperationDropdown]); - toggleInsertSiblingPage = (position) => { + const toggleInsertSiblingPage = useCallback((position) => { let insertPosition = null; if (position === INSERT_POSITION.BELOW || position === INSERT_POSITION.ABOVE) { insertPosition = position; } - this.setState({ - insertPosition, - isShowAddSiblingPage: !this.state.isShowAddSiblingPage, - }); - }; + setInsertPosition(insertPosition); + setIsShowAddSiblingPage(!isShowAddSiblingPage); + }, [isShowAddSiblingPage]); - savePageProperties = () => { - const { name, id } = this.props.page; - const pageName = this.state.pageName.trim(); - if (pageName !== name) { - const isUpdateBySide = true; - this.props.onUpdatePage(id, { name: pageName }, isUpdateBySide); + const toggleInsertPage = useCallback(() => { + setIsShowInsertPage(!isShowInsertPage); + }, [isShowInsertPage]); + + const onAddNewPage = useCallback((newPage) => { + const folded = getFoldState(page.id); + if (folded) { + toggleExpand(page.id); } - }; + addPageInside(Object.assign({ parentPageId: page.id }, newPage)); + }, [page, addPageInside, getFoldState, toggleExpand]); - onChangeName = (newName) => { - this.setState({ pageName: newName }); - }; + const onAddSiblingPage = useCallback((newPage) => { + addSiblingPage(newPage, parentPageId, insertPosition, page.id, toggleInsertSiblingPage); + }, [page, parentPageId, addSiblingPage, insertPosition, toggleInsertSiblingPage]); - toggleDropdown = () => { - const isShowOperationDropdown = !this.state.isShowOperationDropdown; - this.setState({ isShowOperationDropdown }); - this.changeItemFreeze(isShowOperationDropdown); - }; - - changeItemFreeze = (isFreeze) => { - if (isFreeze) { - this.pageItemRef.classList.add('wiki-page-freezed'); - } else { - this.pageItemRef.classList.remove('wiki-page-freezed'); + const updateSelected = useCallback(() => { + const isCurrentSelected = getCurrentPageId() === page.id; + if (isSelected !== isCurrentSelected) { + setIsSelected(isCurrentSelected); } - }; + }, [page, isSelected, getCurrentPageId]); - setDocUuid = (docUuid) => { - window.seafile['docUuid'] = docUuid; - }; + useEffect(() => { + const unsubscribeUpdateCurrentPage = eventBus.subscribe('update-wiki-current-page', updateSelected); - getPageChildrenStyle = () => { - const folded = this.props.getFoldState(this.props.page.id); - return folded ? { height: 0, overflowY: 'hidden' } : { height: 'auto', overflowY: 'visible' }; - }; + return () => { + unsubscribeUpdateCurrentPage(); + }; + }, [updateSelected]); - onClickPageChildren = (e) => { - e.stopPropagation(); - e.nativeEvent.stopImmediatePropagation(); - }; - - renderPage = (page, index, pagesLength, isOnlyOnePage) => { + const renderPage = (page, index, pagesLength, isOnlyOnePage) => { if (!page) return; - const { pages, pathStr } = this.props; - const id = page.id; - if (!pages.find(item => item.id === id)) return; + if (!pages.find(item => item.id === page.id)) return; return ( - item.id === id), page)} + item.id === page.id), page)} pageIndex={index} - parentPageId={this.props.page.id} - duplicatePage={this.props.duplicatePage} - setCurrentPage={this.props.setCurrentPage} - onUpdatePage={this.props.onUpdatePage} - onDeletePage={this.props.onDeletePage} - onMovePage={this.props.onMovePage} - updateWikiConfig={this.props.updateWikiConfig} pages={pages} + pagesLength={pagesLength} + parentPageId={page.id} + isOblyOnePage={isOnlyOnePage} + duplicatePage={duplicatePage} + setCurrentPage={setCurrentPage} + onUpdatePage={onUpdatePage} + onDeletePage={onDeletePage} + onMovePage={onMovePage} + updateWikiConfig={updateWikiConfig} pathStr={pathStr + '-' + page.id} - getCurrentPageId={this.props.getCurrentPageId} - addPageInside={this.props.addPageInside} - getFoldState={this.props.getFoldState} - toggleExpand={this.props.toggleExpand} - setClassName={this.props.setClassName} - getClassName={this.props.getClassName} - layerDragProps={this.props.layerDragProps} - addSiblingPage={this.props.addSiblingPage} + getCurrentPageId={getCurrentPageId} + addPageInside={addPageInside} + addSiblingPage={addSiblingPage} + getFoldState={getFoldState} + toggleExpand={toggleExpand} + setClassName={setClassName} + getClassName={getClassName} /> ); }; - toggleExpand = (e) => { - e.stopPropagation(); - e.nativeEvent.stopImmediatePropagation(); - this.props.toggleExpand(this.props.page.id); - this.forceUpdate(); + const childNumber = Array.isArray(page.children) ? page.children.length : 0; + const customIcon = page.icon; + const navItemId = `page-editor-${page.id}`; + const pageChildrenStyle = getFoldState(page.id) ? { + height: 0, overflowY: 'hidden' + } : { + height: 'auto', overflowY: 'visible' }; - onClickPageItem = () => { - if (!this.state.isShowNameEditor) { - this.props.setCurrentPage(this.props.page.id); - } - }; - - onAddNewPage = (newPage) => { - const { page } = this.props; - const folded = this.props.getFoldState(page.id); - if (folded) { - this.props.toggleExpand(page.id); - } - this.props.addPageInside(Object.assign({ parentPageId: this.props.page.id }, newPage)); - }; - - onAddSiblingPage = (newPage) => { - const { page } = this.props; - this.props.addSiblingPage(newPage, this.props.parentPageId, this.state.insertPosition, page.id, this.toggleInsertSiblingPage); - }; - - getPageClassName = () => { - const { isOver, canDrop, layerDragProps } = this.props; - const isOverPage = isOver && canDrop; - const readonly = wikiPermission === 'r' || wikiPermission === 'public'; - if (!isOverPage || ! layerDragProps || !layerDragProps.clientOffset) { - return classnames('wiki-page-item', { 'selected-page': this.state.isSelected }, { 'readonly': readonly }); - } - let y = layerDragProps.clientOffset.y; - let top = this.pageItemRef.getBoundingClientRect().y; - const className = classnames( - 'wiki-page-item', - { 'dragged-page-over': (top + 10 < y && y < top + 30) }, - { 'page-can-drop-top': (top + 10 > y) }, - { 'page-can-drop-bottom': (top + 30 < y) }, - { 'selected-page': this.state.isSelected }, - { 'readonly': readonly }, - ); - this.props.setClassName(className); - return className; - }; - - render() { - const { connectDragSource, connectDragPreview, connectDropTarget, page, pagesLength, isOnlyOnePage, pathStr } = this.props; - const { isShowNameEditor, pageName, isSelected, isMouseEnter } = this.state; - if (isSelected) this.setDocUuid(page.docUuid); - let navItemId = `page-editor-${page.id}`; - let childNumber = Array.isArray(page.children) ? page.children.length : 0; - const customIcon = page.icon; - const folded = this.props.getFoldState(page.id); - if (wikiPermission === 'rw') { - return ( -
- {connectDragSource(connectDropTarget(connectDragPreview( + if (wikiPermission === 'rw') { + return ( + <> +
+
this.pageItemRef = ref} - onMouseEnter={this.onMouseEnter} - onMouseMove={this.onMouseMove} - onMouseLeave={this.onMouseLeave} - id={navItemId} + className="wiki-page-content" + style={pathStr ? { + marginLeft: (pathStr.split('-').length - 1) * 24 + } : {}} > -
-
- {childNumber === 0 && (customIcon ? : ) - } - {(!isMouseEnter && childNumber > 0) && (customIcon ? : )} - {(isMouseEnter && childNumber > 0) && -
- -
- } - {page.name} - {isShowNameEditor && ( - - )} + {childNumber === 0 && (customIcon ? ( + + ) : ( + + ))} + {(!isMouseEntered && childNumber > 0) && (customIcon ? ( + + ) : ( + + ))} + {(isMouseEntered && childNumber > 0) && ( +
toggleExpand(page.id)}> +
-
-
-
- - {this.state.isShowOperationDropdown && - - } -
-
- -
-
- {this.state.isShowInsertPage && - {page.name} + {isShowNameEditor && ( + - } - {this.state.isShowAddSiblingPage && - +
+
+
+ + {isShowOperationDropdown && ( + onDeletePage(page.id)} + toggleInsertSiblingPage={toggleInsertSiblingPage} /> - } + )}
- )))} -
- {page.children && - page.children.map((item, index) => { - return this.renderPage(item, index, pagesLength, isOnlyOnePage); - }) - } -
-
- ); - } else { - // permission is 'r' or 'public' - return ( -
-
this.pageItemRef = ref} - onMouseEnter={this.onMouseEnter} - onMouseMove={this.onMouseMove} - onMouseLeave={this.onMouseLeave} - id={navItemId} - > -
-
- {childNumber === 0 && (customIcon ? : ) - } - {(!isMouseEnter && childNumber > 0) && (customIcon ? : )} - {(isMouseEnter && childNumber > 0) && -
- -
- } - {page.name} -
+
+
-
-
-
- {page.children && - page.children.map((item, index) => { - return this.renderPage(item, index, pagesLength, isOnlyOnePage); - }) - }
+ {isShowInsertPage && ( + + )} + {isShowAddSiblingPage && ( + + )}
- ); - } +
{ + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + }} + > + {page.children && page.children.map((item, index) => renderPage(item, index, pagesLength, isOnlyOnePage))} +
+ + ); } -} + + return ( + <> +
+
+
+ {childNumber === 0 && (customIcon ? ( + + ) : ( + + ))} + {(!isMouseEntered && childNumber > 0) && (customIcon ? ( + + ) : ( + + ))} + {(isMouseEntered && childNumber > 0) && ( +
toggleExpand(page.id)}> + +
+ )} + {page.name} +
+
+
+
+
{ + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + }} + > + {page.children && page.children.map((item, index) => renderPage(item, index, pagesLength, isOnlyOnePage))} +
+ + ); +}; PageItem.propTypes = { - isOver: PropTypes.bool, - canDrop: PropTypes.bool, - isDragging: PropTypes.bool, - draggedPage: PropTypes.object, page: PropTypes.object, pages: PropTypes.array, pageIndex: PropTypes.number, pagesLength: PropTypes.number, - connectDragSource: PropTypes.func, - connectDragPreview: PropTypes.func, - connectDropTarget: PropTypes.func, + parentPageId: PropTypes.string, + addSiblingPage: PropTypes.func, duplicatePage: PropTypes.func, setCurrentPage: PropTypes.func, onUpdatePage: PropTypes.func, diff --git a/frontend/src/pages/wiki2/wiki-nav/wiki-nav.js b/frontend/src/pages/wiki2/wiki-nav/wiki-nav.js index f835a38a4c..5eea6e1ea0 100644 --- a/frontend/src/pages/wiki2/wiki-nav/wiki-nav.js +++ b/frontend/src/pages/wiki2/wiki-nav/wiki-nav.js @@ -2,9 +2,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { UncontrolledTooltip } from 'reactstrap'; -import { DropTarget, DragLayer } from 'react-dnd'; -import html5DragDropContext from './html5DragDropContext'; -import DraggedPageItem from './pages/dragged-page-item'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import PageItem from './pages/page-item'; import { gettext, wikiPermission } from '../../../utils/constants'; import { Utils } from '../../../utils/utils'; @@ -30,28 +30,34 @@ class WikiNav extends Component { constructor(props) { super(props); + this.state = { + idFoldedStatusMap: {}, // Move idFoldedStatusMap to state + }; this.folderClassNameCache = ''; this.lastScrollTop = 0; this.wikiNavBodyRef = React.createRef(); - // set init pages are all folded - this.idFoldedStatusMap = {}; + // Initialize pages as folded + const idFoldedStatusMap = {}; props.pages.forEach((page) => { - this.idFoldedStatusMap[page.id] = true; + idFoldedStatusMap[page.id] = true; }); + this.state.idFoldedStatusMap = idFoldedStatusMap; } getFoldState = (pageId) => { - return this.idFoldedStatusMap[pageId]; + return this.state.idFoldedStatusMap[pageId]; }; toggleExpand = (pageId) => { - const idFoldedStatusMap = this.idFoldedStatusMap; - if (idFoldedStatusMap[pageId]) { - delete idFoldedStatusMap[pageId]; - } else { - idFoldedStatusMap[pageId] = true; - } - this.idFoldedStatusMap = idFoldedStatusMap; + this.setState((prevState) => { + const idFoldedStatusMap = { ...prevState.idFoldedStatusMap }; + if (idFoldedStatusMap[pageId]) { + delete idFoldedStatusMap[pageId]; + } else { + idFoldedStatusMap[pageId] = true; + } + return { idFoldedStatusMap }; + }); }; componentDidUpdate(prevProps) { @@ -72,12 +78,12 @@ class WikiNav extends Component { return this.folderClassNameCache; }; - renderPage = (page, index, pagesLength, isOnlyOnePage, id_page_map, layerDragProps) => { + renderPage = (page, index, pagesLength, isOnlyOnePage, id_page_map) => { const { pages } = this.props; const id = page.id; if (!pages.find(item => item.id === id)) return; return ( - @@ -105,7 +111,7 @@ class WikiNav extends Component { }; // eslint-disable-next-line - renderStructureBody = React.forwardRef((layerDragProps, ref) => { + renderStructureBody = () => { const { navigation, pages } = this.props; const pagesLen = pages.length; const isOnlyOnePage = pagesLen === 1; @@ -131,7 +137,7 @@ class WikiNav extends Component { }
{navigation.map((item, index) => { - return this.renderPage(item, index, pages.length, isOnlyOnePage, id_page_map, layerDragProps); + return this.renderPage(item, index, pages.length, isOnlyOnePage, id_page_map); })} {wikiPermission === 'rw' && <> @@ -146,27 +152,15 @@ class WikiNav extends Component { }
); - }); - - collect = (monitor) => { - return { - item: monitor.getItem(), - itemType: monitor.getItemType(), - clientOffset: monitor.getClientOffset(), - isDragging: monitor.isDragging() - }; }; render() { - const StructureBody = html5DragDropContext( - DropTarget('WikiNav', {}, connect => ({ - connectDropTarget: connect.dropTarget() - }))(DragLayer(this.collect)(this.renderStructureBody)) - ); return ( -
- -
+ +
+ {this.renderStructureBody()} +
+
); } }