1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-27 07:44:50 +00:00

Upgrade/react dnd (#7767)

* upgrade react-dnd react-dnd-html5-backend

* update groupbys

* update single select option

* update wiki nav

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
Aries
2025-04-28 14:38:12 +08:00
committed by GitHub
parent 668281b099
commit 8241d6950f
13 changed files with 847 additions and 909 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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(
<div
className={classnames('groupby-item',
{ 'group-can-drop-top': isOver && canDrop && isDragging },
{ 'group-can-drop': isOver && canDrop && !isDragging }
)}
>
{!readOnly && (
<div className="delete-groupby" onClick={deleteGroupby} aria-label={gettext('Delete')}>
<Icon className="sf-metadata-icon" symbol="fork-number"/>
</div>
)}
<div className="condition">
<div className="groupby-column">
return (
<div
ref={ref}
className={classnames('groupby-item',
{ 'group-can-drop-top': isOver && canDrop && dropPosition === 'top' },
{ 'group-can-drop-bottom': isOver && canDrop && dropPosition === 'bottom' }
)}
>
{!readOnly && (
<div className="delete-groupby" onClick={deleteGroupby} aria-label={gettext('Delete')}>
<Icon className="sf-metadata-icon" symbol="fork-number"/>
</div>
)}
<div className="condition">
<div className="groupby-column">
<CustomizeSelect
readOnly={readOnly}
value={selectedColumn}
options={columnsOptions}
onSelectOption={selectColumn}
searchable={true}
searchPlaceholder={gettext('Search property')}
noOptionsPlaceholder={gettext('No results')}
/>
</div>
{isShowGroupCountType(column) && (
<div className="groupby-count-type">
<CustomizeSelect
readOnly={readOnly}
value={selectedColumn}
options={columnsOptions}
onSelectOption={selectColumn}
searchable={true}
searchPlaceholder={gettext('Search property')}
noOptionsPlaceholder={gettext('No results')}
value={selectedCountType}
onSelectOption={selectCountType}
options={countTypeOptions}
/>
</div>
{isShowGroupCountType(column) && (
<div className="groupby-count-type">
<CustomizeSelect
readOnly={readOnly}
value={selectedCountType}
onSelectOption={selectCountType}
options={countTypeOptions}
/>
</div>
)}
<div className="groupby-predicate">
{(!column.key || SORT_COLUMN_OPTIONS.includes(column.type)) && (
<CustomizeSelect
readOnly={readOnly}
value={selectedSortType}
options={sortOptions}
onSelectOption={selectSortType}
/>
)}
</div>
</div>
{!readOnly && showDragBtn && connectDragSource(
<div className="groupby-drag">
<Icon symbol="drag" />
</div>
)}
<div className="groupby-predicate">
{(!column.key || SORT_COLUMN_OPTIONS.includes(column.type)) && (
<CustomizeSelect
readOnly={readOnly}
value={selectedSortType}
options={sortOptions}
onSelectOption={selectSortType}
/>
)}
</div>
</div>
)
{!readOnly && showDragBtn && (
<div ref={drag} className="groupby-drag">
<Icon symbol="drag" />
</div>
)}
</div>
);
};
@@ -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;

View File

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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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(
<div
className={classnames('sf-metadata-edit-option-container', {
'sf-metadata-edit-option-can-drop': isOver && canDrop && !isDragging,
'sf-metadata-edit-deleting-option': isDeleting,
'sf-metadata-edit-option-can-drop-top': isOver && canDrop && isDragging,
'sf-metadata-edit-option-viewing': isViewing,
'sf-metadata-edit-option-editing': isEditing,
'sf-metadata-edit-option-disabled': isPredefined,
})}
onMouseEnter={() => onMouseEnter()}
onMouseLeave={onMouseLeave}
>
{connectDragSource(
<div className="sf-metadata-edit-option-drag-container">
<Icon symbol="drag" />
</div>
)}
<div className="sf-metadata-edit-option-content">
<Color option={option} onChange={onUpdate} isViewing={isViewing} isPredefined={isPredefined} />
<Name
option={option}
isPredefined={isPredefined}
isEditing={isEditing}
onChange={onUpdate}
onToggleFreeze={onToggleFreeze}
onOpen={onOpenNameEditor}
onClose={onCloseNameEditor}
/>
</div>
<div id={`sf-metadata-edit-option-more-operation-${option.id}`} className="sf-metadata-edit-option-more-operations">
{(isViewing || isDeleting) && (
<IconBtn className="sf-metadata-edit-option-operation-item" onClick={onDelete} symbol="delete" />
)}
</div>
return (
<div
ref={ref}
className={classnames('sf-metadata-edit-option-container', {
'sf-metadata-edit-deleting-option': isDeleting,
'sf-metadata-edit-option-drop-over-top': isOver && canDrop && dropPosition === 'top',
'sf-metadata-edit-option-drop-over-bottom': isOver && canDrop && dropPosition === 'bottom',
'sf-metadata-edit-option-viewing': isViewing,
'sf-metadata-edit-option-editing': isEditing,
'sf-metadata-edit-option-disabled': isPredefined,
})}
onMouseEnter={() => onMouseEnter()}
onMouseLeave={onMouseLeave}
>
<div ref={drag} className="sf-metadata-edit-option-drag-container">
<Icon symbol="drag" />
</div>
)
<div className="sf-metadata-edit-option-content">
<Color option={option} onChange={onUpdate} isViewing={isViewing} isPredefined={isPredefined} />
<Name
option={option}
isPredefined={isPredefined}
isEditing={isEditing}
onChange={onUpdate}
onToggleFreeze={onToggleFreeze}
onOpen={onOpenNameEditor}
onClose={onCloseNameEditor}
/>
</div>
<div id={`sf-metadata-edit-option-more-operation-${option.id}`} className="sf-metadata-edit-option-more-operations">
{(isViewing || isDeleting) && (
<IconBtn className="sf-metadata-edit-option-operation-item" onClick={onDelete} symbol="delete" />
)}
</div>
</div>
);
};
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;

View File

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

View File

@@ -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 (
<MetadataStatusProvider repoID={repoID} repoInfo={currentRepoInfo} hideMetadataView={this.hideMetadataView} statusCallback={this.metadataStatusCallback} >
<TagsProvider repoID={repoID} currentPath={path} repoInfo={currentRepoInfo} selectTagsView={this.onTreeNodeClick} tagsChangedCallback={this.tagsChangedCallback} >
<MetadataProvider repoID={repoID} currentPath={path} repoInfo={currentRepoInfo} selectMetadataView={this.onTreeNodeClick} >
<CollaboratorsProvider repoID={repoID}>
<div className="main-panel-center flex-row">
<div className="cur-view-container">
{this.state.currentRepoInfo.status === 'read-only' &&
<DndProvider backend={HTML5Backend}>
<MetadataStatusProvider repoID={repoID} repoInfo={currentRepoInfo} hideMetadataView={this.hideMetadataView} statusCallback={this.metadataStatusCallback} >
<TagsProvider repoID={repoID} currentPath={path} repoInfo={currentRepoInfo} selectTagsView={this.onTreeNodeClick} tagsChangedCallback={this.tagsChangedCallback} >
<MetadataProvider repoID={repoID} currentPath={path} repoInfo={currentRepoInfo} selectMetadataView={this.onTreeNodeClick} >
<CollaboratorsProvider repoID={repoID}>
<div className="main-panel-center flex-row">
<div className="cur-view-container">
{this.state.currentRepoInfo.status === 'read-only' &&
<div className="readonly-tip-message">
{gettext('This library has been set to read-only by admin and cannot be updated.')}
</div>
}
<div className="cur-view-path lib-cur-view-path">
<div className={classnames(
'cur-view-path-left', {
'w-100': !isDesktop,
'animation-children': isDirentSelected
})}>
{isDirentSelected ? (
currentMode === TAGS_MODE || currentMode === METADATA_MODE ? (
<MetadataPathToolbar repoID={repoID} repoInfo={currentRepoInfo} mode={currentMode} path={path} />
}
<div className="cur-view-path lib-cur-view-path">
<div className={classnames(
'cur-view-path-left', {
'w-100': !isDesktop,
'animation-children': isDirentSelected
})}>
{isDirentSelected ? (
currentMode === TAGS_MODE || currentMode === METADATA_MODE ? (
<MetadataPathToolbar repoID={repoID} repoInfo={currentRepoInfo} mode={currentMode} path={path} />
) : (
<SelectedDirentsToolbar
repoID={this.props.repoID}
path={this.state.path}
userPerm={userPerm}
repoEncrypted={this.state.repoEncrypted}
repoTags={this.state.repoTags}
selectedDirentList={this.state.selectedDirentList}
direntList={direntItemsList}
onItemsMove={this.onMoveItems}
onItemsCopy={this.onCopyItems}
onItemsDelete={this.onDeleteItems}
onItemRename={this.onMainPanelItemRename}
isRepoOwner={isRepoOwner}
currentRepoInfo={this.state.currentRepoInfo}
enableDirPrivateShare={enableDirPrivateShare}
updateDirent={this.updateDirent}
unSelectDirent={this.unSelectDirent}
onFilesTagChanged={this.onFileTagChanged}
showShareBtn={showShareBtn}
isGroupOwnedRepo={this.state.isGroupOwnedRepo}
showDirentDetail={this.showDirentDetail}
currentMode={this.state.currentMode}
onItemConvert={this.onConvertItem}
onAddFolder={this.onAddFolder}
/>
)
) : (
<SelectedDirentsToolbar
repoID={this.props.repoID}
path={this.state.path}
userPerm={userPerm}
repoEncrypted={this.state.repoEncrypted}
repoTags={this.state.repoTags}
selectedDirentList={this.state.selectedDirentList}
direntList={direntItemsList}
onItemsMove={this.onMoveItems}
onItemsCopy={this.onCopyItems}
onItemsDelete={this.onDeleteItems}
onItemRename={this.onMainPanelItemRename}
isRepoOwner={isRepoOwner}
<CurDirPath
currentRepoInfo={this.state.currentRepoInfo}
enableDirPrivateShare={enableDirPrivateShare}
updateDirent={this.updateDirent}
unSelectDirent={this.unSelectDirent}
onFilesTagChanged={this.onFileTagChanged}
showShareBtn={showShareBtn}
repoID={this.props.repoID}
repoName={this.state.currentRepoInfo.repo_name}
repoEncrypted={this.state.repoEncrypted}
isGroupOwnedRepo={this.state.isGroupOwnedRepo}
showDirentDetail={this.showDirentDetail}
currentMode={this.state.currentMode}
onItemConvert={this.onConvertItem}
pathPrefix={this.props.pathPrefix}
currentPath={this.state.path}
userPerm={userPerm}
onTabNavClick={this.props.onTabNavClick}
onPathClick={this.onMainNavBarClick}
fileTags={this.state.fileTags}
direntList={direntItemsList}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
sortItems={this.sortItems}
toggleTreePanel={this.toggleTreePanel}
enableDirPrivateShare={enableDirPrivateShare}
showShareBtn={showShareBtn}
onAddFolder={this.onAddFolder}
onAddFile={this.onAddFile}
onUploadFile={this.onUploadFile}
onUploadFolder={this.onUploadFolder}
fullDirentList={this.state.direntList}
filePermission={this.state.filePermission}
onFileTagChanged={this.onToolbarFileTagChanged}
repoTags={this.state.repoTags}
onItemMove={this.onMoveItem}
isDesktop={isDesktop}
loadDirentList={this.loadDirentList}
onAddFolderNode={this.onAddFolder}
/>
)
) : (
<CurDirPath
currentRepoInfo={this.state.currentRepoInfo}
repoID={this.props.repoID}
repoName={this.state.currentRepoInfo.repo_name}
repoEncrypted={this.state.repoEncrypted}
isGroupOwnedRepo={this.state.isGroupOwnedRepo}
pathPrefix={this.props.pathPrefix}
currentPath={this.state.path}
userPerm={userPerm}
onTabNavClick={this.props.onTabNavClick}
onPathClick={this.onMainNavBarClick}
fileTags={this.state.fileTags}
direntList={direntItemsList}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
sortItems={this.sortItems}
toggleTreePanel={this.toggleTreePanel}
enableDirPrivateShare={enableDirPrivateShare}
showShareBtn={showShareBtn}
onAddFolder={this.onAddFolder}
onAddFile={this.onAddFile}
onUploadFile={this.onUploadFile}
onUploadFolder={this.onUploadFolder}
fullDirentList={this.state.direntList}
filePermission={this.state.filePermission}
onFileTagChanged={this.onToolbarFileTagChanged}
repoTags={this.state.repoTags}
onItemMove={this.onMoveItem}
isDesktop={isDesktop}
loadDirentList={this.loadDirentList}
onAddFolderNode={this.onAddFolder}
/>
)}
</div>
{isDesktop &&
)}
</div>
{isDesktop &&
<div className="cur-view-path-right py-1">
<DirTool
repoID={this.props.repoID}
@@ -2453,135 +2456,136 @@ class LibContentView extends React.Component {
onCloseDetail={this.closeDirentDetail}
/>
</div>
}
</div>
<div className='cur-view-content lib-content-container' onScroll={this.onItemsScroll}>
{this.state.pathExist ?
<DirColumnView
isSidePanelFolded={this.props.isSidePanelFolded}
isTreePanelShown={this.state.isTreePanelShown}
isDirentDetailShow={this.state.isDirentDetailShow}
currentMode={this.state.currentMode}
path={this.state.path}
repoID={this.props.repoID}
currentRepoInfo={this.state.currentRepoInfo}
isGroupOwnedRepo={this.state.isGroupOwnedRepo}
userPerm={userPerm}
enableDirPrivateShare={enableDirPrivateShare}
isTreeDataLoading={this.state.isTreeDataLoading}
treeData={this.state.treeData}
currentNode={this.state.currentNode}
onNodeClick={this.onTreeNodeClick}
onNodeCollapse={this.onTreeNodeCollapse}
onNodeExpanded={this.onTreeNodeExpanded}
onAddFolderNode={this.onAddFolder}
onAddFileNode={this.onAddFile}
onRenameNode={this.onRenameTreeNode}
onDeleteNode={this.onDeleteTreeNode}
isViewFile={this.state.isViewFile}
isFileLoading={this.state.isFileLoading}
filePermission={this.state.filePermission}
content={this.state.content}
viewId={this.state.viewId}
tagId={this.state.tagId}
lastModified={this.state.lastModified}
latestContributor={this.state.latestContributor}
onLinkClick={this.onLinkClick}
isRepoInfoBarShow={isRepoInfoBarShow}
repoTags={this.state.repoTags}
usedRepoTags={this.state.usedRepoTags}
updateUsedRepoTags={this.updateUsedRepoTags}
isDirentListLoading={this.state.isDirentListLoading}
direntList={direntItemsList}
fullDirentList={this.state.direntList}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
sortItems={this.sortItems}
onAddFolder={this.onAddFolder}
onAddFile={this.onAddFile}
onItemClick={this.onItemClick}
onItemSelected={this.onDirentSelected}
onItemDelete={this.onMainPanelItemDelete}
onItemRename={this.onMainPanelItemRename}
deleteFilesCallback={this.deleteItemsAjaxCallback}
renameFileCallback={this.renameItemAjaxCallback}
onItemMove={this.onMoveItem}
moveFileCallback={this.moveItemsAjaxCallback}
onItemCopy={this.onCopyItem}
copyFileCallback={this.copyItemsAjaxCallback}
convertFileCallback={this.convertFileAjaxCallback}
onItemConvert={this.onConvertItem}
onDirentClick={this.onDirentClick}
updateDirent={this.updateDirent}
isAllItemSelected={this.state.isAllDirentSelected}
onAllItemSelected={this.onAllDirentSelected}
selectedDirentList={this.state.selectedDirentList}
onSelectedDirentListUpdate={this.onSelectedDirentListUpdate}
onItemsMove={this.onMoveItems}
onItemsCopy={this.onCopyItems}
onItemsDelete={this.onDeleteItems}
onFileTagChanged={this.onFileTagChanged}
showDirentDetail={this.showDirentDetail}
onItemsScroll={this.onItemsScroll}
eventBus={this.props.eventBus}
updateCurrentDirent={this.updateCurrentDirent}
updateCurrentPath={this.updatePath}
toggleShowDirentToolbar={this.toggleShowDirentToolbar}
updateTreeNode={this.updateTreeNode}
/>
:
<div className="message err-tip">{gettext('Folder does not exist.')}</div>
}
{!isCustomPermission && this.state.isDirentDetailShow && (
<Detail
path={detailPath}
repoID={this.props.repoID}
currentRepoInfo={{ ...this.state.currentRepoInfo }}
dirent={detailDirent}
repoTags={this.state.repoTags}
fileTags={this.state.isViewFile ? this.state.fileTags : []}
onFileTagChanged={this.onFileTagChanged}
onClose={this.closeDirentDetail}
currentMode={this.state.currentMode}
/>
)}
}
</div>
<div className='cur-view-content lib-content-container' onScroll={this.onItemsScroll}>
{this.state.pathExist ?
<DirColumnView
isSidePanelFolded={this.props.isSidePanelFolded}
isTreePanelShown={this.state.isTreePanelShown}
isDirentDetailShow={this.state.isDirentDetailShow}
currentMode={this.state.currentMode}
path={this.state.path}
repoID={this.props.repoID}
currentRepoInfo={this.state.currentRepoInfo}
isGroupOwnedRepo={this.state.isGroupOwnedRepo}
userPerm={userPerm}
enableDirPrivateShare={enableDirPrivateShare}
isTreeDataLoading={this.state.isTreeDataLoading}
treeData={this.state.treeData}
currentNode={this.state.currentNode}
onNodeClick={this.onTreeNodeClick}
onNodeCollapse={this.onTreeNodeCollapse}
onNodeExpanded={this.onTreeNodeExpanded}
onAddFolderNode={this.onAddFolder}
onAddFileNode={this.onAddFile}
onRenameNode={this.onRenameTreeNode}
onDeleteNode={this.onDeleteTreeNode}
isViewFile={this.state.isViewFile}
isFileLoading={this.state.isFileLoading}
filePermission={this.state.filePermission}
content={this.state.content}
viewId={this.state.viewId}
tagId={this.state.tagId}
lastModified={this.state.lastModified}
latestContributor={this.state.latestContributor}
onLinkClick={this.onLinkClick}
isRepoInfoBarShow={isRepoInfoBarShow}
repoTags={this.state.repoTags}
usedRepoTags={this.state.usedRepoTags}
updateUsedRepoTags={this.updateUsedRepoTags}
isDirentListLoading={this.state.isDirentListLoading}
direntList={direntItemsList}
fullDirentList={this.state.direntList}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
sortItems={this.sortItems}
onAddFolder={this.onAddFolder}
onAddFile={this.onAddFile}
onItemClick={this.onItemClick}
onItemSelected={this.onDirentSelected}
onItemDelete={this.onMainPanelItemDelete}
onItemRename={this.onMainPanelItemRename}
deleteFilesCallback={this.deleteItemsAjaxCallback}
renameFileCallback={this.renameItemAjaxCallback}
onItemMove={this.onMoveItem}
moveFileCallback={this.moveItemsAjaxCallback}
onItemCopy={this.onCopyItem}
copyFileCallback={this.copyItemsAjaxCallback}
convertFileCallback={this.convertFileAjaxCallback}
onItemConvert={this.onConvertItem}
onDirentClick={this.onDirentClick}
updateDirent={this.updateDirent}
isAllItemSelected={this.state.isAllDirentSelected}
onAllItemSelected={this.onAllDirentSelected}
selectedDirentList={this.state.selectedDirentList}
onSelectedDirentListUpdate={this.onSelectedDirentListUpdate}
onItemsMove={this.onMoveItems}
onItemsCopy={this.onCopyItems}
onItemsDelete={this.onDeleteItems}
onFileTagChanged={this.onFileTagChanged}
showDirentDetail={this.showDirentDetail}
onItemsScroll={this.onItemsScroll}
eventBus={this.props.eventBus}
updateCurrentDirent={this.updateCurrentDirent}
updateCurrentPath={this.updatePath}
toggleShowDirentToolbar={this.toggleShowDirentToolbar}
updateTreeNode={this.updateTreeNode}
/>
:
<div className="message err-tip">{gettext('Folder does not exist.')}</div>
}
{!isCustomPermission && this.state.isDirentDetailShow && (
<Detail
path={detailPath}
repoID={this.props.repoID}
currentRepoInfo={{ ...this.state.currentRepoInfo }}
dirent={detailDirent}
repoTags={this.state.repoTags}
fileTags={this.state.isViewFile ? this.state.fileTags : []}
onFileTagChanged={this.onFileTagChanged}
onClose={this.closeDirentDetail}
currentMode={this.state.currentMode}
/>
)}
</div>
</div>
{canUpload && this.state.pathExist && !this.state.isViewFile && ![METADATA_MODE, TAGS_MODE].includes(this.state.currentMode) && (
<FileUploader
ref={uploader => this.uploader = uploader}
dragAndDrop={true}
path={this.state.path}
repoID={this.props.repoID}
direntList={this.state.direntList}
onFileUploadSuccess={this.onFileUploadSuccess}
isCustomPermission={isCustomPermission}
/>
)}
</div>
{canUpload && this.state.pathExist && !this.state.isViewFile && ![METADATA_MODE, TAGS_MODE].includes(this.state.currentMode) && (
<FileUploader
ref={uploader => this.uploader = uploader}
dragAndDrop={true}
path={this.state.path}
repoID={this.props.repoID}
direntList={this.state.direntList}
onFileUploadSuccess={this.onFileUploadSuccess}
isCustomPermission={isCustomPermission}
{isCopyMoveProgressDialogShow && (
<CopyMoveDirentProgressDialog
type={this.state.asyncOperationType}
asyncOperatedFilesLength={this.state.asyncOperatedFilesLength}
asyncOperationProgress={this.state.asyncOperationProgress}
toggleDialog={this.onMoveProgressDialogToggle}
/>
)}
</div>
{isCopyMoveProgressDialogShow && (
<CopyMoveDirentProgressDialog
type={this.state.asyncOperationType}
asyncOperatedFilesLength={this.state.asyncOperatedFilesLength}
asyncOperationProgress={this.state.asyncOperationProgress}
toggleDialog={this.onMoveProgressDialogToggle}
/>
)}
{isDeleteFolderDialogOpen && (
<DeleteFolderDialog
repoID={this.props.repoID}
path={this.state.folderToDelete}
deleteFolder={this.deleteFolder}
toggleDialog={this.toggleDeleteFolderDialog}
/>
)}
<MediaQuery query="(max-width: 767.8px)">
<Modal zIndex="1030" isOpen={!isDesktop && this.state.isTreePanelShown} toggle={this.toggleTreePanel} contentClassName="d-none"></Modal>
</MediaQuery>
</CollaboratorsProvider>
</MetadataProvider>
</TagsProvider>
</MetadataStatusProvider>
{isDeleteFolderDialogOpen && (
<DeleteFolderDialog
repoID={this.props.repoID}
path={this.state.folderToDelete}
deleteFolder={this.deleteFolder}
toggleDialog={this.toggleDeleteFolderDialog}
/>
)}
<MediaQuery query="(max-width: 767.8px)">
<Modal zIndex="1030" isOpen={!isDesktop && this.state.isTreePanelShown} toggle={this.toggleTreePanel} contentClassName="d-none"></Modal>
</MediaQuery>
</CollaboratorsProvider>
</MetadataProvider>
</TagsProvider>
</MetadataStatusProvider>
</DndProvider>
);
}
}

View File

@@ -1,4 +0,0 @@
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
export default DragDropContext(HTML5Backend);

View File

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

View File

@@ -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 (
<DraggedPageItem
key={id}
pagesLength={pagesLength}
isOnlyOnePage={isOnlyOnePage}
page={Object.assign({}, pages.find(item => item.id === id), page)}
<PageItem
key={page.id}
page={Object.assign({}, pages.find(item => 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 (
<div>
{connectDragSource(connectDropTarget(connectDragPreview(
if (wikiPermission === 'rw') {
return (
<>
<div
id={navItemId}
ref={ref}
className={classnames('wiki-page-item', {
'selected-page': isSelected,
'dragged-page-over': isOver && canDrop && !dropPosition,
'page-can-drop-top': isOver && canDrop && dropPosition === 'top',
'page-can-drop-bottom': isOver && canDrop && dropPosition === 'bottom',
})}
onMouseEnter={onMouseEnter}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
>
<div className="wiki-page-item-main" onClick={onClickPageItem}>
<div
className={this.getPageClassName()}
ref={ref => 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
} : {}}
>
<div className="wiki-page-item-main" onClick={this.onClickPageItem}>
<div className='wiki-page-content' style={pathStr ? { marginLeft: `${(pathStr.split('-').length - 1) * 24}px` } : {}}>
{childNumber === 0 && (customIcon ? <CustomIcon icon={customIcon} /> : <NavItemIcon symbol={'file'} disable={true} />)
}
{(!isMouseEnter && childNumber > 0) && (customIcon ? <CustomIcon icon={customIcon} /> : <NavItemIcon symbol={'files'} disable={true} />)}
{(isMouseEnter && childNumber > 0) &&
<div className='nav-item-icon' onClick={this.toggleExpand} role='button'>
<i className={`sf3-font-down sf3-font ${folded ? 'rotate-270' : ''}`} aria-hidden="true"></i>
</div>
}
<span className="wiki-page-title text-truncate" title={page.name}>{page.name}</span>
{isShowNameEditor && (
<NameEditPopover
oldName={pageName}
targetId={navItemId}
onChangeName={this.onChangeName}
toggleEditor={this.toggleNameEditor}
/>
)}
{childNumber === 0 && (customIcon ? (
<CustomIcon icon={customIcon} />
) : (
<NavItemIcon symbol={'file'} disable={true} />
))}
{(!isMouseEntered && childNumber > 0) && (customIcon ? (
<CustomIcon icon={customIcon} />
) : (
<NavItemIcon symbol={'file'} disable={true} />
))}
{(isMouseEntered && childNumber > 0) && (
<div role="button" className="nav-item-icon" onClick={() => toggleExpand(page.id)}>
<i className={`sf3-font-down sf3-font ${getFoldState(page.id) ? 'rotate-270' : ''}`} aria-hidden="true"></i>
</div>
</div>
<div className="d-none d-md-flex">
<div className="more-wiki-page-operation" onClick={this.toggleDropdown}>
<Icon symbol={'more-level'} />
{this.state.isShowOperationDropdown &&
<PageDropdownMenu
page={page}
pages={this.props.pages}
pagesLength={pagesLength}
isOnlyOnePage={isOnlyOnePage}
toggle={this.toggleDropdown}
toggleNameEditor={this.toggleNameEditor}
duplicatePage={this.props.duplicatePage}
onDeletePage={this.props.onDeletePage.bind(this, page.id)}
toggleInsertSiblingPage={this.toggleInsertSiblingPage}
/>
}
</div>
<div className="wiki-add-page-btn" onClick={this.toggleInsertPage} role='button'>
<span className='sf3-font sf3-font-enlarge' aria-hidden="true"></span>
</div>
</div>
{this.state.isShowInsertPage &&
<AddNewPageDialog
toggle={this.toggleInsertPage}
onAddNewPage={this.onAddNewPage}
title={gettext('Add page inside')}
page={this.props.page}
)}
<span className="wiki-page-title text-truncate" title={page.name}>{page.name}</span>
{isShowNameEditor && (
<NameEditPopover
oldName={pageName}
target_page_id={navItemId}
onChangeName={onChangeName}
toggleEditor={toggleNameEditor}
/>
}
{this.state.isShowAddSiblingPage &&
<AddNewPageDialog
toggle={this.toggleInsertSiblingPage}
onAddNewPage={this.onAddSiblingPage}
title={gettext('Add page')}
insertPosition={this.state.insertPosition}
page={this.props.page}
)}
</div>
</div>
<div className="d-none d-md-flex">
<div className="more-wiki-page-operation" onClick={toggleDropdown}>
<Icon symbol="more-level" />
{isShowOperationDropdown && (
<PageDropdownMenu
page={page}
pages={pages}
pagesLength={pagesLength}
isOnlyOnePage={isOnlyOnePage}
toggle={toggleDropdown}
toggleNameEditor={toggleNameEditor}
duplicatePage={duplicatePage}
onDeletePage={() => onDeletePage(page.id)}
toggleInsertSiblingPage={toggleInsertSiblingPage}
/>
}
)}
</div>
)))}
<div className="page-children" style={this.getPageChildrenStyle()} onClick={this.onClickPageChildren}>
{page.children &&
page.children.map((item, index) => {
return this.renderPage(item, index, pagesLength, isOnlyOnePage);
})
}
</div>
</div>
);
} else {
// permission is 'r' or 'public'
return (
<div>
<div
className={this.getPageClassName()}
ref={ref => this.pageItemRef = ref}
onMouseEnter={this.onMouseEnter}
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}
id={navItemId}
>
<div className="wiki-page-item-main" onClick={this.onClickPageItem}>
<div className='wiki-page-content' style={pathStr ? { marginLeft: `${(pathStr.split('-').length - 1) * 24}px` } : {}}>
{childNumber === 0 && (customIcon ? <CustomIcon icon={customIcon} /> : <NavItemIcon symbol={'file'} disable={true} />)
}
{(!isMouseEnter && childNumber > 0) && (customIcon ? <CustomIcon icon={customIcon} /> : <NavItemIcon symbol={'files'} disable={true} />)}
{(isMouseEnter && childNumber > 0) &&
<div className='nav-item-icon' onClick={this.toggleExpand} role='button'>
<i className={`sf3-font-down sf3-font ${folded ? 'rotate-270' : ''}`} aria-hidden="true"></i>
</div>
}
<span className="wiki-page-title text-truncate" title={page.name}>{page.name}</span>
</div>
<div className="wiki-add-page-btn" onClick={toggleInsertPage} role='button'>
<span className='sf3-font sf3-font-enlarge' aria-hidden="true"></span>
</div>
<div className="d-none d-md-flex"></div>
</div>
<div className="page-children" style={this.getPageChildrenStyle()} onClick={this.onClickPageChildren}>
{page.children &&
page.children.map((item, index) => {
return this.renderPage(item, index, pagesLength, isOnlyOnePage);
})
}
</div>
{isShowInsertPage && (
<AddNewPageDialog
toggle={toggleInsertPage}
onAddNewPage={onAddNewPage}
title={gettext('Add page inside')}
page={page}
/>
)}
{isShowAddSiblingPage && (
<AddNewPageDialog
toggle={toggleInsertSiblingPage}
onAddNewPage={onAddSiblingPage}
title={gettext('Add page')}
insertPosition={insertPosition}
page={page}
/>
)}
</div>
);
}
<div
className="page-children"
style={pageChildrenStyle}
onClick={(e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}}
>
{page.children && page.children.map((item, index) => renderPage(item, index, pagesLength, isOnlyOnePage))}
</div>
</>
);
}
}
return (
<>
<div
id={navItemId}
ref={ref}
className={classnames('wiki-page-item readonly', {
'selected-page': isSelected,
'dragged-page-over': isOver && canDrop,
'page-can-drop-top': isOver && canDrop && dropPosition === 'top',
'page-can-drop-bottom': isOver && canDrop && dropPosition === 'bottom',
})}
onMouseEnter={onMouseEnter}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
>
<div className="wiki-page-item-main" onClick={onClickPageItem}>
<div
className="wiki-page-content"
style={pathStr ? {
marginLeft: (pathStr.split('-').length - 1) * 24,
} : {}}
>
{childNumber === 0 && (customIcon ? (
<CustomIcon icon={customIcon} />
) : (
<NavItemIcon symbol="file" disable={true} />
))}
{(!isMouseEntered && childNumber > 0) && (customIcon ? (
<CustomIcon icon={customIcon} />
) : (
<NavItemIcon symbol="files" disable={true} />
))}
{(isMouseEntered && childNumber > 0) && (
<div role="button" className="nav-item-icon" onClick={() => toggleExpand(page.id)}>
<i className={`sf3-font-down sf3-font ${getFoldState(page.id) ? 'rotate-270' : ''}`} aria-hidden="true"></i>
</div>
)}
<span className="wiki-page-title text-truncate">{page.name}</span>
</div>
</div>
<div className="d-none d-md-flex"></div>
</div>
<div
className="page-children"
style={pageChildrenStyle}
onClick={(e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}}
>
{page.children && page.children.map((item, index) => renderPage(item, index, pagesLength, isOnlyOnePage))}
</div>
</>
);
};
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,

View File

@@ -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 (
<DraggedPageItem
<PageItem
key={id}
pagesLength={pagesLength}
isOnlyOnePage={isOnlyOnePage}
@@ -94,10 +100,10 @@ class WikiNav extends Component {
getCurrentPageId={this.props.getCurrentPageId}
addPageInside={this.props.addPageInside}
addSiblingPage={this.props.addSiblingPage}
idFoldedStatusMap={this.idFoldedStatusMap}
getFoldState={this.getFoldState}
toggleExpand={this.toggleExpand}
id_page_map={id_page_map}
layerDragProps={layerDragProps}
setClassName={this.setClassName}
getClassName={this.getClassName}
/>
@@ -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 {
}
</div>
{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 {
}
</div>
);
});
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 (
<div className='wiki-nav'>
<StructureBody />
</div>
<DndProvider backend={HTML5Backend}>
<div className='wiki-nav'>
{this.renderStructureBody()}
</div>
</DndProvider>
);
}
}