diff --git a/frontend/src/components/sf-table/index.js b/frontend/src/components/sf-table/index.js index 3527188615..2f7e0ecadf 100644 --- a/frontend/src/components/sf-table/index.js +++ b/frontend/src/components/sf-table/index.js @@ -158,6 +158,8 @@ SFTable.propTypes = { onGridKeyUp: PropTypes.func, loadMore: PropTypes.func, loadAll: PropTypes.func, + moveRecords: PropTypes.func, + renderCustomDraggedRows: PropTypes.func, }; export default SFTable; diff --git a/frontend/src/components/sf-table/table-main/records/index.js b/frontend/src/components/sf-table/table-main/records/index.js index 1d7808375b..4f88636baa 100644 --- a/frontend/src/components/sf-table/table-main/records/index.js +++ b/frontend/src/components/sf-table/table-main/records/index.js @@ -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 ( + + ); + }; + + 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: (
- {this.renderRecordsBody({ containerWidth })} + {this.renderRecordsBody({ containerWidth, recordDraggable })}
+ {this.createRecordsDragLayer()} {this.isWindows && this.isWebkit && ( { + 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 ( +
+ + {renderDraggedRows()} +
+
+ ); +}; + +export default RecordDragLayer; diff --git a/frontend/src/components/sf-table/table-main/records/record/actions-cell/index.js b/frontend/src/components/sf-table/table-main/records/record/actions-cell/index.js index cad64d3daf..3cd506bf8a 100644 --- a/frontend/src/components/sf-table/table-main/records/record/actions-cell/index.js +++ b/frontend/src/components/sf-table/table-main/records/record/actions-cell/index.js @@ -70,6 +70,14 @@ class ActionsCell extends Component { onMouseEnter={this.onCellMouseEnter} onMouseLeave={this.onCellMouseLeave} > + {this.props.recordDraggable && +
+
+ } {!isSelected &&
{this.getRecordNo()}
}
@@ -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; diff --git a/frontend/src/components/sf-table/table-main/records/record/index.css b/frontend/src/components/sf-table/table-main/records/record/index.css index 99b4f9c10e..e12850bdd0 100644 --- a/frontend/src/components/sf-table/table-main/records/record/index.css +++ b/frontend/src/components/sf-table/table-main/records/record/index.css @@ -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; +} diff --git a/frontend/src/components/sf-table/table-main/records/record/index.js b/frontend/src/components/sf-table/table-main/records/record/index.js index 626aa9c3bf..c1f48fc46b 100644 --- a/frontend/src/components/sf-table/table-main/records/record/index.js +++ b/frontend/src/components/sf-table/table-main/records/record/index.js @@ -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 (
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 */}
} {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, diff --git a/frontend/src/components/sf-table/table-main/records/tree-body.js b/frontend/src/components/sf-table/table-main/records/tree-body.js index f6d809e238..96323fed50 100644 --- a/frontend/src/components/sf-table/table-main/records/tree-body.js +++ b/frontend/src/components/sf-table/table-main/records/tree-body.js @@ -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, diff --git a/frontend/src/components/sf-table/utils/record-metrics.js b/frontend/src/components/sf-table/utils/record-metrics.js index fa4e4ad5d0..656e39b79a 100644 --- a/frontend/src/components/sf-table/utils/record-metrics.js +++ b/frontend/src/components/sf-table/utils/record-metrics.js @@ -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, }; diff --git a/frontend/src/components/sf-table/utils/tree-metrics.js b/frontend/src/components/sf-table/utils/tree-metrics.js index 97c30ed69f..41b7078219 100644 --- a/frontend/src/components/sf-table/utils/tree-metrics.js +++ b/frontend/src/components/sf-table/utils/tree-metrics.js @@ -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, }; diff --git a/frontend/src/tag/hooks/tags.js b/frontend/src/tag/hooks/tags.js index 291c112083..f2c2a9de13 100644 --- a/frontend/src/tag/hooks/tags.js +++ b/frontend/src/tag/hooks/tags.js @@ -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, diff --git a/frontend/src/tag/store/data-processor.js b/frontend/src/tag/store/data-processor.js index fa7c10bcc7..3dbb3ab7f6 100644 --- a/frontend/src/tag/store/data-processor.js +++ b/frontend/src/tag/store/data-processor.js @@ -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; diff --git a/frontend/src/tag/store/index.js b/frontend/src/tag/store/index.js index d80be19577..a62438d492 100644 --- a/frontend/src/tag/store/index.js +++ b/frontend/src/tag/store/index.js @@ -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({ diff --git a/frontend/src/tag/store/operations/apply.js b/frontend/src/tag/store/operations/apply.js index 9c02b139bf..93e47ffe2b 100644 --- a/frontend/src/tag/store/operations/apply.js +++ b/frontend/src/tag/store/operations/apply.js @@ -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); diff --git a/frontend/src/tag/store/operations/constants.js b/frontend/src/tag/store/operations/constants.js index 9419618597..2b0df564db 100644 --- a/frontend/src/tag/store/operations/constants.js +++ b/frontend/src/tag/store/operations/constants.js @@ -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'], diff --git a/frontend/src/tag/store/server-operator.js b/frontend/src/tag/store/server-operator.js index b6b075bf10..3944a50f18 100644 --- a/frontend/src/tag/store/server-operator.js +++ b/frontend/src/tag/store/server-operator.js @@ -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) => { diff --git a/frontend/src/tag/views/all-tags/tags-table/dragged-tags-layer.js b/frontend/src/tag/views/all-tags/tags-table/dragged-tags-layer.js new file mode 100644 index 0000000000..6f73464e5f --- /dev/null +++ b/frontend/src/tag/views/all-tags/tags-table/dragged-tags-layer.js @@ -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 ( + + + + ); + }); +}; + +export default DraggedTagsLayer; diff --git a/frontend/src/tag/views/all-tags/tags-table/formatter/tag-name.js b/frontend/src/tag/views/all-tags/tags-table/formatter/tag-name.js index f4f096ec69..d2ce9bfef0 100644 --- a/frontend/src/tag/views/all-tags/tags-table/formatter/tag-name.js +++ b/frontend/src/tag/views/all-tags/tags-table/formatter/tag-name.js @@ -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 ( diff --git a/frontend/src/tag/views/all-tags/tags-table/index.js b/frontend/src/tag/views/all-tags/tags-table/index.js index 455ac83622..2037204c74 100644 --- a/frontend/src/tag/views/all-tags/tags-table/index.js +++ b/frontend/src/tag/views/all-tags/tags-table/index.js @@ -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 ( + + ); + }, []); + + 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 && (