1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-20 19:08:21 +00:00

Feature/modify tag links by drag and drop (#7469)

* change tags link by drag and drop

* optimize

* optimize drag image

* optimize drag effect

* update codes

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
Co-authored-by: renjie-run <rj.aiyayao@gmail.com>
This commit is contained in:
Aries
2025-02-19 10:24:30 +08:00
committed by GitHub
parent 680006a883
commit c957dd238f
18 changed files with 431 additions and 19 deletions

View File

@@ -158,6 +158,8 @@ SFTable.propTypes = {
onGridKeyUp: PropTypes.func,
loadMore: PropTypes.func,
loadAll: PropTypes.func,
moveRecords: PropTypes.func,
renderCustomDraggedRows: PropTypes.func,
};
export default SFTable;

View File

@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { HorizontalScrollbar } from '../../scrollbar';
import RecordsHeader from '../records-header';
import Body from './body';
@@ -7,6 +8,7 @@ import TreeBody from './tree-body';
import GroupBody from './group-body';
import RecordsFooter from '../records-footer';
import ContextMenu from '../../context-menu';
import RecordDragLayer from './record-drag-layer';
import { RecordMetrics } from '../../utils/record-metrics';
import { TreeMetrics } from '../../utils/tree-metrics';
import { recalculate } from '../../utils/column';
@@ -43,6 +45,7 @@ class Records extends Component {
const { width: tableContentWidth } = props.getTableContentRect();
const initHorizontalScrollState = this.getHorizontalScrollState({ gridWidth: tableContentWidth, columnMetrics, scrollLeft: 0 });
this.state = {
draggingRecordSource: null,
columnMetrics,
recordMetrics: this.createRowMetrics(),
treeMetrics: this.createTreeMetrics(),
@@ -832,13 +835,66 @@ class Records extends Component {
return columnVisibleEnd;
};
renderRecordsBody = ({ containerWidth }) => {
const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx } = this.state;
handleDragRecordsEnd = () => {
this.setState({ draggingRecordSource: null });
};
handleDragRecordStart = (event, { draggingRecordId, draggingTreeNodeKey }) => {
this.setState({
draggingRecordSource: {
event,
draggingRecordId,
draggingTreeNodeKey
}
});
};
handleDropRecords = ({ dropRecordId, dropTreeNodeKey } = {}) => {
const { showRecordAsTree } = this.props;
const { draggingRecordSource, treeMetrics, recordMetrics } = this.state;
const dropTarget = showRecordAsTree ? dropTreeNodeKey : dropRecordId;
if (!dropTarget) return;
const { draggingRecordId, draggingTreeNodeKey } = draggingRecordSource;
const draggingSource = showRecordAsTree ? TreeMetrics.getDraggedTreeNodesKeys(draggingTreeNodeKey, treeMetrics) : RecordMetrics.getDraggedRecordsIds(draggingRecordId, recordMetrics);
this.props.moveRecords({ draggingSource, dropTarget });
this.handleDragRecordsEnd();
};
getRecordDragDropEvents = () => {
if (!this.props.moveRecords) return null;
if (!this.recordDragDropEvents) {
this.recordDragDropEvents = {
onDragStart: this.handleDragRecordStart,
onDrop: this.handleDropRecords,
onDragEnd: this.handleDragRecordsEnd,
};
}
return this.recordDragDropEvents;
};
createRecordsDragLayer = () => {
const { draggingRecordSource, recordMetrics, treeMetrics } = this.state;
if (!draggingRecordSource) return null;
return (
<RecordDragLayer
showRecordAsTree={this.props.showRecordAsTree}
draggingRecordSource={draggingRecordSource}
recordMetrics={recordMetrics}
treeMetrics={treeMetrics}
renderCustomDraggedRows={this.props.renderCustomDraggedRows}
/>
);
};
renderRecordsBody = ({ containerWidth, recordDraggable }) => {
const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx, draggingRecordSource } = this.state;
const { columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth } = columnMetrics;
const recordDragDropEvents = this.getRecordDragDropEvents();
const commonProps = {
...this.props,
columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth,
recordMetrics, colOverScanStartIdx, colOverScanEndIdx,
recordMetrics, colOverScanStartIdx, colOverScanEndIdx, recordDraggable, recordDragDropEvents, draggingRecordSource,
contextMenu: (
<ContextMenu
{...this.props}
@@ -900,11 +956,12 @@ class Records extends Component {
const containerWidth = this.getContainerWidth();
const hasSelectedRecord = this.checkHasSelectedRecord();
const isSelectedAll = this.checkIsSelectAll();
const recordDraggable = !!this.props.moveRecords;
return (
<>
<div
className={`sf-table-result-container ${this.isWindows ? 'windows-browser' : ''}`}
className={classnames('sf-table-result-container', { 'windows-browser': this.isWindows, 'record-draggable': recordDraggable })}
ref={this.setResultContainerRef}
onScroll={this.onContentScroll}
onClick={this.onClickContainer}
@@ -933,9 +990,10 @@ class Records extends Component {
modifyColumnWidth={this.props.modifyColumnWidth}
insertColumn={this.props.insertColumn}
/>
{this.renderRecordsBody({ containerWidth })}
{this.renderRecordsBody({ containerWidth, recordDraggable })}
</div>
</div>
{this.createRecordsDragLayer()}
{this.isWindows && this.isWebkit && (
<HorizontalScrollbar
ref={this.setHorizontalScrollbarRef}
@@ -1010,6 +1068,7 @@ Records.propTypes = {
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
moveRecord: PropTypes.func,
addFolder: PropTypes.func,
moveRecords: PropTypes.func,
};
export default Records;

View File

@@ -0,0 +1,36 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { TreeMetrics } from '../../utils/tree-metrics';
import { RecordMetrics } from '../../utils/record-metrics';
const RecordDragLayer = ({ showRecordAsTree, draggingRecordSource, recordMetrics, treeMetrics, renderCustomDraggedRows }) => {
const layerRef = useRef(null);
useEffect(() => {
if (layerRef.current && draggingRecordSource.event) {
draggingRecordSource.event.dataTransfer.setDragImage(layerRef.current, 15, 15);
}
}, [draggingRecordSource]);
const getDraggedRowsIds = useCallback(() => {
const { draggingRecordId, draggingTreeNodeKey } = draggingRecordSource;
return showRecordAsTree ? TreeMetrics.getDraggedTreeNodesKeys(draggingTreeNodeKey, treeMetrics) : RecordMetrics.getDraggedRecordsIds(draggingRecordId, recordMetrics);
}, [showRecordAsTree, draggingRecordSource, treeMetrics, recordMetrics]);
const renderDraggedRows = useCallback(() => {
const draggedRowsIds = getDraggedRowsIds();
if (renderCustomDraggedRows) {
return renderCustomDraggedRows(draggedRowsIds);
}
return null;
}, [getDraggedRowsIds, renderCustomDraggedRows]);
return (
<div className="sf-table-record-drag-layer" ref={layerRef}>
<table className='record-drag-layer-table'>
<tbody>{renderDraggedRows()}</tbody>
</table>
</div>
);
};
export default RecordDragLayer;

View File

@@ -70,6 +70,14 @@ class ActionsCell extends Component {
onMouseEnter={this.onCellMouseEnter}
onMouseLeave={this.onCellMouseLeave}
>
{this.props.recordDraggable &&
<div
draggable
className="drag-handler"
onDragStart={this.props.handleDragStart}
>
</div>
}
{!isSelected && <div className="sf-table-column-content row-index text-truncate">{this.getRecordNo()}</div>}
<div className="sf-table-column-content actions-checkbox">
<div className="select-cell-checkbox-container" onClick={this.props.onSelectRecord}>
@@ -100,10 +108,12 @@ ActionsCell.propTypes = {
isLocked: PropTypes.bool,
isSelected: PropTypes.bool,
isLastFrozenCell: PropTypes.bool,
recordDraggable: PropTypes.bool,
recordId: PropTypes.string,
index: PropTypes.number,
height: PropTypes.number,
onSelectRecord: PropTypes.func,
handleDragStart: PropTypes.func,
};
export default ActionsCell;

View File

@@ -24,3 +24,62 @@
.frozen-columns {
background-color: #fff;
}
.sf-table-result-content .sf-table-row.can-drop-tip .sf-table-cell {
background-color: rgb(200, 220, 240) !important;
}
.sf-table-result-container.record-draggable .sf-table-row .drag-handler {
position: absolute;
left: 0;
top: 1px;
height: 30px;
width: 12px;
}
.sf-table-result-container.record-draggable .sf-table-row:hover .drag-handler {
background-image: url(../../../../../../../media/img/grippy_large.png);
background-repeat: no-repeat;
cursor: grab;
}
.sf-table-record-drag-layer {
position: fixed;
z-index: -1;
left: -9999;
top: 0;
pointer-events: none;
cursor: grabbing;
}
.sf-table-record-drag-layer .record-drag-layer-table {
width: auto;
}
.sf-table-record-drag-layer .rdg-dragged-record {
display: flex;
min-height: 32px;
height: auto;
padding: 0 1rem;
border-left: 1px solid #ddd;
border-bottom: 1px solid #ddd;
border-right: 1px solid #ddd;
background-color: #fff;
}
.sf-table-record-drag-layer .rdg-dragged-record:first-child {
border-top: 1px solid #ddd;
}
.sf-table-record-drag-layer .rdg-dragged-record .rdg-dragged-record-cell {
display: flex;
align-items: center;
min-width: 80px;
height: 32px;
padding: 8px;
border-bottom: none;
}
.sf-table-record-drag-layer .rdg-dragged-record .rdg-dragged-record-cell:last-child {
border-right: none;
}

View File

@@ -10,11 +10,18 @@ import './index.css';
class Record extends React.Component {
constructor(props) {
super(props);
this.state = {
canDropTip: false,
};
}
componentDidMount() {
this.checkScroll();
}
shouldComponentUpdate(nextProps) {
shouldComponentUpdate(nextProps, nextState) {
return (
nextProps.isGroupView !== this.props.isGroupView ||
nextProps.hasSelectedCell !== this.props.hasSelectedCell ||
@@ -40,7 +47,8 @@ class Record extends React.Component {
nextProps.treeNodeKey !== this.props.treeNodeKey ||
nextProps.treeNodeDepth !== this.props.treeNodeDepth ||
nextProps.hasChildNodes !== this.props.hasChildNodes ||
nextProps.isFoldedTreeNode !== this.props.isFoldedTreeNode
nextProps.isFoldedTreeNode !== this.props.isFoldedTreeNode ||
nextState.canDropTip !== this.state.canDropTip
);
}
@@ -222,23 +230,67 @@ class Record extends React.Component {
return style;
};
// handle drag copy
handleDragStart = (event) => {
event.stopPropagation();
if (!this.props.recordDraggable) return;
const { record, treeNodeKey } = this.props;
const draggingRecordSource = { draggingRecordId: record._id, draggingTreeNodeKey: treeNodeKey };
event.dataTransfer.effectAllowed = 'move';
this.props.recordDragDropEvents.onDragStart(event, draggingRecordSource);
};
checkHasDraggedRecord = () => {
return !!this.props.draggingRecordSource;
};
checkOverDraggingRecord = () => {
const { draggingRecordSource, record, treeNodeKey, showRecordAsTree } = this.props;
if (!this.checkHasDraggedRecord()) return false;
const { draggingRecordId, draggingTreeNodeKey } = draggingRecordSource;
if (showRecordAsTree) {
return draggingTreeNodeKey === treeNodeKey;
}
return draggingRecordId === record._id;
};
handleDragEnter = (e) => {
// Prevent default to allow drop
e.preventDefault();
const { index, groupRecordIndex, cellMetaData: { onDragEnter } } = this.props;
onDragEnter({ overRecordIdx: index, overGroupRecordIndex: groupRecordIndex });
if (this.checkHasDraggedRecord() && !this.checkOverDraggingRecord()) {
this.setState({ canDropTip: true });
}
};
handleDragLeave = (e) => {
const { clientX, clientY } = e;
const { left, top, width, height } = this.rowRef.getBoundingClientRect();
if (clientX > left && clientX < left + width && clientY > top && clientY < top + height - 2) return;
this.setState({ canDropTip: false });
};
handleDragOver = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
e.dataTransfer.dropEffect = this.checkHasDraggedRecord() ? 'move' : 'copy';
if (this.checkHasDraggedRecord() && !this.checkOverDraggingRecord()) {
this.setState({ canDropTip: true });
}
};
handleDrop = (e) => {
// The default in Firefox is to treat data in dataTransfer as a URL and perform navigation on it, even if the data type used is 'text'
// To bypass this, we need to capture and prevent the drop event.
e.preventDefault();
e.stopPropagation();
this.setState({ canDropTip: false });
if (!this.checkHasDraggedRecord() || this.checkOverDraggingRecord()) {
this.props.recordDragDropEvents.onDragEnd();
return;
}
const { record, treeNodeKey } = this.props;
const dropTarget = { dropRecordId: record._id, dropTreeNodeKey: treeNodeKey };
this.props.recordDragDropEvents.onDrop(dropTarget);
};
onDragEnd = () => {
this.props.recordDragDropEvents.onDragEnd();
};
render() {
@@ -253,15 +305,19 @@ class Record extends React.Component {
return (
<div
ref={rowRef => this.rowRef = rowRef}
className={classnames('sf-table-row', {
'sf-table-last-row': isLastRecord,
'row-selected': isSelected,
'row-locked': isLocked
'row-locked': isLocked,
'can-drop-tip': this.state.canDropTip,
})}
style={this.getRecordStyle()}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}
onDragEnd={this.onDragEnd}
>
{/* frozen */}
<div
@@ -280,6 +336,8 @@ class Record extends React.Component {
onSelectRecord={this.onSelectRecord}
isLastFrozenCell={!lastFrozenColumnKey}
height={cellHeight}
recordDraggable={this.props.recordDraggable}
handleDragStart={this.handleDragStart}
/>
}
{frozenCells}
@@ -311,6 +369,7 @@ Record.propTypes = {
top: PropTypes.number,
left: PropTypes.number,
height: PropTypes.number,
recordDraggable: PropTypes.bool,
selectNoneCells: PropTypes.func,
onSelectRecord: PropTypes.func,
checkCanModifyRecord: PropTypes.func,

View File

@@ -598,6 +598,9 @@ class TreeBody extends Component {
colOverScanStartIdx={this.props.colOverScanStartIdx}
colOverScanEndIdx={this.props.colOverScanEndIdx}
lastFrozenColumnKey={this.props.lastFrozenColumnKey}
recordDraggable={this.props.recordDraggable}
recordDragDropEvents={this.props.recordDragDropEvents}
draggingRecordSource={this.props.draggingRecordSource}
scrollLeft={scrollLeft}
height={rowHeight}
cellMetaData={cellMetaData}
@@ -693,6 +696,9 @@ TreeBody.propTypes = {
treeNodeKeyRecordIdMap: PropTypes.object,
keyTreeNodeFoldedMap: PropTypes.object,
treeMetrics: PropTypes.object,
recordDraggable: PropTypes.bool,
recordDragDropEvents: PropTypes.object,
draggingRecordSource: PropTypes.object,
columns: PropTypes.array.isRequired,
CellOperationBtn: PropTypes.object,
colOverScanStartIdx: PropTypes.number,
@@ -727,7 +733,6 @@ TreeBody.propTypes = {
frozenColumnsWidth: PropTypes.number,
editMobileCell: PropTypes.func,
reloadRecords: PropTypes.func,
appPage: PropTypes.object,
showCellColoring: PropTypes.bool,
columnColors: PropTypes.object,
onFillingDragRows: PropTypes.func,

View File

@@ -42,6 +42,14 @@ function isSelectedAll(recordIds, recordMetrics) {
return recordIds.every(recordId => isRecordSelected(recordId, recordMetrics));
}
function getDraggedRecordsIds(draggingRecordId, recordMetrics) {
const selectedRecordIds = getSelectedIds(recordMetrics);
if (selectedRecordIds.includes(draggingRecordId)) {
return selectedRecordIds;
}
return [draggingRecordId];
}
export const RecordMetrics = {
selectRecord,
selectRecordsById,
@@ -51,4 +59,5 @@ export const RecordMetrics = {
getSelectedIds,
hasSelectedRecords,
isSelectedAll,
getDraggedRecordsIds,
};

View File

@@ -49,6 +49,14 @@ const checkIsSelectedAll = (nodeKeys, treeMetrics) => {
return nodeKeys.every(nodeKey => checkIsTreeNodeSelected(nodeKey, treeMetrics));
};
const getDraggedTreeNodesKeys = (draggingTreeNodeKey, treeMetrics) => {
const selectedNodeKeys = getSelectedTreeNodesKeys(treeMetrics);
if (selectedNodeKeys.includes(draggingTreeNodeKey)) {
return selectedNodeKeys;
}
return [draggingTreeNodeKey];
};
export const TreeMetrics = {
checkIsTreeNodeSelected,
selectTreeNode,
@@ -59,4 +67,5 @@ export const TreeMetrics = {
getSelectedIds,
checkHasSelectedTreeNodes,
checkIsSelectedAll,
getDraggedTreeNodesKeys,
};