1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-21 11:27:18 +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,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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} />