mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-21 19:37:28 +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:
@@ -158,6 +158,8 @@ SFTable.propTypes = {
|
||||
onGridKeyUp: PropTypes.func,
|
||||
loadMore: PropTypes.func,
|
||||
loadAll: PropTypes.func,
|
||||
moveRecords: PropTypes.func,
|
||||
renderCustomDraggedRows: PropTypes.func,
|
||||
};
|
||||
|
||||
export default SFTable;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -200,6 +200,10 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
||||
storeRef.current.deleteTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
||||
}, [storeRef]);
|
||||
|
||||
const deleteTagsLinks = useCallback((columnKey, tagId, idLinkedRowsIdsMap, { success_callback, fail_callback } = {}) => {
|
||||
storeRef.current.deleteTagsLinks(columnKey, tagId, idLinkedRowsIdsMap, success_callback, fail_callback);
|
||||
}, [storeRef]);
|
||||
|
||||
const mergeTags = useCallback((target_tag_id, merged_tags_ids, { success_callback, fail_callback } = {}) => {
|
||||
storeRef.current.mergeTags(target_tag_id, merged_tags_ids, success_callback, fail_callback);
|
||||
}, [storeRef]);
|
||||
@@ -284,6 +288,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
||||
updateTag,
|
||||
addTagLinks,
|
||||
deleteTagLinks,
|
||||
deleteTagsLinks,
|
||||
mergeTags,
|
||||
updateLocalTag,
|
||||
selectTag: handleSelectTag,
|
||||
|
@@ -237,6 +237,7 @@ class DataProcessor {
|
||||
}
|
||||
case OPERATION_TYPE.ADD_TAG_LINKS:
|
||||
case OPERATION_TYPE.DELETE_TAG_LINKS:
|
||||
case OPERATION_TYPE.DELETE_TAGS_LINKS:
|
||||
case OPERATION_TYPE.MERGE_TAGS: {
|
||||
this.buildTagsTree(table.rows, table);
|
||||
break;
|
||||
|
@@ -386,6 +386,19 @@ class Store {
|
||||
this.applyOperation(operation);
|
||||
}
|
||||
|
||||
deleteTagsLinks(column_key, id_linked_rows_ids_map, success_callback, fail_callback) {
|
||||
const type = OPERATION_TYPE.DELETE_TAGS_LINKS;
|
||||
const operation = this.createOperation({
|
||||
type,
|
||||
repo_id: this.repoId,
|
||||
column_key,
|
||||
id_linked_rows_ids_map,
|
||||
success_callback,
|
||||
fail_callback,
|
||||
});
|
||||
this.applyOperation(operation);
|
||||
}
|
||||
|
||||
mergeTags(target_tag_id, merged_tags_ids, success_callback, fail_callback) {
|
||||
const type = OPERATION_TYPE.MERGE_TAGS;
|
||||
const operation = this.createOperation({
|
||||
|
@@ -219,6 +219,56 @@ export default function apply(data, operation) {
|
||||
}
|
||||
return data;
|
||||
}
|
||||
case OPERATION_TYPE.DELETE_TAGS_LINKS: {
|
||||
const { column_key, id_linked_rows_ids_map } = operation;
|
||||
const operatedIds = id_linked_rows_ids_map && Object.keys(id_linked_rows_ids_map);
|
||||
if (!operatedIds || operatedIds.length === 0) {
|
||||
return data;
|
||||
}
|
||||
data.rows = [...data.rows];
|
||||
if (column_key === PRIVATE_COLUMN_KEY.PARENT_LINKS) {
|
||||
data.rows.forEach((row, index) => {
|
||||
const currentRowId = row._id;
|
||||
const other_rows_ids = id_linked_rows_ids_map[currentRowId];
|
||||
let updatedRow = { ...row };
|
||||
if (other_rows_ids) {
|
||||
// remove parent tags from current tag
|
||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, other_rows_ids);
|
||||
}
|
||||
|
||||
// remove current tag as child tag from related tags
|
||||
operatedIds.forEach((operatedId) => {
|
||||
const other_rows_ids = id_linked_rows_ids_map[operatedId];
|
||||
if (other_rows_ids && other_rows_ids.includes(currentRowId)) {
|
||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, [operatedId]);
|
||||
}
|
||||
});
|
||||
data.rows[index] = updatedRow;
|
||||
data.id_row_map[currentRowId] = updatedRow;
|
||||
});
|
||||
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
||||
data.rows.forEach((row, index) => {
|
||||
const currentRowId = row._id;
|
||||
const other_rows_ids = id_linked_rows_ids_map[currentRowId];
|
||||
let updatedRow = { ...row };
|
||||
if (other_rows_ids) {
|
||||
// remove child tags from current tag
|
||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
||||
}
|
||||
|
||||
// remove current tag as parent tag from related tags
|
||||
operatedIds.forEach((operatedId) => {
|
||||
const other_rows_ids = id_linked_rows_ids_map[operatedId];
|
||||
if (other_rows_ids && other_rows_ids.includes(currentRowId)) {
|
||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, [operatedId]);
|
||||
}
|
||||
});
|
||||
data.rows[index] = updatedRow;
|
||||
data.id_row_map[currentRowId] = updatedRow;
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
case OPERATION_TYPE.MERGE_TAGS: {
|
||||
const { target_tag_id, merged_tags_ids } = operation;
|
||||
const targetTag = getRowById(data, target_tag_id);
|
||||
|
@@ -7,6 +7,7 @@ export const OPERATION_TYPE = {
|
||||
RELOAD_RECORDS: 'reload_records',
|
||||
ADD_TAG_LINKS: 'add_tag_links',
|
||||
DELETE_TAG_LINKS: 'delete_tag_links',
|
||||
DELETE_TAGS_LINKS: 'delete_tags_links',
|
||||
MERGE_TAGS: 'merge_tags',
|
||||
|
||||
MODIFY_LOCAL_RECORDS: 'modify_local_records',
|
||||
@@ -24,6 +25,7 @@ export const OPERATION_ATTRIBUTES = {
|
||||
[OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', 'row_ids'],
|
||||
[OPERATION_TYPE.ADD_TAG_LINKS]: ['repo_id', 'column_key', 'row_id', 'other_rows_ids'],
|
||||
[OPERATION_TYPE.DELETE_TAG_LINKS]: ['repo_id', 'column_key', 'row_id', 'other_rows_ids'],
|
||||
[OPERATION_TYPE.DELETE_TAGS_LINKS]: ['repo_id', 'column_key', 'id_linked_rows_ids_map'],
|
||||
[OPERATION_TYPE.MERGE_TAGS]: ['repo_id', 'target_tag_id', 'merged_tags_ids'],
|
||||
[OPERATION_TYPE.MODIFY_LOCAL_RECORDS]: ['repo_id', 'row_ids', 'id_row_updates', 'id_original_row_updates', 'id_old_row_data', 'id_original_old_row_data', 'is_copy_paste', 'is_rename', 'id_obj_id'],
|
||||
[OPERATION_TYPE.MODIFY_LOCAL_FILE_TAGS]: ['file_id', 'tags_ids'],
|
||||
|
@@ -97,6 +97,15 @@ class ServerOperator {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.DELETE_TAGS_LINKS: {
|
||||
const { column_key, id_linked_rows_ids_map } = operation;
|
||||
this.context.deleteTagLinks(column_key, id_linked_rows_ids_map).then(res => {
|
||||
callback({ operation });
|
||||
}).catch(error => {
|
||||
callback({ error: gettext('Failed to delete linked tags') });
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.MERGE_TAGS: {
|
||||
const { target_tag_id, merged_tags_ids } = operation;
|
||||
this.context.mergeTags(target_tag_id, merged_tags_ids).then((res) => {
|
||||
|
@@ -0,0 +1,27 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { getTreeNodeByKey, getTreeNodeId } from '../../../../components/sf-table/utils/tree';
|
||||
import { useTags } from '../../../hooks';
|
||||
import { getRowById } from '../../../../components/sf-table/utils/table';
|
||||
import TagNameFormatter from './formatter/tag-name';
|
||||
|
||||
const DraggedTagsLayer = ({ draggedNodesKeys }) => {
|
||||
const { tagsData } = useTags();
|
||||
|
||||
const keyTreeNodeMap = useMemo(() => {
|
||||
return tagsData.key_tree_node_map || [];
|
||||
}, [tagsData]);
|
||||
|
||||
return draggedNodesKeys.map((nodeKey) => {
|
||||
const node = getTreeNodeByKey(nodeKey, keyTreeNodeMap);
|
||||
const tagId = getTreeNodeId(node);
|
||||
const tag = getRowById(tagsData, tagId);
|
||||
if (!tag) return null;
|
||||
return (
|
||||
<tr key={`rdg-dragged-record-${nodeKey}`} className="rdg-dragged-record">
|
||||
<td className="rdg-dragged-record-cell"><TagNameFormatter record={tag} /></td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default DraggedTagsLayer;
|
@@ -4,6 +4,7 @@ import { useTags } from '../../../../hooks';
|
||||
import { PRIVATE_COLUMN_KEY } from '../../../../constants';
|
||||
import { getRecordIdFromRecord } from '../../../../../metadata/utils/cell';
|
||||
import { getTreeNodeKey } from '../../../../../components/sf-table/utils/tree';
|
||||
import { isNumber } from '../../../../../utils/number';
|
||||
|
||||
const TagNameFormatter = ({ record, isCellSelected, setDisplayTag, treeNodeIndex }) => {
|
||||
const { tagsData } = useTags();
|
||||
@@ -13,7 +14,7 @@ const TagNameFormatter = ({ record, isCellSelected, setDisplayTag, treeNodeIndex
|
||||
}, [tagsData]);
|
||||
|
||||
const currentNode = useMemo(() => {
|
||||
return tree[treeNodeIndex];
|
||||
return isNumber(treeNodeIndex) ? tree[treeNodeIndex] : null;
|
||||
}, [tree, treeNodeIndex]);
|
||||
|
||||
const tagColor = useMemo(() => {
|
||||
@@ -28,7 +29,7 @@ const TagNameFormatter = ({ record, isCellSelected, setDisplayTag, treeNodeIndex
|
||||
if (!isCellSelected) return;
|
||||
const tagId = getRecordIdFromRecord(record);
|
||||
const nodeKey = getTreeNodeKey(currentNode);
|
||||
setDisplayTag(tagId, nodeKey);
|
||||
setDisplayTag && setDisplayTag(tagId, nodeKey);
|
||||
}, [isCellSelected, record, currentNode, setDisplayTag]);
|
||||
|
||||
return (
|
||||
|
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import SFTable from '../../../../components/sf-table';
|
||||
import EditTagDialog from '../../../components/dialog/edit-tag-dialog';
|
||||
import MergeTagsSelector from '../../../components/merge-tags-selector';
|
||||
import DraggedTagsLayer from './dragged-tags-layer';
|
||||
import { createTableColumns } from './columns-factory';
|
||||
import { createContextMenuOptions } from './context-menu-options';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
@@ -13,6 +14,9 @@ import { EVENT_BUS_TYPE } from '../../../../metadata/constants';
|
||||
import { EVENT_BUS_TYPE as TABLE_EVENT_BUS_TYPE } from '../../../../components/sf-table/constants/event-bus-type';
|
||||
import { LOCAL_KEY_TREE_NODE_FOLDED } from '../../../../components/sf-table/constants/tree';
|
||||
import { isNumber } from '../../../../utils/number';
|
||||
import { getTreeNodeByKey, getTreeNodeId } from '../../../../components/sf-table/utils/tree';
|
||||
import { getRowById } from '../../../../components/sf-table/utils/table';
|
||||
import { getParentLinks } from '../../../utils/cell';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -34,7 +38,7 @@ const TagsTable = ({
|
||||
loadMore,
|
||||
getTagsTableWrapperOffsets,
|
||||
}) => {
|
||||
const { tagsData, updateTag, deleteTags, addTagLinks, deleteTagLinks, addChildTag, mergeTags } = useTags();
|
||||
const { tagsData, updateTag, deleteTags, addTagLinks, deleteTagLinks, deleteTagsLinks, addChildTag, mergeTags } = useTags();
|
||||
|
||||
const [isShowNewSubTagDialog, setIsShowNewSubTagDialog] = useState(false);
|
||||
const [isShowMergeTagsSelector, setIsShowMergeTagsSelector] = useState(false);
|
||||
@@ -208,6 +212,56 @@ const TagsTable = ({
|
||||
}
|
||||
}, [scrollToCurrentSelectedCell]);
|
||||
|
||||
const renderCustomDraggedRows = useCallback((draggedNodesKeys) => {
|
||||
if (!Array.isArray(draggedNodesKeys) || draggedNodesKeys.length === 0) return null;
|
||||
return (
|
||||
<DraggedTagsLayer draggedNodesKeys={draggedNodesKeys} />
|
||||
);
|
||||
}, []);
|
||||
|
||||
const moveTags = useCallback(({ draggingSource, dropTarget }) => {
|
||||
const targetNode = getTreeNodeByKey(dropTarget, table.key_tree_node_map);
|
||||
if (!Array.isArray(draggingSource) || draggingSource.length === 0 || !targetNode) return;
|
||||
let draggingTagsIds = [];
|
||||
let idNeedDeleteChildIds = {}; // { [parent_tag._id]: [child_tag._id] }
|
||||
draggingSource.forEach((nodeKey) => {
|
||||
const node = getTreeNodeByKey(nodeKey, table.key_tree_node_map);
|
||||
const nodeId = getTreeNodeId(node);
|
||||
const tag = getRowById(table, nodeId);
|
||||
|
||||
// find the child tags to delete which related to dragging tags
|
||||
const parentLinks = getParentLinks(tag);
|
||||
if (Array.isArray(parentLinks) && parentLinks.length > 0) {
|
||||
parentLinks.forEach((link) => {
|
||||
const parentTagId = link.row_id;
|
||||
if (nodeKey.includes(parentTagId)) {
|
||||
if (!idNeedDeleteChildIds[parentTagId]) {
|
||||
idNeedDeleteChildIds[parentTagId] = [nodeId];
|
||||
} else if (!idNeedDeleteChildIds[parentTagId].includes(nodeId)) {
|
||||
idNeedDeleteChildIds[parentTagId].push(nodeId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// get none-repeat dragging tags ids
|
||||
if (!draggingTagsIds.includes(nodeId)) {
|
||||
draggingTagsIds.push(nodeId);
|
||||
}
|
||||
});
|
||||
if (draggingTagsIds.length === 0) return;
|
||||
|
||||
const targetTagId = getTreeNodeId(targetNode);
|
||||
if (Object.keys(idNeedDeleteChildIds).length > 0) {
|
||||
// need to delete child tags first
|
||||
deleteTagsLinks(PRIVATE_COLUMN_KEY.SUB_LINKS, idNeedDeleteChildIds, () => {
|
||||
addTagLinks(PRIVATE_COLUMN_KEY.SUB_LINKS, targetTagId, draggingTagsIds);
|
||||
});
|
||||
} else {
|
||||
addTagLinks(PRIVATE_COLUMN_KEY.SUB_LINKS, targetTagId, draggingTagsIds);
|
||||
}
|
||||
}, [table, addTagLinks, deleteTagsLinks]);
|
||||
|
||||
useEffect(() => {
|
||||
const eventBus = EventBus.getInstance();
|
||||
const unsubscribeUpdateSearchResult = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SEARCH_RESULT, updateSearchResult);
|
||||
@@ -242,6 +296,8 @@ const TagsTable = ({
|
||||
checkCellValueChanged={checkCellValueChanged}
|
||||
modifyColumnWidth={modifyColumnWidth}
|
||||
loadMore={loadMore}
|
||||
renderCustomDraggedRows={renderCustomDraggedRows}
|
||||
moveRecords={moveTags}
|
||||
/>
|
||||
{isShowNewSubTagDialog && (
|
||||
<EditTagDialog tags={table.rows} title={gettext('New child tag')} onToggle={closeNewSubTagDialog} onSubmit={handelAddChildTag} />
|
||||
|
Reference in New Issue
Block a user