diff --git a/frontend/src/components/sf-table/constants/tree.js b/frontend/src/components/sf-table/constants/tree.js new file mode 100644 index 0000000000..118ae6a25f --- /dev/null +++ b/frontend/src/components/sf-table/constants/tree.js @@ -0,0 +1,12 @@ +export const TREE_NODE_KEY = { + ID: '_id', + KEY: 'node_key', + DEPTH: 'node_depth', + HAS_SUB_NODES: 'has_sub_nodes', +}; + +export const LOCAL_KEY_TREE_NODE_FOLDED = 'table_key_tree_node_folded_map'; + +export const NODE_ICON_LEFT_INDENT = 18; + +export const NODE_CONTENT_LEFT_INDENT = 22; diff --git a/frontend/src/components/sf-table/editors/editor-container/popup-editor-container.js b/frontend/src/components/sf-table/editors/editor-container/popup-editor-container.js index 37a589840f..356feb1cab 100644 --- a/frontend/src/components/sf-table/editors/editor-container/popup-editor-container.js +++ b/frontend/src/components/sf-table/editors/editor-container/popup-editor-container.js @@ -71,6 +71,7 @@ class PopupEditorContainer extends React.Component { editorContainer: document.body, modifyColumnData, editorPosition, + editingRowId: this.editingRowId, record, height, columns, diff --git a/frontend/src/components/sf-table/editors/tags-editor/delete-tags/index.js b/frontend/src/components/sf-table/editors/tags-editor/delete-tags/index.js index bf868bbfbd..5c760f3872 100644 --- a/frontend/src/components/sf-table/editors/tags-editor/delete-tags/index.js +++ b/frontend/src/components/sf-table/editors/tags-editor/delete-tags/index.js @@ -1,7 +1,7 @@ import React from './index'; import PropTypes from 'prop-types'; import { IconBtn } from '@seafile/sf-metadata-ui-component'; -import { getTagColor, getTagName } from '../../../../../tag/utils/cell/core'; +import { getTagColor, getTagName } from '../../../../../tag/utils/cell'; import { getRowById } from '../../../utils/table'; import './index.css'; diff --git a/frontend/src/components/sf-table/editors/tags-editor/index.js b/frontend/src/components/sf-table/editors/tags-editor/index.js index 16f7fa8794..1862493f59 100644 --- a/frontend/src/components/sf-table/editors/tags-editor/index.js +++ b/frontend/src/components/sf-table/editors/tags-editor/index.js @@ -6,7 +6,7 @@ import DeleteTags from './delete-tags'; import { Utils } from '../../../../utils/utils'; import { KeyCodes } from '../../../../constants'; import { gettext } from '../../../../utils/constants'; -import { getTagColor, getTagId, getTagName, getTagsByNameOrColor } from '../../../../tag/utils/cell/core'; +import { getTagColor, getTagId, getTagName, getTagsByNameOrColor } from '../../../../tag/utils/cell'; import { getRecordIdFromRecord } from '../../../../metadata/utils/cell'; import { SELECT_OPTION_COLORS } from '../../../../metadata/constants'; import { getRowById } from '../../utils/table'; diff --git a/frontend/src/components/sf-table/index.css b/frontend/src/components/sf-table/index.css index bffd652ae7..2e442725e7 100644 --- a/frontend/src/components/sf-table/index.css +++ b/frontend/src/components/sf-table/index.css @@ -85,6 +85,7 @@ } .sf-table-column-content { + position: relative; height: 100%; width: 100%; text-align: left; diff --git a/frontend/src/components/sf-table/index.js b/frontend/src/components/sf-table/index.js index 8be9fb41d3..71f754568d 100644 --- a/frontend/src/components/sf-table/index.js +++ b/frontend/src/components/sf-table/index.js @@ -4,6 +4,7 @@ import classnames from 'classnames'; import TableMain from './table-main'; import './index.css'; +import './tree.css'; const SFTable = ({ table, @@ -14,6 +15,10 @@ const SFTable = ({ groups, showSequenceColumn, isGroupView, + showRecordAsTree, + recordsTree, + treeNodeKeyRecordIdMap, + keyTreeNodeFoldedMap, noRecordsTipsText, isLoadingMoreRecords, hasMoreRecords, @@ -46,10 +51,22 @@ const SFTable = ({ return recordId && recordGetterById(recordId); }, [recordGetterById]); + const getTreeNodeByIndex = useCallback((nodeIndex) => { + if (!window.sfTableBody || !window.sfTableBody.getTreeNodeByIndex) return null; + return window.sfTableBody.getTreeNodeByIndex(nodeIndex); + }, []); + + const treeRecordGetter = useCallback((nodeIndex) => { + const node = getTreeNodeByIndex(nodeIndex); + const recordId = node && node._id; + return recordId && recordGetterById(recordId); + }, [getTreeNodeByIndex, recordGetterById]); + const recordGetterByIndex = useCallback(({ isGroupView, groupRecordIndex, recordIndex }) => { + if (showRecordAsTree) return treeRecordGetter(recordIndex); if (isGroupView) return groupRecordGetter(groupRecordIndex); return recordGetter(recordIndex); - }, [groupRecordGetter, recordGetter]); + }, [showRecordAsTree, groupRecordGetter, treeRecordGetter, recordGetter]); const beforeUnloadHandler = useCallback(() => { if (window.sfTableBody) { @@ -71,17 +88,21 @@ const SFTable = ({ recordsIds={recordsIds} groupbys={groupbys} groups={groups} + recordsTree={recordsTree} + keyTreeNodeFoldedMap={keyTreeNodeFoldedMap} showSequenceColumn={showSequenceColumn} isGroupView={isGroupView} noRecordsTipsText={noRecordsTipsText} hasMoreRecords={hasMoreRecords} isLoadingMoreRecords={isLoadingMoreRecords} showGridFooter={showGridFooter} + showRecordAsTree={showRecordAsTree} loadMore={loadMore} loadAll={loadAll} getTableContentRect={getTableContentRect} onGridKeyDown={onGridKeyDown} onGridKeyUp={onGridKeyUp} + getTreeNodeByIndex={getTreeNodeByIndex} recordGetterById={recordGetterById} recordGetterByIndex={recordGetterByIndex} /> @@ -121,6 +142,16 @@ SFTable.propTypes = { supportCut: PropTypes.bool, supportPaste: PropTypes.bool, supportDragFill: PropTypes.bool, + showRecordAsTree: PropTypes.bool, + /** + * recordsTree: [ + * { _id, node_depth, node_index, node_key, ... } + * ... + * ] + * keyTreeNodeFoldedMap: { [node_key]: true, ... } + */ + recordsTree: PropTypes.array, + keyTreeNodeFoldedMap: PropTypes.object, checkCanModifyRecord: PropTypes.func, checkCellValueChanged: PropTypes.func, // for complex cell value compare onGridKeyDown: PropTypes.func, @@ -134,6 +165,7 @@ SFTable.defaultProps = { groupbys: [], groups: [], isGroupView: false, + showRecordAsTree: false, showSequenceColumn: true, hasMoreRecords: false, isLoadingMoreRecords: false, diff --git a/frontend/src/components/sf-table/masks/interaction-masks/index.js b/frontend/src/components/sf-table/masks/interaction-masks/index.js index 9a83d0302d..a21e167cbf 100644 --- a/frontend/src/components/sf-table/masks/interaction-masks/index.js +++ b/frontend/src/components/sf-table/masks/interaction-masks/index.js @@ -20,7 +20,8 @@ import { getSelectedRangeDimensions, getSelectedRow, getSelectedColumn, getRecordsFromSelectedRange, getSelectedCellValue, checkIsSelectedCellEditable, } from '../../utils/selected-cell-utils'; -import RecordMetrics from '../../utils/record-metrics'; +import { RecordMetrics } from '../../utils/record-metrics'; +import { TreeMetrics } from '../../utils/tree-metrics'; import setEventTransfer from '../../utils/set-event-transfer'; import getEventTransfer from '../../utils/get-event-transfer'; import { getGroupRecordByIndex } from '../../utils/group-metrics'; @@ -486,10 +487,12 @@ class InteractionMasks extends React.Component { onCopy = (e) => { e.preventDefault(); - const { recordMetrics } = this.props; + const { showRecordAsTree, recordMetrics, treeMetrics, treeNodeKeyRecordIdMap } = this.props; // select the records to copy - const selectedRecordIds = RecordMetrics.getSelectedIds(recordMetrics); + // TODO: need copy each nodes from tree? + const selectedRecordIds = showRecordAsTree ? TreeMetrics.getSelectedIds(treeMetrics, treeNodeKeyRecordIdMap) : RecordMetrics.getSelectedIds(recordMetrics); + if (selectedRecordIds.length > 0) { this.copyRows(e, selectedRecordIds); return; @@ -1074,6 +1077,9 @@ class InteractionMasks extends React.Component { {isValidElement(contextMenu) && cloneElement(contextMenu, { selectedPosition: isSelectedSingleCell ? selectedPosition : null, selectedRange: !isSelectedSingleCell ? selectedRange : null, + showRecordAsTree: this.props.showRecordAsTree, + treeNodeKeyRecordIdMap: this.props.treeNodeKeyRecordIdMap, + treeMetrics: this.props.treeMetrics, onClearSelected: this.handleSelectCellsDelete, onCopySelected: this.onCopySelected, getTableContentRect: this.props.getTableContentRect, @@ -1097,6 +1103,9 @@ InteractionMasks.propTypes = { rowHeight: PropTypes.number, groupOffsetLeft: PropTypes.number, frozenColumnsWidth: PropTypes.number, + showRecordAsTree: PropTypes.bool, + treeNodeKeyRecordIdMap: PropTypes.object, + treeMetrics: PropTypes.object, enableCellSelect: PropTypes.bool, canModifyRecords: PropTypes.bool, getRowTop: PropTypes.func, diff --git a/frontend/src/components/sf-table/table-main/index.js b/frontend/src/components/sf-table/table-main/index.js index f6cecb81da..327594dc8b 100644 --- a/frontend/src/components/sf-table/table-main/index.js +++ b/frontend/src/components/sf-table/table-main/index.js @@ -8,6 +8,7 @@ import { GROUP_VIEW_OFFSET } from '../constants/group'; import { SEQUENCE_COLUMN_WIDTH } from '../constants/grid'; import { getCellValueByColumn } from '../utils/cell'; import GridUtils from '../utils/grid-utils'; +import { generateKeyTreeNodeRowIdMap } from '../utils/tree'; const TableMain = ({ table, @@ -21,8 +22,11 @@ const TableMain = ({ hasMoreRecords, isLoadingMoreRecords, showGridFooter, + recordsTree, + showRecordAsTree, loadMore, loadAll, + getTreeNodeByIndex, recordGetterByIndex, recordGetterById, getClientCellValueDisplayString, @@ -44,6 +48,15 @@ const TableMain = ({ return recordsIds.length; }, [recordsIds]); + const treeNodesCount = useMemo(() => { + return recordsTree.length; + }, [recordsTree]); + + const treeNodeKeyRecordIdMap = useMemo(() => { + // treeNodeKeyRecordIdMap: { [node_key]: _id, ... } + return generateKeyTreeNodeRowIdMap(recordsTree); + }, [recordsTree]); + const hasNoRecords = useMemo(() => { return recordsCount === 0 && !hasMoreRecords; }, [recordsCount, hasMoreRecords]); @@ -76,7 +89,7 @@ const TableMain = ({ }, [getClientCellValueDisplayString]); return ( -
0 })}> +
0, 'sf-table-tree': showRecordAsTree })}> {hasNoRecords && } {!hasNoRecords && { - const { hasMoreRecords, hasSelectedRecord, recordMetrics, selectedRange, recordsCount } = this.props; + const { hasMoreRecords, hasSelectedRecord, recordMetrics, selectedRange, recordsCount, showRecordAsTree, treeMetrics } = this.props; if (hasSelectedRecord) { - const selectedRecordsCount = RecordMetrics.getSelectedIds(recordMetrics).length; + let selectedRecordsCount = 1; + if (showRecordAsTree) { + selectedRecordsCount = TreeMetrics.getSelectedTreeNodesKeys(treeMetrics).length; + } else { + selectedRecordsCount = RecordMetrics.getSelectedIds(recordMetrics).length; + } return selectedRecordsCount > 1 ? gettext('xxx records selected').replace('xxx', selectedRecordsCount) : gettext('1 record selected'); } const selectedCellsCount = this.getSelectedCellsCount(selectedRange); @@ -173,6 +179,8 @@ RecordsFooter.propTypes = { sequenceColumnWidth: PropTypes.number, groupOffsetLeft: PropTypes.number, recordMetrics: PropTypes.object, + showRecordAsTree: PropTypes.bool, + treeMetrics: PropTypes.object, selectedRange: PropTypes.object, recordGetterById: PropTypes.func, recordGetterByIndex: PropTypes.func, diff --git a/frontend/src/components/sf-table/table-main/records-header/cell/index.js b/frontend/src/components/sf-table/table-main/records-header/cell/index.js index 000962a1a0..f16fd282e3 100644 --- a/frontend/src/components/sf-table/table-main/records-header/cell/index.js +++ b/frontend/src/components/sf-table/table-main/records-header/cell/index.js @@ -8,14 +8,16 @@ import HeaderDropdownMenu from '../dropdown-menu'; import EventBus from '../../../../common/event-bus'; import { EVENT_BUS_TYPE } from '../../../constants/event-bus-type'; import { checkIsNameColumn } from '../../../utils/column'; +import { MIN_COLUMN_WIDTH } from '../../../constants/grid'; +import { NODE_CONTENT_LEFT_INDENT, NODE_ICON_LEFT_INDENT } from '../../../constants/tree'; import './index.css'; -import { MIN_COLUMN_WIDTH } from '../../../constants/grid'; const Cell = ({ frozen, moveable, resizable, + showRecordAsTree, groupOffsetLeft, isLastFrozenCell, height, @@ -150,16 +152,10 @@ const Cell = ({ const { key, display_name, icon_name, icon_tooltip } = column; const isNameColumn = checkIsNameColumn(column); - const cell = ( -
handleHeaderCellClick(column, frozen)} - onContextMenu={onContextMenu} - > -
+ + const cellName = useMemo(() => { + return ( + <> {icon_name && } @@ -171,11 +167,42 @@ const Cell = ({
{display_name}
+ + ); + }, [icon_name, display_name, height, icon_tooltip, key]); + + const cellContent = useMemo(() => { + if (showRecordAsTree && isNameColumn) { + return ( +
+ +
+ {cellName} +
+
+ ); + } + return cellName; + }, [cellName, isNameColumn, showRecordAsTree]); + + const cell = useMemo(() => { + return ( +
handleHeaderCellClick(column, frozen)} + onContextMenu={onContextMenu} + > +
+ {cellContent} +
+ {isValidElement(ColumnDropdownMenu) && } + {resizable && }
- {isValidElement(ColumnDropdownMenu) && } - {resizable && } -
- ); + ); + }, [ColumnDropdownMenu, cellContent, key, column, style, frozen, resizable, isLastFrozenCell, isNameColumn, handleDragEndColumnWidth, handleHeaderCellClick, onContextMenu, onDraggingColumnWidth]); if (!moveable || isNameColumn) { return ( diff --git a/frontend/src/components/sf-table/table-main/records/body.js b/frontend/src/components/sf-table/table-main/records/body.js index 1d5d386231..12b877bfd7 100644 --- a/frontend/src/components/sf-table/table-main/records/body.js +++ b/frontend/src/components/sf-table/table-main/records/body.js @@ -4,7 +4,7 @@ import { Loading } from '@seafile/sf-metadata-ui-component'; import { RightScrollbar } from '../../scrollbar'; import Record from './record'; import InteractionMasks from '../../masks/interaction-masks'; -import RecordMetrics from '../../utils/record-metrics'; +import { RecordMetrics } from '../../utils/record-metrics'; import { getColumnScrollPosition, getColVisibleStartIdx, getColVisibleEndIdx } from '../../utils/records-body-utils'; import EventBus from '../../../common/event-bus'; import { EVENT_BUS_TYPE } from '../../constants/event-bus-type'; @@ -41,7 +41,7 @@ class RecordsBody extends Component { this.rowVisibleStart = 0; this.rowVisibleEnd = this.setRecordVisibleEnd(); this.columnVisibleStart = 0; - this.columnVisibleEnd = this.setColumnVisibleEnd(); + this.columnVisibleEnd = this.props.getColumnVisibleEnd(); this.timer = null; } @@ -103,22 +103,6 @@ class RecordsBody extends Component { return max(ceil(CONTENT_HEIGHT / ROW_HEIGHT), 0); }; - setColumnVisibleEnd = () => { - const { columns, getScrollLeft, getTableContentRect } = this.props; - const { width: tableContentWidth } = getTableContentRect(); - let columnVisibleEnd = 0; - const contentScrollLeft = getScrollLeft(); - let endColumnWidth = tableContentWidth + contentScrollLeft; - for (let i = 0; i < columns.length; i++) { - const { width } = columns[i]; - endColumnWidth = endColumnWidth - width; - if (endColumnWidth < 0) { - return columnVisibleEnd = i; - } - } - return columnVisibleEnd; - }; - recalculateRenderIndex = (recordIds) => { const { startRenderIndex, endRenderIndex } = this.state; const contentScrollTop = this.resultContentRef.scrollTop; @@ -628,6 +612,7 @@ RecordsBody.propTypes = { hasSelectedRecord: PropTypes.bool, recordMetrics: PropTypes.object, totalWidth: PropTypes.number, + getColumnVisibleEnd: PropTypes.func, getScrollLeft: PropTypes.func, setRecordsScrollLeft: PropTypes.func, storeScrollPosition: PropTypes.func, diff --git a/frontend/src/components/sf-table/table-main/records/group-body/index.js b/frontend/src/components/sf-table/table-main/records/group-body/index.js index 22ae0d52e5..27a2c54369 100644 --- a/frontend/src/components/sf-table/table-main/records/group-body/index.js +++ b/frontend/src/components/sf-table/table-main/records/group-body/index.js @@ -6,7 +6,7 @@ import InteractionMasks from '../../../masks/interaction-masks'; import GroupContainer from './group-container'; import Record from '../record'; import { isShiftKeyDown } from '../../../../../utils/keyboard-utils'; -import RecordMetrics from '../../../utils/record-metrics'; +import { RecordMetrics } from '../../../utils/record-metrics'; import { getColumnScrollPosition, getColVisibleEndIdx, getColVisibleStartIdx } from '../../../utils/records-body-utils'; import { addClassName, removeClassName } from '../../../utils'; import { createGroupMetrics, getGroupRecordByIndex, isNestedGroupRow } from '../../../utils/group-metrics'; @@ -53,7 +53,7 @@ class GroupBody extends Component { this.rowVisibleStart = startRenderIndex; this.rowVisibleEnd = endRenderIndex; this.columnVisibleStart = 0; - this.columnVisibleEnd = this.setColumnVisibleEnd(); + this.columnVisibleEnd = this.props.getColumnVisibleEnd(); this.disabledAnimation = false; this.nextPathFoldedGroupMap = null; } @@ -191,22 +191,6 @@ class GroupBody extends Component { this.rightScrollbar = ref; }; - setColumnVisibleEnd = () => { - const { columns, getScrollLeft, getTableContentRect } = this.props; - const { width: tableContentWidth } = getTableContentRect(); - let columnVisibleEnd = 0; - const contentScrollLeft = getScrollLeft(); - let endColumnWidth = tableContentWidth + contentScrollLeft; - for (let i = 0; i < columns.length; i++) { - const { width } = columns[i]; - endColumnWidth = endColumnWidth - width; - if (endColumnWidth < 0) { - return columnVisibleEnd = i; - } - } - return columnVisibleEnd; - }; - getScrollTop = () => { return this.resultContentRef ? this.resultContentRef.scrollTop : 0; }; @@ -974,6 +958,7 @@ GroupBody.propTypes = { searchResult: PropTypes.object, editorPortalTarget: PropTypes.instanceOf(Element), onRef: PropTypes.func, + getColumnVisibleEnd: PropTypes.func, getScrollLeft: PropTypes.func, setRecordsScrollLeft: PropTypes.func, storeScrollPosition: PropTypes.func, 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 7f9412835c..1d7808375b 100644 --- a/frontend/src/components/sf-table/table-main/records/index.js +++ b/frontend/src/components/sf-table/table-main/records/index.js @@ -3,10 +3,12 @@ import PropTypes from 'prop-types'; import { HorizontalScrollbar } from '../../scrollbar'; import RecordsHeader from '../records-header'; import Body from './body'; +import TreeBody from './tree-body'; import GroupBody from './group-body'; import RecordsFooter from '../records-footer'; import ContextMenu from '../../context-menu'; -import RecordMetrics from '../../utils/record-metrics'; +import { RecordMetrics } from '../../utils/record-metrics'; +import { TreeMetrics } from '../../utils/tree-metrics'; import { recalculate } from '../../utils/column'; import { getVisibleBoundaries } from '../../utils/viewport'; import { getColOverScanEndIdx, getColOverScanStartIdx } from '../../utils/grid'; @@ -18,6 +20,7 @@ import { EVENT_BUS_TYPE } from '../../constants/event-bus-type'; import { CANVAS_RIGHT_INTERVAL } from '../../constants/grid'; import { GROUP_ROW_TYPE } from '../../constants/group'; import { isNumber } from '../../../../utils/number'; +import { getTreeNodeKey } from '../../utils/tree'; class Records extends Component { @@ -42,6 +45,7 @@ class Records extends Component { this.state = { columnMetrics, recordMetrics: this.createRowMetrics(), + treeMetrics: this.createTreeMetrics(), lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex: -1 }, touchStartPosition: {}, selectedRange: { @@ -147,6 +151,15 @@ class Records extends Component { }; }; + createTreeMetrics = (props = this.props) => { + if (!props.showRecordAsTree) { + return null; + } + return { + idSelectedNodeMap: {}, + }; + }; + setScrollLeft = (scrollLeft) => { this.resultContainerRef.scrollLeft = scrollLeft; }; @@ -407,31 +420,108 @@ class Records extends Component { this.setState({ selectedPosition: cellPosition }); }; + handleSelectTreeNode = ({ groupRecordIndex, recordIndex }) => { + const { treeMetrics } = this.state; + const node = this.props.getTreeNodeByIndex(recordIndex); + const nodeKey = getTreeNodeKey(node); + if (!nodeKey) return; + + if (TreeMetrics.checkIsTreeNodeSelected(nodeKey, treeMetrics)) { + this.deselectTreeNodeByKey(nodeKey); + this.setState({ lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex: -1 } }); + return; + } + this.selectTreeNodeByKey(nodeKey); + this.setState({ lastRowIdxUiSelected: { groupRecordIndex, recordIndex } }); + }; + onSelectRecord = ({ groupRecordIndex, recordIndex }, e) => { e.stopPropagation(); if (isShiftKeyDown(e)) { this.selectRecordWithShift({ groupRecordIndex, recordIndex }); return; } - const { isGroupView } = this.props; + + const { isGroupView, showRecordAsTree } = this.props; const { recordMetrics } = this.state; - const operateRecord = this.props.recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex }); - if (!operateRecord) { + if (showRecordAsTree) { + this.handleSelectTreeNode({ groupRecordIndex, recordIndex }); return; } + const operateRecord = this.props.recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex }); + if (!operateRecord) return; + const operateRecordId = operateRecord._id; if (RecordMetrics.isRecordSelected(operateRecordId, recordMetrics)) { - this.deselectRecord(operateRecordId); + this.deselectRecordById(operateRecordId); this.setState({ lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex: -1 } }); return; } - this.selectRecord(operateRecordId); + this.selectRecordById(operateRecordId); + this.setState({ lastRowIdxUiSelected: { groupRecordIndex, recordIndex } }); + }; + + getTreeNodesKeysBetweenRange = ({ start, end }) => { + const startIndex = Math.min(start, end); + const endIndex = Math.max(start, end); + let nodeKeys = []; + for (let i = startIndex; i <= endIndex; i++) { + const node = this.props.getTreeNodeByIndex(i); + const nodeKey = getTreeNodeKey(node); + if (nodeKey) { + nodeKeys.push(nodeKey); + } + } + return nodeKeys; + }; + + getRecordIdsBetweenRange = ({ start, end }) => { + const { recordIds: propsRecordIds } = this.props; + const startIndex = Math.min(start, end); + const endIndex = Math.max(start, end); + let recordIds = []; + for (let i = startIndex; i <= endIndex; i++) { + const recordId = propsRecordIds[i]; + if (recordId) { + recordIds.push(recordId); + } + } + return recordIds; + }; + + selectTreeNodesWithShift = ({ groupRecordIndex, recordIndex }) => { + const { lastRowIdxUiSelected, treeMetrics } = this.state; + const node = this.props.getTreeNodeByIndex(recordIndex); + const nodeKey = getTreeNodeKey(node); + if (!nodeKey) return; + + const lastSelectedRecordIndex = lastRowIdxUiSelected.recordIndex; + if (lastSelectedRecordIndex < 0) { + this.selectTreeNodeByKey(nodeKey); + this.setState({ lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex } }); + return; + } + if (recordIndex === lastSelectedRecordIndex || TreeMetrics.checkIsTreeNodeSelected(nodeKey, treeMetrics)) { + this.deselectTreeNodeByKey(nodeKey); + this.setState({ lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex: -1 } }); + return; + } + const nodesKeys = this.getTreeNodesKeysBetweenRange({ start: lastSelectedRecordIndex, end: recordIndex }); + if (nodesKeys.length === 0) { + return; + } + this.selectTreeNodesByKeys(nodesKeys); this.setState({ lastRowIdxUiSelected: { groupRecordIndex, recordIndex } }); }; selectRecordWithShift = ({ groupRecordIndex, recordIndex }) => { - const { recordIds, isGroupView } = this.props; + const { recordIds, isGroupView, showRecordAsTree } = this.props; + if (showRecordAsTree) { + this.selectTreeNodesWithShift({ groupRecordIndex, recordIndex }); + return; + } + const { lastRowIdxUiSelected, recordMetrics } = this.state; let selectedRecordIds = []; if (isGroupView) { @@ -456,12 +546,12 @@ class Records extends Component { } const lastSelectedRecordIndex = lastRowIdxUiSelected.recordIndex; if (lastSelectedRecordIndex < 0) { - this.selectRecord(operateRecordId); + this.selectRecordById(operateRecordId); this.setState({ lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex } }); return; } if (recordIndex === lastSelectedRecordIndex || RecordMetrics.isRecordSelected(operateRecordId, recordMetrics)) { - this.deselectRecord(operateRecordId); + this.deselectRecordById(operateRecordId); this.setState({ lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex: -1 } }); return; } @@ -475,21 +565,7 @@ class Records extends Component { this.setState({ lastRowIdxUiSelected: { groupRecordIndex, recordIndex } }); }; - getRecordIdsBetweenRange = ({ start, end }) => { - const { recordIds: propsRecordIds } = this.props; - const startIndex = Math.min(start, end); - const endIndex = Math.max(start, end); - let recordIds = []; - for (let i = startIndex; i <= endIndex; i++) { - const recordId = propsRecordIds[i]; - if (recordId) { - recordIds.push(recordId); - } - } - return recordIds; - }; - - selectRecord = (recordId) => { + selectRecordById = (recordId) => { const { recordMetrics } = this.state; if (RecordMetrics.isRecordSelected(recordId, recordMetrics)) { return; @@ -514,7 +590,7 @@ class Records extends Component { }); }; - deselectRecord = (recordId) => { + deselectRecordById = (recordId) => { const { recordMetrics } = this.state; if (!RecordMetrics.isRecordSelected(recordId, recordMetrics)) { return; @@ -526,9 +602,51 @@ class Records extends Component { }); }; + selectTreeNodesByKeys = (nodesKeys) => { + const { treeMetrics } = this.state; + let updatedTreeMetrics = { ...treeMetrics }; + TreeMetrics.selectTreeNodesByKeys(nodesKeys, updatedTreeMetrics); + this.setState({ treeMetrics: updatedTreeMetrics }); + }; + + selectTreeNodeByKey = (nodeKey) => { + const { treeMetrics } = this.state; + if (TreeMetrics.checkIsTreeNodeSelected(nodeKey, treeMetrics)) { + return; + } + + let updatedTreeMetrics = { ...treeMetrics }; + TreeMetrics.selectTreeNode(nodeKey, updatedTreeMetrics); + this.setState({ treeMetrics: updatedTreeMetrics }); + }; + + deselectTreeNodeByKey = (nodeKey) => { + const { treeMetrics } = this.state; + if (!TreeMetrics.checkIsTreeNodeSelected(nodeKey, treeMetrics)) { + return; + } + let updatedTreeMetrics = { ...treeMetrics }; + TreeMetrics.deselectTreeNode(nodeKey, updatedTreeMetrics); + this.setState({ treeMetrics: updatedTreeMetrics }); + }; + + selectAllTreeNodes = () => { + const { recordsTree } = this.props; + const { treeMetrics } = this.state; + let updatedTreeMetrics = { ...treeMetrics }; + const allNodesKeys = recordsTree.map((node) => getTreeNodeKey(node)).filter(Boolean); + TreeMetrics.selectTreeNodesByKeys(allNodesKeys, updatedTreeMetrics); + this.setState({ recordMetrics: updatedTreeMetrics }); + }; + selectAllRecords = () => { - const { recordIds, isGroupView } = this.props; + const { recordIds, isGroupView, showRecordAsTree } = this.props; const { recordMetrics } = this.state; + if (showRecordAsTree) { + this.selectAllTreeNodes(); + return; + } + let updatedRecordMetrics = { ...recordMetrics }; let selectedRowIds = []; if (isGroupView) { @@ -548,13 +666,29 @@ class Records extends Component { selectedRowIds = recordIds; } RecordMetrics.selectRecordsById(selectedRowIds, updatedRecordMetrics); + this.setState({ recordMetrics: updatedRecordMetrics }); + }; + + deselectAllTreeNodes = () => { + const { treeMetrics } = this.state; + if (!TreeMetrics.checkHasSelectedTreeNodes(treeMetrics)) { + return; + } + let updatedTreeMetrics = { ...treeMetrics }; + TreeMetrics.deselectAllTreeNodes(updatedTreeMetrics); this.setState({ - recordMetrics: updatedRecordMetrics, + treeMetrics: updatedTreeMetrics, + lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex: -1 }, }); }; onDeselectAllRecords = () => { const { recordMetrics } = this.state; + if (this.props.showRecordAsTree) { + this.deselectAllTreeNodes(); + return; + } + if (!RecordMetrics.hasSelectedRecords(recordMetrics)) { return; } @@ -577,13 +711,27 @@ class Records extends Component { }; checkHasSelectedRecord = () => { - const { recordMetrics } = this.state; - if (!RecordMetrics.hasSelectedRecords(recordMetrics)) { + const { showSequenceColumn, showRecordAsTree, treeNodeKeyRecordIdMap } = this.props; + const { recordMetrics, treeMetrics } = this.state; + if (!showSequenceColumn) { return false; } - const selectedRecordIds = RecordMetrics.getSelectedIds(recordMetrics); - const selectedRecords = selectedRecordIds && selectedRecordIds.map(id => this.props.recordGetterById(id)).filter(Boolean); - return selectedRecords && selectedRecords.length > 0; + + let selectedRecordIds = []; + if (showRecordAsTree) { + if (!TreeMetrics.checkHasSelectedTreeNodes(treeMetrics)) { + return false; + } + selectedRecordIds = TreeMetrics.getSelectedIds(treeMetrics, treeNodeKeyRecordIdMap); + } else { + if (!RecordMetrics.hasSelectedRecords(recordMetrics)) { + return false; + } + selectedRecordIds = RecordMetrics.getSelectedIds(recordMetrics); + } + + const selectedRecords = selectedRecordIds.map(id => this.props.recordGetterById(id)).filter(Boolean); + return selectedRecords.length > 0; }; getHorizontalScrollState = ({ gridWidth, columnMetrics, scrollLeft }) => { @@ -612,15 +760,26 @@ class Records extends Component { }; onCellContextMenu = (cell) => { + const { isGroupView, recordGetterByIndex, showRecordAsTree } = this.props; + const { recordMetrics, treeMetrics } = this.state; const { rowIdx: recordIndex, idx, groupRecordIndex } = cell; - const { isGroupView, recordGetterByIndex } = this.props; - const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex }); - if (!record) return; - const { recordMetrics } = this.state; - const recordId = record._id; - if (!RecordMetrics.isRecordSelected(recordId, recordMetrics)) { - this.setState({ recordMetrics: this.createRowMetrics() }); + if (showRecordAsTree) { + const node = this.props.getTreeNodeByIndex(recordIndex); + const nodeKey = getTreeNodeKey(node); + if (!nodeKey) return; + + if (!TreeMetrics.checkIsTreeNodeSelected(nodeKey, treeMetrics)) { + this.setState({ treeMetrics: this.createTreeMetrics() }); + } + } else { + const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex }); + if (!record) return; + + const recordId = record._id; + if (!RecordMetrics.isRecordSelected(recordId, recordMetrics)) { + this.setState({ recordMetrics: this.createRowMetrics() }); + } } // select cell when click out of selectRange @@ -641,6 +800,38 @@ class Records extends Component { getRecordsSummaries = () => {}; + checkIsSelectAll = () => { + const { + recordIds, showSequenceColumn, showRecordAsTree, recordsTree, + } = this.props; + const { recordMetrics, treeMetrics } = this.state; + if (!showSequenceColumn) { + return false; + } + if (showRecordAsTree) { + const allNodesKeys = recordsTree.map((node) => getTreeNodeKey(node)).filter(Boolean); + return TreeMetrics.checkIsSelectedAll(allNodesKeys, treeMetrics); + } + return RecordMetrics.isSelectedAll(recordIds, recordMetrics); + }; + + getColumnVisibleEnd = () => { + const { columnMetrics } = this.state; + const { columns } = columnMetrics; + const { width: tableContentWidth } = this.props.getTableContentRect(); + let columnVisibleEnd = 0; + const contentScrollLeft = this.getScrollLeft(); + let endColumnWidth = tableContentWidth + contentScrollLeft; + for (let i = 0; i < columns.length; i++) { + const { width } = columns[i]; + endColumnWidth = endColumnWidth - width; + if (endColumnWidth < 0) { + return columnVisibleEnd = i; + } + } + return columnVisibleEnd; + }; + renderRecordsBody = ({ containerWidth }) => { const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx } = this.state; const { columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth } = columnMetrics; @@ -655,6 +846,7 @@ class Records extends Component { /> ), hasSelectedRecord: this.checkHasSelectedRecord(), + getColumnVisibleEnd: this.getColumnVisibleEnd, getScrollLeft: this.getScrollLeft, getScrollTop: this.getScrollTop, selectNone: this.selectNone, @@ -667,6 +859,17 @@ class Records extends Component { onCellContextMenu: this.onCellContextMenu, getTableCanvasContainerRect: this.getTableCanvasContainerRect, }; + if (this.props.showRecordAsTree) { + return ( + this.bodyRef = ref} + {...commonProps} + recordsTree={this.props.recordsTree} + treeMetrics={this.state.treeMetrics} + storeFoldedTreeNodes={this.props.storeFoldedTreeNodes} + /> + ); + } if (this.props.isGroupView) { return ( @@ -721,6 +924,7 @@ class Records extends Component { sequenceColumnWidth={sequenceColumnWidth} isSelectedAll={isSelectedAll} isGroupView={isGroupView} + showRecordAsTree={this.props.showRecordAsTree} groupOffsetLeft={groupOffsetLeft} lastFrozenColumnKey={lastFrozenColumnKey} selectNoneRecords={this.selectNone} @@ -749,6 +953,8 @@ class Records extends Component { sequenceColumnWidth={sequenceColumnWidth} groupOffsetLeft={groupOffsetLeft} recordMetrics={recordMetrics} + showRecordAsTree={this.props.showRecordAsTree} + treeMetrics={this.state.treeMetrics} selectedRange={selectedRange} isGroupView={isGroupView} hasSelectedRecord={hasSelectedRecord} @@ -777,11 +983,13 @@ Records.propTypes = { hasMoreRecords: PropTypes.bool, isLoadingMoreRecords: PropTypes.bool, isGroupView: PropTypes.bool, + showRecordAsTree: PropTypes.bool, groupOffsetLeft: PropTypes.number, recordIds: PropTypes.array, recordsCount: PropTypes.number, groups: PropTypes.array, groupbys: PropTypes.array, + recordsTree: PropTypes.array, searchResult: PropTypes.object, showGridFooter: PropTypes.bool, supportCopy: PropTypes.bool, 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 12db21ffc3..b579a68402 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 @@ -51,8 +51,15 @@ class ActionsCell extends Component { ); }; + getRecordNo = () => { + if (this.props.showRecordAsTree) { + return this.props.treeNodeDisplayIndex; + } + return this.props.index + 1; + }; + render() { - const { isSelected, isLastFrozenCell, index, height, recordId } = this.props; + const { isSelected, isLastFrozenCell, height, recordId } = this.props; const cellStyle = { height, width: SEQUENCE_COLUMN_WIDTH, @@ -66,7 +73,7 @@ class ActionsCell extends Component { onMouseEnter={this.onCellMouseEnter} onMouseLeave={this.onCellMouseLeave} > - {!isSelected &&
{index + 1}
} + {!isSelected &&
{this.getRecordNo()}
}
{ const cellEditable = useMemo(() => { return checkIsColumnEditable(column) && checkCanModifyRecord && checkCanModifyRecord(record); }, [column, record, checkCanModifyRecord]); + const isNameColumn = useMemo(() => { + return checkIsNameColumn(column); + }, [column]); + const className = useMemo(() => { const { type } = column; return classnames('sf-table-cell', `sf-table-${type}-cell`, highlightClassName, { @@ -35,10 +45,11 @@ const Cell = React.memo(({ 'last-cell': isLastCell, 'table-last--frozen': isLastFrozenCell, 'cell-selected': isCellSelected, + 'name-cell': isNameColumn, // 'dragging-file-to-cell': , // 'row-comment-cell': , }); - }, [cellEditable, column, highlightClassName, isLastCell, isLastFrozenCell, isCellSelected]); + }, [cellEditable, column, highlightClassName, isLastCell, isLastFrozenCell, isCellSelected, isNameColumn]); const style = useMemo(() => { const { left, width } = column; @@ -155,10 +166,26 @@ const Cell = React.memo(({ style, ...cellEvents, }; + + const renderCellContent = useCallback(() => { + const columnFormatter = isValidElement(column.formatter) && cloneElement(column.formatter, { isCellSelected, value: cellValue, column, record, onChange: modifyRecord }); + if (showRecordAsTree && isNameColumn) { + return ( +
+ {hasSubNodes && } +
+ {columnFormatter} +
+
+ ); + } + return columnFormatter; + }, [isNameColumn, column, isCellSelected, cellValue, record, showRecordAsTree, nodeDepth, hasSubNodes, isFoldedNode, modifyRecord, toggleExpandNode]); + return (
- {isValidElement(column.formatter) && cloneElement(column.formatter, { isCellSelected, value: cellValue, column, record, onChange: modifyRecord })} - {(isCellSelected && isValidElement(cellMetaData.CellOperationBtn)) && (cloneElement(cellMetaData.CellOperationBtn, { record, column }))} + {renderCellContent()} + {(isCellSelected && isValidElement(cellMetaData.CellOperationBtn)) && cloneElement(cellMetaData.CellOperationBtn, { record, column })}
); }, (props, nextProps) => { @@ -184,6 +211,11 @@ Cell.propTypes = { modifyRecord: PropTypes.func, highlightClassName: PropTypes.string, bgColor: PropTypes.string, + showRecordAsTree: PropTypes.bool, + nodeDepth: PropTypes.number, + hasSubNodes: PropTypes.bool, + isFoldedNode: PropTypes.bool, + toggleExpandNode: PropTypes.func, }; export default Cell; 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 08ff6326f7..795ce6e975 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 @@ -34,7 +34,13 @@ class Record extends React.Component { nextProps.left !== this.props.left || nextProps.height !== this.props.height || nextProps.searchResult !== this.props.searchResult || - nextProps.columnColor !== this.props.columnColor + nextProps.columnColor !== this.props.columnColor || + nextProps.showRecordAsTree !== this.props.showRecordAsTree || + nextProps.nodeKey !== this.props.nodeKey || + nextProps.nodeDepth !== this.props.nodeDepth || + nextProps.hasSubNodes !== this.props.hasSubNodes || + nextProps.treeNodeDisplayIndex !== this.props.treeNodeDisplayIndex || + nextProps.isFoldedNode !== this.props.isFoldedNode ); } @@ -109,6 +115,11 @@ class Record extends React.Component { reloadCurrentRecord={this.reloadCurrentRecord} highlightClassName={highlightClassName} bgColor={bgColor} + showRecordAsTree={this.props.showRecordAsTree} + nodeDepth={this.props.nodeDepth} + hasSubNodes={this.props.hasSubNodes} + isFoldedNode={this.props.isFoldedNode} + toggleExpandNode={this.props.toggleExpandNode} /> ); }); @@ -171,6 +182,11 @@ class Record extends React.Component { reloadCurrentRecord={this.reloadCurrentRecord} highlightClassName={highlightClassName} bgColor={bgColor} + showRecordAsTree={this.props.showRecordAsTree} + nodeDepth={this.props.nodeDepth} + hasSubNodes={this.props.hasSubNodes} + isFoldedNode={this.props.isFoldedNode} + toggleExpandNode={this.props.toggleExpandNode} /> ); }); @@ -256,6 +272,8 @@ class Record extends React.Component { isSelected={isSelected} recordId={record._id} index={index} + showRecordAsTree={this.props.showRecordAsTree} + treeNodeDisplayIndex={this.props.treeNodeDisplayIndex} onSelectRecord={this.onSelectRecord} isLastFrozenCell={!lastFrozenColumnKey} height={cellHeight} @@ -297,6 +315,13 @@ Record.propTypes = { reloadRecords: PropTypes.func, searchResult: PropTypes.object, columnColor: PropTypes.object, + showRecordAsTree: PropTypes.bool, + nodeKey: PropTypes.string, + nodeDepth: PropTypes.number, + hasSubNodes: PropTypes.bool, + treeNodeDisplayIndex: PropTypes.number, + isFoldedNode: PropTypes.bool, + toggleExpandNode: PropTypes.func, }; export default Record; 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 new file mode 100644 index 0000000000..95d39a4dc2 --- /dev/null +++ b/frontend/src/components/sf-table/table-main/records/tree-body.js @@ -0,0 +1,703 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Loading } from '@seafile/sf-metadata-ui-component'; +import { RightScrollbar } from '../../scrollbar'; +import InteractionMasks from '../../masks/interaction-masks'; +import Record from './record'; +import EventBus from '../../../common/event-bus'; +import { EVENT_BUS_TYPE } from '../../constants/event-bus-type'; +import { checkIsTreeNodeShown, getTreeNodeId, getTreeNodeKey, getValidKeyTreeNodeFoldedMap } from '../../utils/tree'; +import { isShiftKeyDown } from '../../../../utils/keyboard-utils'; +import { getColumnScrollPosition, getColVisibleStartIdx, getColVisibleEndIdx } from '../../utils/records-body-utils'; +import { checkEditableViaClickCell, checkIsColumnSupportDirectEdit, getColumnByIndex, getColumnIndexByKey } from '../../utils/column'; +import { checkIsCellSupportOpenEditor } from '../../utils/selected-cell-utils'; +import { LOCAL_KEY_TREE_NODE_FOLDED } from '../../constants/tree'; +import { TreeMetrics } from '../../utils/tree-metrics'; + +const ROW_HEIGHT = 33; +const RENDER_MORE_NUMBER = 10; +const CONTENT_HEIGHT = window.innerHeight - 174; +const { max, min, ceil, round } = Math; + +class TreeBody extends Component { + + static defaultProps = { + editorPortalTarget: document.body, + scrollToRowIndex: 0, + }; + + constructor(props) { + super(props); + const { recordsTree, treeNodeKeyRecordIdMap, keyTreeNodeFoldedMap } = props; + const validKeyTreeNodeFoldedMap = getValidKeyTreeNodeFoldedMap(keyTreeNodeFoldedMap, treeNodeKeyRecordIdMap); + const nodes = this.getShownNodes(recordsTree, validKeyTreeNodeFoldedMap); + this.state = { + nodes, + startRenderIndex: 0, + endRenderIndex: this.getInitEndIndex(nodes), + keyNodeFoldedMap: validKeyTreeNodeFoldedMap, + selectedPosition: null, + isScrollingRightScrollbar: false, + }; + this.eventBus = EventBus.getInstance(); + this.resultContentRef = null; + this.resultRef = null; + this.rowVisibleStart = 0; + this.rowVisibleEnd = this.setRecordVisibleEnd(); + this.columnVisibleStart = 0; + this.columnVisibleEnd = this.props.getColumnVisibleEnd(); + this.timer = null; + this.initFrozenNodesRef(); + } + + componentDidMount() { + this.props.onRef(this); + window.sfTableBody = this; + this.unsubscribeFocus = this.eventBus.subscribe(EVENT_BUS_TYPE.FOCUS_CANVAS, this.onFocus); + this.unsubscribeSelectColumn = this.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_COLUMN, this.onColumnSelect); + } + + UNSAFE_componentWillReceiveProps(nextProps) { + const { recordsCount, recordIds, treeNodesCount, recordsTree } = nextProps; + if ( + recordsCount !== this.props.recordsCount || recordIds !== this.props.recordIds || + treeNodesCount !== this.props.treeNodesCount || recordsTree !== this.props.recordsTree + ) { + this.recalculateRenderIndex(recordsTree); + } + } + + componentWillUnmount() { + this.storeScrollPosition(); + this.clearHorizontalScroll(); + this.clearScrollbarTimer(); + this.unsubscribeFocus(); + this.unsubscribeSelectColumn(); + window.sfTableBody = null; + this.setState = (state, callback) => { + return; + }; + } + + initFrozenNodesRef = () => { + this.recordFrozenRefs = []; + }; + + addFrozenNodeRef = (node) => { + this.recordFrozenRefs.push(node); + }; + + getShownNodes = (recordsTree, keyNodeFoldedMap) => { + if (!Array.isArray(recordsTree)) { + return []; + } + let shownNodes = []; + recordsTree.forEach((node, index) => { + const nodeId = getTreeNodeId(node); + const row = this.props.recordGetterById(nodeId); + const nodeKey = getTreeNodeKey(node); + if (row && checkIsTreeNodeShown(nodeKey, keyNodeFoldedMap)) { + shownNodes.push({ + ...node, + node_display_index: index + 1, + }); + } + }); + return shownNodes; + }; + + getInitEndIndex = (nodes) => { + if (nodes.length === 0) { + return 0; + } + return Math.min(Math.ceil(window.innerHeight / ROW_HEIGHT) + RENDER_MORE_NUMBER, nodes.length); + }; + + recalculateRenderEndIndex = (nodes) => { + const { height } = this.props.getTableContentRect(); + const contentScrollTop = this.resultContentRef.scrollTop; + return Math.min(Math.ceil((contentScrollTop + height) / ROW_HEIGHT) + RENDER_MORE_NUMBER, nodes.length); + }; + + recalculateRenderIndex = (recordsTree) => { + const { startRenderIndex, endRenderIndex, keyNodeFoldedMap } = this.state; + const nodes = this.getShownNodes(recordsTree, keyNodeFoldedMap); + const contentScrollTop = this.resultContentRef.scrollTop; + const start = Math.max(0, Math.floor(contentScrollTop / ROW_HEIGHT) - RENDER_MORE_NUMBER); + const end = this.recalculateRenderEndIndex(nodes); + const updates = { nodes }; + if (start !== startRenderIndex) { + updates.startRenderIndex = start; + } + if (end !== endRenderIndex) { + updates.endRenderIndex = end; + } + this.setState(updates); + }; + + clearScrollbarTimer = () => { + if (!this.scrollbarTimer) return; + clearTimeout(this.scrollbarTimer); + this.scrollbarTimer = null; + }; + + clearHorizontalScroll = () => { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }; + + setResultContentRef = (ref) => { + this.resultContentRef = ref; + }; + + setResultRef = (ref) => { + this.resultRef = ref; + }; + + setRightScrollbar = (ref) => { + this.rightScrollbar = ref; + }; + + setInteractionMaskRef = (ref) => { + this.interactionMask = ref; + }; + + setRecordVisibleEnd = () => { + return max(ceil(CONTENT_HEIGHT / ROW_HEIGHT), 0); + }; + + setScrollTop = (scrollTop) => { + this.resultContentRef.scrollTop = scrollTop; + }; + + setRightScrollbarScrollTop = (scrollTop) => { + this.rightScrollbar && this.rightScrollbar.setScrollTop(scrollTop); + }; + + setScrollLeft = (scrollLeft, scrollTop) => { + const { interactionMask } = this; + interactionMask && interactionMask.setScrollLeft(scrollLeft, scrollTop); + }; + + cancelSetScrollLeft = () => { + const { interactionMask } = this; + interactionMask && interactionMask.cancelSetScrollLeft(); + }; + + storeScrollPosition = () => { + this.props.storeScrollPosition(); + }; + + getCanvasClientHeight = () => { + return (this.resultContentRef && this.resultContentRef.clientHeight) || 0; + }; + + getClientScrollTopOffset = (node) => { + const rowHeight = this.getRowHeight(); + const scrollVariation = node.scrollTop % rowHeight; + return scrollVariation > 0 ? rowHeight - scrollVariation : 0; + }; + + getRecordsWrapperScrollHeight = () => { + return (this.resultRef && this.resultRef.scrollHeight) || 0; + }; + + getTreeNodeByIndex = (nodeIndex) => { + const { nodes } = this.state; + return nodes[nodeIndex]; + }; + + getRowHeight = () => { + return ROW_HEIGHT; + }; + + getRowTop = (rowIdx) => { + return ROW_HEIGHT * rowIdx; + }; + + getScrollTop = () => { + return this.resultContentRef ? this.resultContentRef.scrollTop : 0; + }; + + getVisibleIndex = () => { + return { rowVisibleStartIdx: this.rowVisibleStart, rowVisibleEndIdx: this.rowVisibleEnd }; + }; + + getCellMetaData = () => { + if (!this.cellMetaData) { + this.cellMetaData = { + CellOperationBtn: this.props.CellOperationBtn, + onCellClick: this.onCellClick, + onCellDoubleClick: this.onCellDoubleClick, + onCellMouseDown: this.onCellMouseDown, + onCellMouseEnter: this.onCellMouseEnter, + onCellMouseMove: this.onCellMouseMove, + onDragEnter: this.handleDragEnter, + modifyRecord: this.props.modifyRecord, + onCellContextMenu: this.onCellContextMenu, + }; + } + return this.cellMetaData; + }; + + onFocus = () => { + if (this.interactionMask.container) { + this.interactionMask.focus(); + return; + } + this.resultContentRef.focus(); + }; + + onColumnSelect = (column) => { + const { columns } = this.props; + const selectColumnIndex = getColumnIndexByKey(column.key, columns); + this.setState({ + selectedPosition: { ...this.state.selectedPosition, idx: selectColumnIndex, rowIdx: 0 }, + }); + }; + + scrollToRight = () => { + if (this.timer) return; + this.timer = setInterval(() => { + const scrollLeft = this.props.getScrollLeft(); + this.props.setRecordsScrollLeft(scrollLeft + 20); + }, 10); + }; + + scrollToLeft = () => { + if (this.timer) return; + this.timer = setInterval(() => { + const scrollLeft = this.props.getScrollLeft(); + if (scrollLeft <= 0) { + this.clearHorizontalScroll(); + return; + } + this.props.setRecordsScrollLeft(scrollLeft - 20); + }, 10); + }; + + onScroll = () => { + const { nodes, startRenderIndex, endRenderIndex } = this.state; + const { offsetHeight, scrollTop: contentScrollTop } = this.resultContentRef; + const nodesCount = nodes.length; + + // Calculate the start rendering row index, and end rendering row index + const start = Math.max(0, Math.floor(contentScrollTop / ROW_HEIGHT) - RENDER_MORE_NUMBER); + const end = Math.min(Math.ceil((contentScrollTop + this.resultContentRef.offsetHeight) / ROW_HEIGHT) + RENDER_MORE_NUMBER, nodesCount); + + this.oldScrollTop = contentScrollTop; + const renderedRecordsCount = ceil(this.resultContentRef.offsetHeight / ROW_HEIGHT); + const newRecordVisibleStart = max(0, round(contentScrollTop / ROW_HEIGHT)); + const newRecordVisibleEnd = min(newRecordVisibleStart + renderedRecordsCount, nodesCount); + this.rowVisibleStart = newRecordVisibleStart; + this.rowVisibleEnd = newRecordVisibleEnd; + + if (Math.abs(start - startRenderIndex) > 5 || start < 5) { + this.setState({ startRenderIndex: start }); + } + if (Math.abs(end - endRenderIndex) > 5 || end > nodesCount - 5) { + this.setState({ endRenderIndex: end }); + } + // Scroll to the bottom of the page, load more records + if (offsetHeight + contentScrollTop >= this.resultContentRef.scrollHeight) { + if (this.props.scrollToLoadMore) { + this.props.scrollToLoadMore(); + } + } + + if (!this.isScrollingRightScrollbar) { + this.setRightScrollbarScrollTop(this.oldScrollTop); + } + + // solve the bug that the scroll bar disappears when scrolling too fast + this.clearScrollbarTimer(); + this.scrollbarTimer = setTimeout(() => { + this.setState({ isScrollingRightScrollbar: false }); + }, 300); + }; + + onScrollbarScroll = (scrollTop) => { + // solve canvas&rightScrollbar circle scroll problem + if (this.oldScrollTop === scrollTop) { + return; + } + this.setState({ isScrollingRightScrollbar: true }, () => { + this.setScrollTop(scrollTop); + }); + }; + + onScrollbarMouseUp = () => { + this.setState({ isScrollingRightScrollbar: false }); + }; + + updateColVisibleIndex = (scrollLeft) => { + const { columns } = this.props; + const columnVisibleStart = getColVisibleStartIdx(columns, scrollLeft); + const columnVisibleEnd = getColVisibleEndIdx(columns, window.innerWidth, scrollLeft); + this.columnVisibleStart = columnVisibleStart; + this.columnVisibleEnd = columnVisibleEnd; + }; + + scrollToColumn = (idx) => { + const { columns, getTableContentRect } = this.props; + const { width: tableContentWidth } = getTableContentRect(); + const newScrollLeft = getColumnScrollPosition(columns, idx, tableContentWidth); + if (newScrollLeft !== null) { + this.props.setRecordsScrollLeft(newScrollLeft); + } + this.updateColVisibleIndex(newScrollLeft); + }; + + selectNoneCells = () => { + this.interactionMask && this.interactionMask.selectNone(); + const { selectedPosition } = this.state; + if (!selectedPosition || selectedPosition.idx < 0 || selectedPosition.rowIdx < 0) { + return; + } + this.selectNone(); + }; + + selectNone = () => { + this.setState({ selectedPosition: { idx: -1, rowIdx: -1 } }); + }; + + selectCell = (cell, openEditor) => { + this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_CELL, cell, openEditor); + }; + + selectStart = (cellPosition) => { + this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_START, cellPosition); + }; + + selectUpdate = (cellPosition, isFromKeyboard, callback) => { + this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_UPDATE, cellPosition, isFromKeyboard, callback); + }; + + selectEnd = () => { + this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_END); + }; + + onHitTopCanvas = () => { + const rowHeight = this.getRowHeight(); + const node = this.resultContentRef; + node.scrollTop -= (rowHeight - this.getClientScrollTopOffset(node)); + }; + + onHitBottomCanvas = () => { + const rowHeight = this.getRowHeight(); + const node = this.resultContentRef; + node.scrollTop += rowHeight + this.getClientScrollTopOffset(node); + }; + + onCellClick = (cell, e) => { + const { selectedPosition } = this.state; + if (isShiftKeyDown(e)) { + if (!selectedPosition || selectedPosition.idx === -1) { + // need select cell first + this.selectCell(cell, false); + return; + } + const isFromKeyboard = true; + this.selectUpdate(cell, isFromKeyboard); + } else { + const { columns, recordGetterByIndex, checkCanModifyRecord } = this.props; + const column = getColumnByIndex(cell.idx, columns); + const supportOpenEditor = checkIsColumnSupportDirectEdit(column); + const hasOpenPermission = checkIsCellSupportOpenEditor(cell, column, false, recordGetterByIndex, checkCanModifyRecord); + this.selectCell(cell, supportOpenEditor && hasOpenPermission); + } + this.props.onCellClick(cell); + this.setState({ selectedPosition: cell }); + }; + + onCellDoubleClick = (cell, e) => { + const { columns, recordGetterByIndex, checkCanModifyRecord } = this.props; + const column = getColumnByIndex(cell.idx, columns); + const supportOpenEditor = checkEditableViaClickCell(column); + const hasOpenPermission = checkIsCellSupportOpenEditor(cell, column, false, recordGetterByIndex, checkCanModifyRecord); + this.selectCell(cell, supportOpenEditor && hasOpenPermission); + }; + + onCellMouseDown = (cellPosition, event) => { + // onRangeSelectStart + if (!isShiftKeyDown(event)) { + this.selectCell(cellPosition); + this.selectStart(cellPosition); + window.addEventListener('mouseup', this.onWindowMouseUp); + } + }; + + onCellMouseEnter = (cellPosition) => { + // onRangeSelectUpdate + this.selectUpdate(cellPosition, false, this.updateViewableArea); + }; + + onCellMouseMove = (cellPosition) => { + this.selectUpdate(cellPosition, false, this.updateViewableArea); + }; + + onWindowMouseUp = (event) => { + window.removeEventListener('mouseup', this.onWindowMouseUp); + if (isShiftKeyDown(event)) return; + this.selectEnd(); + this.clearHorizontalScroll(); + }; + + onCellRangeSelectionUpdated = (selectedRange) => { + this.props.onCellRangeSelectionUpdated(selectedRange); + }; + + onCellContextMenu = (cellPosition) => { + this.setState({ + selectedPosition: Object.assign({}, this.state.selectedPosition, cellPosition), + }); + this.props.onCellContextMenu(cellPosition); + }; + + handleDragEnter = ({ overRecordIdx, overGroupRecordIndex }) => { + this.eventBus.dispatch(EVENT_BUS_TYPE.DRAG_ENTER, { overRecordIdx, overGroupRecordIndex }); + }; + + /** + * When updating the selection by moving the mouse, you need to automatically scroll to expand the visible area + * @param {object} selectedRange + */ + updateViewableArea = (selectedRange) => { + const { sequenceColumnWidth } = this.props; + const { mousePosition } = selectedRange.cursorCell; + const { x: mouseX, y: mouseY } = mousePosition; + const tableHeaderHeight = 50 + 48 + 32; + const interval = 100; + const step = 8; + + // cursor is at right boundary + if (mouseX + interval > window.innerWidth) { + this.scrollToRight(); + } else if (mouseX - interval < sequenceColumnWidth + this.props.frozenColumnsWidth) { + // cursor is at left boundary + this.scrollToLeft(); + } else if (mouseY + interval > window.innerHeight - tableHeaderHeight) { + // cursor is at bottom boundary + const scrollTop = this.getScrollTop(); + this.resultContentRef.scrollTop = scrollTop + step; + this.clearHorizontalScroll(); + } else if (mouseY - interval < tableHeaderHeight) { + // cursor is at top boundary + const scrollTop = this.getScrollTop(); + if (scrollTop - 16 >= 0) { + this.resultContentRef.scrollTop = scrollTop - step; + } + this.clearHorizontalScroll(); + } else { + // cursor is at middle area + this.clearHorizontalScroll(); + } + }; + + toggleExpandNode = (nodeKey) => { + const { recordsTree } = this.props; + const { keyNodeFoldedMap, endRenderIndex } = this.state; + let updatedKeyNodeFoldedMap = { ...keyNodeFoldedMap }; + if (updatedKeyNodeFoldedMap[nodeKey]) { + delete updatedKeyNodeFoldedMap[nodeKey]; + } else { + updatedKeyNodeFoldedMap[nodeKey] = true; + } + + if (this.props.storeFoldedTreeNodes) { + // store folded status + this.props.storeFoldedTreeNodes(LOCAL_KEY_TREE_NODE_FOLDED, updatedKeyNodeFoldedMap); + } + + const updatedNodes = this.getShownNodes(recordsTree, updatedKeyNodeFoldedMap); + let updates = { nodes: updatedNodes, keyNodeFoldedMap: updatedKeyNodeFoldedMap }; + const end = this.recalculateRenderEndIndex(updatedNodes); + if (end !== endRenderIndex) { + updates.endRenderIndex = end; + } + this.setState(updates); + }; + + getVisibleNodesInRange = () => { + const { nodes, startRenderIndex, endRenderIndex } = this.state; + return nodes.slice(startRenderIndex, endRenderIndex); + }; + + renderRecords = () => { + const { treeMetrics, showCellColoring, columnColors } = this.props; + const { nodes, keyNodeFoldedMap, startRenderIndex, endRenderIndex, selectedPosition } = this.state; + this.initFrozenNodesRef(); + const visibleNodes = this.getVisibleNodesInRange(); + const nodesCount = nodes.length; + const lastRecordIndex = nodesCount - 1; + const scrollLeft = this.props.getScrollLeft(); + const rowHeight = this.getRowHeight(); + const cellMetaData = this.getCellMetaData(); + let shownNodes = visibleNodes.map((node, index) => { + const { _id: recordId, node_key, node_depth, has_sub_nodes, node_display_index } = node; + const record = this.props.recordGetterById(recordId); + const isSelected = TreeMetrics.checkIsTreeNodeSelected(node_key, treeMetrics); + const recordIndex = startRenderIndex + index; + const isLastRecord = lastRecordIndex === recordIndex; + const hasSelectedCell = this.props.hasSelectedCell({ recordIndex }, selectedPosition); + const columnColor = showCellColoring ? columnColors[recordId] : {}; + const isFoldedNode = !!keyNodeFoldedMap[node_key]; + return ( + { + this.addFrozenNodeRef(ref); + }} + isSelected={isSelected} + index={recordIndex} + treeNodeDisplayIndex={node_display_index} + isLastRecord={isLastRecord} + showSequenceColumn={this.props.showSequenceColumn} + record={record} + columns={this.props.columns} + sequenceColumnWidth={this.props.sequenceColumnWidth} + colOverScanStartIdx={this.props.colOverScanStartIdx} + colOverScanEndIdx={this.props.colOverScanEndIdx} + lastFrozenColumnKey={this.props.lastFrozenColumnKey} + scrollLeft={scrollLeft} + height={rowHeight} + cellMetaData={cellMetaData} + columnColor={columnColor} + searchResult={this.props.searchResult} + nodeKey={node_key} + nodeDepth={node_depth} + hasSubNodes={has_sub_nodes} + isFoldedNode={isFoldedNode} + checkCanModifyRecord={this.props.checkCanModifyRecord} + checkCellValueChanged={this.props.checkCellValueChanged} + hasSelectedCell={hasSelectedCell} + selectedPosition={selectedPosition} + selectNoneCells={this.selectNoneCells} + onSelectRecord={this.props.onSelectRecord} + toggleExpandNode={() => this.toggleExpandNode(node_key)} + /> + ); + }); + + const upperHeight = startRenderIndex * ROW_HEIGHT; + const belowHeight = (nodesCount - endRenderIndex) * ROW_HEIGHT; + + // add top placeholder + if (upperHeight > 0) { + const style = { height: upperHeight, width: '100%' }; + const upperRow =
; + shownNodes.unshift(upperRow); + } + + // add bottom placeholder + if (belowHeight > 0) { + const style = { height: belowHeight, width: '100%' }; + const belowRow =
; + shownNodes.push(belowRow); + } + return shownNodes; + }; + + render() { + return ( + <> +
+ +
+ {this.renderRecords()} +
+
+ + + ); + } +} + +TreeBody.propTypes = { + onRef: PropTypes.func, + contextMenu: PropTypes.oneOfType([PropTypes.node, PropTypes.element]), + tableId: PropTypes.string, + recordsCount: PropTypes.number, + recordIds: PropTypes.array, + recordsTree: PropTypes.array, + treeNodesCount: PropTypes.number, + treeNodeKeyRecordIdMap: PropTypes.object, + keyTreeNodeFoldedMap: PropTypes.object, + treeMetrics: PropTypes.object, + columns: PropTypes.array.isRequired, + CellOperationBtn: PropTypes.object, + colOverScanStartIdx: PropTypes.number, + colOverScanEndIdx: PropTypes.number, + lastFrozenColumnKey: PropTypes.string, + showSequenceColumn: PropTypes.bool, + sequenceColumnWidth: PropTypes.number, + hasSelectedRecord: PropTypes.bool, + totalWidth: PropTypes.number, + getColumnVisibleEnd: PropTypes.func, + getScrollLeft: PropTypes.func, + setRecordsScrollLeft: PropTypes.func, + storeScrollPosition: PropTypes.func, + hasSelectedCell: PropTypes.func, + scrollToLoadMore: PropTypes.func, + getTableContentRect: PropTypes.func, + getMobileFloatIconStyle: PropTypes.func, + onToggleMobileMoreOperations: PropTypes.func, + onToggleInsertRecordDialog: PropTypes.func, + editorPortalTarget: PropTypes.instanceOf(Element), + recordGetterByIndex: PropTypes.func, + recordGetterById: PropTypes.func, + modifyRecord: PropTypes.func, + selectNone: PropTypes.func, + onCellClick: PropTypes.func, + onCellRangeSelectionUpdated: PropTypes.func, + onSelectRecord: PropTypes.func, + checkCanModifyRecord: PropTypes.func, + paste: PropTypes.func, + searchResult: PropTypes.object, + scrollToRowIndex: PropTypes.number, + frozenColumnsWidth: PropTypes.number, + editMobileCell: PropTypes.func, + reloadRecords: PropTypes.func, + appPage: PropTypes.object, + showCellColoring: PropTypes.bool, + columnColors: PropTypes.object, + onFillingDragRows: PropTypes.func, + getUpdateDraggedRecords: PropTypes.func, + getCopiedRecordsAndColumnsFromRange: PropTypes.func, + onCellContextMenu: PropTypes.func, + getTableCanvasContainerRect: PropTypes.func, + storeFoldedTreeNodes: PropTypes.func, +}; + +export default TreeBody; diff --git a/frontend/src/components/sf-table/tree.css b/frontend/src/components/sf-table/tree.css new file mode 100644 index 0000000000..eede9d4d26 --- /dev/null +++ b/frontend/src/components/sf-table/tree.css @@ -0,0 +1,44 @@ +.sf-table-tree .sf-table-result-content .sf-table-cell.name-cell { + display: inline-flex; + align-items: center; +} + +.sf-table-cell-tree-node { + position: relative; + width: 100%; + display: flex; + align-items: center; +} + +.sf-table-cell-tree-node-content { + position: relative; + width: 100%; + display: flex; + align-items: center; + padding-left: 22px; +} + +.sf-table-tree .sf-table-record-tree-expand-icon { + position: absolute; + display: inline-flex; + left: 0; + top: 50%; + width: 20px; + height: 20px; + align-items: center; + justify-content: center; + transform: translateY(-50%); + z-index: 1; +} + +.sf-table-tree .sf-table-record-tree-expand-icon .sf3-font { + color: #666; +} + +.sf-table-tree .sf-table-record-tree-expand-icon:hover { + cursor: pointer; +} + +.sf-table-tree .sf-table-cell.name-cell .sf-table-cell-formatter { + flex: 1; +} diff --git a/frontend/src/components/sf-table/utils/cell-comparer.js b/frontend/src/components/sf-table/utils/cell-comparer.js index af2b3e324f..64dcaa37a6 100644 --- a/frontend/src/components/sf-table/utils/cell-comparer.js +++ b/frontend/src/components/sf-table/utils/cell-comparer.js @@ -22,6 +22,7 @@ export const checkCellValueChanged = (oldVal, newVal) => { export const cellCompare = (props, nextProps) => { const { record: oldRecord, column, isCellSelected, isLastCell, highlightClassName, height, bgColor, + showRecordAsTree, nodeDepth, hasSubNodes, isFoldedNode, } = props; const { record: newRecord, highlightClassName: newHighlightClassName, height: newHeight, column: newColumn, bgColor: newBgColor, @@ -47,6 +48,10 @@ export const cellCompare = (props, nextProps) => { column.width !== newColumn.width || !ObjectUtils.isSameObject(column.data, newColumn.data) || bgColor !== newBgColor || + showRecordAsTree !== nextProps.showRecordAsTree || + nodeDepth !== nextProps.nodeDepth || + hasSubNodes !== nextProps.hasSubNodes || + isFoldedNode !== nextProps.isFoldedNode || props.groupRecordIndex !== nextProps.groupRecordIndex || props.recordIndex !== nextProps.recordIndex ); diff --git a/frontend/src/components/sf-table/utils/record-metrics.js b/frontend/src/components/sf-table/utils/record-metrics.js index 7ffa0c187e..fa4e4ad5d0 100644 --- a/frontend/src/components/sf-table/utils/record-metrics.js +++ b/frontend/src/components/sf-table/utils/record-metrics.js @@ -42,7 +42,7 @@ function isSelectedAll(recordIds, recordMetrics) { return recordIds.every(recordId => isRecordSelected(recordId, recordMetrics)); } -const recordMetrics = { +export const RecordMetrics = { selectRecord, selectRecordsById, deselectRecord, @@ -52,5 +52,3 @@ const recordMetrics = { hasSelectedRecords, isSelectedAll, }; - -export default recordMetrics; diff --git a/frontend/src/components/sf-table/utils/tree-metrics.js b/frontend/src/components/sf-table/utils/tree-metrics.js new file mode 100644 index 0000000000..97c30ed69f --- /dev/null +++ b/frontend/src/components/sf-table/utils/tree-metrics.js @@ -0,0 +1,62 @@ +import { getRecordsIdsByTreeNodeKeys } from './tree'; + +const checkIsTreeNodeSelected = (nodeKey, treeMetrics) => { + return treeMetrics.idSelectedNodeMap[nodeKey]; +}; + +const selectTreeNode = (nodeKey, treeMetrics) => { + if (checkIsTreeNodeSelected(nodeKey, treeMetrics)) { + return; + } + treeMetrics.idSelectedNodeMap[nodeKey] = true; +}; + +const selectTreeNodesByKeys = (nodeKeys, treeMetrics) => { + nodeKeys.forEach((nodeKey) => { + selectTreeNode(nodeKey, treeMetrics); + }); +}; + +const deselectTreeNode = (nodeKey, treeMetrics) => { + if (!checkIsTreeNodeSelected(nodeKey, treeMetrics)) { + return; + } + delete treeMetrics.idSelectedNodeMap[nodeKey]; +}; + +const deselectAllTreeNodes = (treeMetrics) => { + treeMetrics.idSelectedNodeMap = {}; +}; + +const getSelectedTreeNodesKeys = (treeMetrics) => { + return Object.keys(treeMetrics.idSelectedNodeMap); +}; + +const getSelectedIds = (treeMetrics, treeNodeKeyRecordIdMap) => { + const selectedNodesKeys = getSelectedTreeNodesKeys(treeMetrics); + return getRecordsIdsByTreeNodeKeys(selectedNodesKeys, treeNodeKeyRecordIdMap); +}; + +const checkHasSelectedTreeNodes = (treeMetrics) => { + return getSelectedTreeNodesKeys(treeMetrics).length > 0; +}; + +const checkIsSelectedAll = (nodeKeys, treeMetrics) => { + const selectedNodesKeysLen = getSelectedTreeNodesKeys(treeMetrics).length; + if (selectedNodesKeysLen === 0) { + return false; + } + return nodeKeys.every(nodeKey => checkIsTreeNodeSelected(nodeKey, treeMetrics)); +}; + +export const TreeMetrics = { + checkIsTreeNodeSelected, + selectTreeNode, + selectTreeNodesByKeys, + deselectTreeNode, + deselectAllTreeNodes, + getSelectedTreeNodesKeys, + getSelectedIds, + checkHasSelectedTreeNodes, + checkIsSelectedAll, +}; diff --git a/frontend/src/components/sf-table/utils/tree.js b/frontend/src/components/sf-table/utils/tree.js new file mode 100644 index 0000000000..3b3ff4f802 --- /dev/null +++ b/frontend/src/components/sf-table/utils/tree.js @@ -0,0 +1,76 @@ +import { TREE_NODE_KEY } from '../constants/tree'; + +export const createTreeNode = (nodeId, nodeKey, depth, hasSubNodes) => { + return { + [TREE_NODE_KEY.ID]: nodeId, + [TREE_NODE_KEY.KEY]: nodeKey, + [TREE_NODE_KEY.DEPTH]: depth, + [TREE_NODE_KEY.HAS_SUB_NODES]: hasSubNodes, + }; +}; + +/** + * for get row by node key from 'tree_node_key_row_id_map' directly + * @param {array} tree + * @returns tree_node_key_row_id_map + * tree_node_key_row_id_map: { [node_key]: _id, ... } + */ +export const generateKeyTreeNodeRowIdMap = (tree) => { + let tree_node_key_row_id_map = {}; + tree.forEach((node) => { + tree_node_key_row_id_map[node[TREE_NODE_KEY.KEY]] = node[TREE_NODE_KEY.ID]; + }); + return tree_node_key_row_id_map; +}; + + +export const getValidKeyTreeNodeFoldedMap = (keyTreeNodeFoldedMap, treeNodeKeyRecordIdMap) => { + if (!keyTreeNodeFoldedMap) return {}; + + let validKeyTreeNodeFoldedMap = {}; + Object.keys(keyTreeNodeFoldedMap).forEach((nodeKey) => { + if (treeNodeKeyRecordIdMap[nodeKey]) { + // just keep the folded status of exist nodes + validKeyTreeNodeFoldedMap[nodeKey] = keyTreeNodeFoldedMap[nodeKey]; + } + }); + return validKeyTreeNodeFoldedMap; +}; + +export const getRecordIdByTreeNodeKey = (nodeKey, treeNodeKeyRecordIdMap) => { + return treeNodeKeyRecordIdMap[nodeKey]; +}; + +export const getRecordsIdsByTreeNodeKeys = (nodesKeys, treeNodeKeyRecordIdMap) => { + if (!Array.isArray(nodesKeys) || nodesKeys.length === 0 || !treeNodeKeyRecordIdMap) { + return []; + } + let idExistMap = {}; + let selectedIds = []; + nodesKeys.forEach((nodeKey) => { + const selectedId = treeNodeKeyRecordIdMap[nodeKey]; + if (selectedId && !idExistMap[selectedId]) { + selectedIds.push(nodeKey); + idExistMap[selectedId] = true; + } + }); + return selectedIds; +}; + +export const checkIsTreeNodeShown = (nodeKey, keyFoldedNodeMap) => { + const foldedNodeKeys = keyFoldedNodeMap && Object.keys(keyFoldedNodeMap); + if (!Array.isArray(foldedNodeKeys) || foldedNodeKeys.length === 0) { + return true; + } + + // parent node is folded + return !foldedNodeKeys.some((foldedNodeKey) => nodeKey !== foldedNodeKey && nodeKey.includes(foldedNodeKey)); +}; + +export const getTreeNodeId = (node) => { + return node ? node[TREE_NODE_KEY.ID] : ''; +}; + +export const getTreeNodeKey = (node) => { + return node ? node[TREE_NODE_KEY.KEY] : ''; +}; diff --git a/frontend/src/metadata/components/cell-editors/tags-editor/delete-tags/index.js b/frontend/src/metadata/components/cell-editors/tags-editor/delete-tags/index.js index 050eecae4f..3c81d38073 100644 --- a/frontend/src/metadata/components/cell-editors/tags-editor/delete-tags/index.js +++ b/frontend/src/metadata/components/cell-editors/tags-editor/delete-tags/index.js @@ -2,7 +2,7 @@ import React from './index'; import PropTypes from 'prop-types'; import { IconBtn } from '@seafile/sf-metadata-ui-component'; import { getRowById } from '../../../../utils/table'; -import { getTagColor, getTagName } from '../../../../../tag/utils/cell/core'; +import { getTagColor, getTagName } from '../../../../../tag/utils/cell'; import './index.css'; diff --git a/frontend/src/metadata/components/cell-editors/tags-editor/index.js b/frontend/src/metadata/components/cell-editors/tags-editor/index.js index 4a6b945aa5..d3d30588e8 100644 --- a/frontend/src/metadata/components/cell-editors/tags-editor/index.js +++ b/frontend/src/metadata/components/cell-editors/tags-editor/index.js @@ -6,7 +6,7 @@ import { Utils } from '../../../../utils/utils'; import { KeyCodes } from '../../../../constants'; import { gettext } from '../../../../utils/constants'; import { useTags } from '../../../../tag/hooks'; -import { getTagColor, getTagId, getTagName, getTagsByNameOrColor, getTagByNameOrColor } from '../../../../tag/utils/cell/core'; +import { getTagColor, getTagId, getTagName, getTagsByNameOrColor, getTagByNameOrColor } from '../../../../tag/utils/cell'; import { getRecordIdFromRecord } from '../../../utils/cell'; import { getRowById } from '../../../utils/table'; import { SELECT_OPTION_COLORS } from '../../../constants'; diff --git a/frontend/src/metadata/components/dialog/file-tags-dialog/index.js b/frontend/src/metadata/components/dialog/file-tags-dialog/index.js index 2fe630733d..268424503e 100644 --- a/frontend/src/metadata/components/dialog/file-tags-dialog/index.js +++ b/frontend/src/metadata/components/dialog/file-tags-dialog/index.js @@ -1,19 +1,19 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap'; import { CenteredLoading } from '@seafile/sf-metadata-ui-component'; +import toaster from '../../../../components/toast'; +import EmptyTip from '../../../../components/empty-tip'; +import SeahubModalHeader from '@/components/common/seahub-modal-header'; import { gettext } from '../../../../utils/constants'; import { Utils } from '../../../../utils/utils'; -import { getFileNameFromRecord, getParentDirFromRecord, getTagsFromRecord, getRecordIdFromRecord -} from '../../../utils/cell'; -import toaster from '../../../../components/toast'; -import classNames from 'classnames'; -import { getTagByName, getTagId } from '../../../../tag/utils'; +import { getFileNameFromRecord, getParentDirFromRecord, getTagsFromRecord, getRecordIdFromRecord } from '../../../utils/cell'; +import { getTagByName } from '../../../../tag/utils/row'; +import { getTagId } from '../../../../tag/utils/cell'; import { PRIVATE_COLUMN_KEY as TAGS_PRIVATE_COLUMN_KEY } from '../../../../tag/constants'; import { SELECT_OPTION_COLORS } from '../../../constants'; import { useTags } from '../../../../tag/hooks'; -import EmptyTip from '../../../../components/empty-tip'; -import SeahubModalHeader from '@/components/common/seahub-modal-header'; import './index.css'; diff --git a/frontend/src/metadata/components/popover/filter-popover/basic-filters/tags-filter.js b/frontend/src/metadata/components/popover/filter-popover/basic-filters/tags-filter.js index 804b299c03..2bbb8c9cde 100644 --- a/frontend/src/metadata/components/popover/filter-popover/basic-filters/tags-filter.js +++ b/frontend/src/metadata/components/popover/filter-popover/basic-filters/tags-filter.js @@ -4,7 +4,7 @@ import { CustomizeSelect, Icon, FileTagsFormatter } from '@seafile/sf-metadata-u import { gettext } from '../../../../../utils/constants'; import { useMetadataStatus } from '../../../../../hooks'; import { useTags } from '../../../../../tag/hooks'; -import { getTagId, getTagName, getTagColor } from '../../../../../tag/utils'; +import { getTagId, getTagName, getTagColor } from '../../../../../tag/utils/cell'; import { getRowById } from '../../../../utils/table'; const TagsFilter = ({ readOnly, value: oldValue, onChange: onChangeAPI }) => { diff --git a/frontend/src/metadata/utils/cell/column/tag.js b/frontend/src/metadata/utils/cell/column/tag.js index f60b972eaa..c4ae364c0e 100644 --- a/frontend/src/metadata/utils/cell/column/tag.js +++ b/frontend/src/metadata/utils/cell/column/tag.js @@ -1,4 +1,4 @@ -import { getTagName } from '../../../../tag/utils'; +import { getTagName } from '../../../../tag/utils/cell'; import { getRowById } from '../../table'; export const getTagsDisplayString = (tagsData, cellValue) => { diff --git a/frontend/src/tag/components/dialog/edit-tag-dialog/index.js b/frontend/src/tag/components/dialog/edit-tag-dialog/index.js index 7b24193657..ed94951318 100644 --- a/frontend/src/tag/components/dialog/edit-tag-dialog/index.js +++ b/frontend/src/tag/components/dialog/edit-tag-dialog/index.js @@ -3,14 +3,14 @@ import PropTypes from 'prop-types'; import { Modal, ModalBody, ModalFooter, FormGroup, Input, Button, Alert, Label } from 'reactstrap'; import classnames from 'classnames'; import { IconBtn } from '@seafile/sf-metadata-ui-component'; -import { gettext } from '../../../../utils/constants'; -import { getTagColor, getTagId, getTagName } from '../../../utils/cell/core'; -import { SELECT_OPTION_COLORS } from '../../../../metadata/constants'; -import { isEnter } from '../../../../metadata/utils/hotkey'; -import { isValidTagName } from '../../../utils'; -import { PRIVATE_COLUMN_KEY } from '../../../constants'; import toaster from '../../../../components/toast'; import SeahubModalHeader from '@/components/common/seahub-modal-header'; +import { gettext } from '../../../../utils/constants'; +import { getTagColor, getTagId, getTagName } from '../../../utils/cell'; +import { isValidTagName } from '../../../utils/validate/tag'; +import { SELECT_OPTION_COLORS } from '../../../../metadata/constants'; +import { isEnter } from '../../../../metadata/utils/hotkey'; +import { PRIVATE_COLUMN_KEY } from '../../../constants'; import './index.css'; diff --git a/frontend/src/tag/components/tag-view-name/index.js b/frontend/src/tag/components/tag-view-name/index.js index 8d752c949e..5406548254 100644 --- a/frontend/src/tag/components/tag-view-name/index.js +++ b/frontend/src/tag/components/tag-view-name/index.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useTags } from '../../hooks'; import { getRowById } from '../../../metadata/utils/table'; -import { getTagName } from '../../utils'; +import { getTagName } from '../../utils/cell'; import { ALL_TAGS_ID } from '../../constants'; import { gettext } from '../../../utils/constants'; import AllTagsOperationToolbar from './all-tags-operation-toolbar'; diff --git a/frontend/src/tag/hooks/tags.js b/frontend/src/tag/hooks/tags.js index cc5a07e490..f7c9bc0224 100644 --- a/frontend/src/tag/hooks/tags.js +++ b/frontend/src/tag/hooks/tags.js @@ -3,7 +3,8 @@ import { Utils } from '../../utils/utils'; import toaster from '../../components/toast'; import { useMetadataStatus } from '../../hooks'; import { PRIVATE_FILE_TYPE } from '../../constants'; -import { getTagColor, getTagId, getTagName, getCellValueByColumn, updateFavicon } from '../utils'; +import { getTagColor, getTagId, getTagName, getCellValueByColumn } from '../utils/cell'; +import { updateFavicon } from '../utils/favicon'; import Context from '../context'; import Store from '../store'; import { PER_LOAD_NUMBER, EVENT_BUS_TYPE } from '../../metadata/constants'; diff --git a/frontend/src/tag/store/data-processor.js b/frontend/src/tag/store/data-processor.js index 153bfe0c02..9cdfbfbe50 100644 --- a/frontend/src/tag/store/data-processor.js +++ b/frontend/src/tag/store/data-processor.js @@ -3,6 +3,10 @@ import { getColumnByKey } from '../../metadata/utils/column'; import { getGroupRows } from '../../metadata/utils/group'; import { getRowsByIds } from '../../metadata/utils/table'; import { OPERATION_TYPE } from './operations'; +import { buildTagsTree } from '../utils/tree'; +import { getRecordIdFromRecord } from '../../metadata/utils/cell'; +import { TREE_NODE_KEY } from '../../components/sf-table/constants/tree'; +import { createTreeNode } from '../../components/sf-table/utils/tree'; // const DEFAULT_COMPUTER_PROPERTIES_CONTROLLER = { // isUpdateSummaries: true, @@ -13,6 +17,44 @@ import { OPERATION_TYPE } from './operations'; // get rendered rows depend on filters/sorts etc. class DataProcessor { + static buildTagsTree(rows, table) { + table.rows_tree = buildTagsTree(rows, table); + } + + static updateTagsTreeWithNewTags(tags, table) { + if (!Array.isArray(tags) || tags.length === 0) return; + const { rows_tree } = table; + let updated_rows_tree = [...rows_tree]; + tags.forEach((tag) => { + const tagId = getRecordIdFromRecord(tag); + const nodeKey = tagId; + const node = createTreeNode(tagId, nodeKey, 0, false); + updated_rows_tree.push(node); + }); + table.rows_tree = updated_rows_tree; + } + + static updateTagsTreeWithDeletedTagsIds(deletedTagsIds, table) { + if (!Array.isArray(deletedTagsIds) || deletedTagsIds.length === 0) return; + const { rows_tree } = table; + const idTagDeletedMap = deletedTagsIds.reduce((currIdTagDeletedMap, tagId) => ({ ...currIdTagDeletedMap, [tagId]: true }), {}); + const hasDeletedParentNode = rows_tree.some((node) => idTagDeletedMap[node[TREE_NODE_KEY.ID]] && node[TREE_NODE_KEY.HAS_SUB_NODES]); + if (hasDeletedParentNode) { + // need re-build tree if some parent nodes deleted + this.buildTagsTree(table.rows, table); + return; + } + + // remove the nodes which has no sub nodes directly + let updated_rows_tree = []; + rows_tree.forEach((node) => { + if (!idTagDeletedMap[node[TREE_NODE_KEY.ID]]) { + updated_rows_tree.push(node); + } + }); + table.rows_tree = updated_rows_tree; + } + static getGroupedRows(table, rows, groupbys, { collaborators }) { const tableRows = isTableRows(rows) ? rows : getRowsByIds(table, rows); const groups = getGroupRows(table, tableRows, groupbys, { collaborators }); @@ -57,15 +99,15 @@ class DataProcessor { }; static run(table, { collaborators }) { - // todo + this.buildTagsTree(table.rows, table); } static updateDataWithModifyRecords(table, relatedColumnKeyMap, rowIds, { collaborators }) { // todo } - static updatePageDataWithDeleteRecords(deletedRowsIds, table) { - // todo + static updatePageDataWithDeleteRecords(deletedTagsIds, table) { + this.updateTagsTreeWithDeletedTagsIds(deletedTagsIds, table); } static handleReloadedRecords(table, reloadedRecords, relatedColumnKeyMap) { @@ -142,8 +184,8 @@ class DataProcessor { break; } case OPERATION_TYPE.DELETE_RECORDS: { - const { rows_ids } = operation; - this.updatePageDataWithDeleteRecords(rows_ids, table); + const { tag_ids } = operation; + this.updatePageDataWithDeleteRecords(tag_ids, table); this.updateSummaries(); break; } @@ -151,6 +193,16 @@ class DataProcessor { // todo break; } + case OPERATION_TYPE.ADD_RECORDS: { + const { tags } = operation; + this.updateTagsTreeWithNewTags(tags, table); + break; + } + case OPERATION_TYPE.ADD_TAG_LINKS: + case OPERATION_TYPE.DELETE_TAG_LINKS: { + this.buildTagsTree(table.rows, table); + break; + } default: { break; } diff --git a/frontend/src/tag/tags-tree-view/index.js b/frontend/src/tag/tags-tree-view/index.js index be69c3e162..bad844637b 100644 --- a/frontend/src/tag/tags-tree-view/index.js +++ b/frontend/src/tag/tags-tree-view/index.js @@ -1,10 +1,10 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; -import { useTags } from '../hooks'; -import Tag from './tag'; -import { getTagId } from '../utils'; -import { PRIVATE_FILE_TYPE } from '../../constants'; import AllTags from './all-tags'; +import Tag from './tag'; +import { useTags } from '../hooks'; +import { getTagId } from '../utils/cell'; +import { PRIVATE_FILE_TYPE } from '../../constants'; import './index.css'; diff --git a/frontend/src/tag/tags-tree-view/tag/index.js b/frontend/src/tag/tags-tree-view/tag/index.js index 04d024ce67..234bcef64d 100644 --- a/frontend/src/tag/tags-tree-view/tag/index.js +++ b/frontend/src/tag/tags-tree-view/tag/index.js @@ -1,7 +1,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { getTagColor, getTagName, getTagFilesCount } from '../../utils'; +import { getTagColor, getTagName, getTagFilesCount } from '../../utils/cell'; import './index.css'; diff --git a/frontend/src/tag/utils/cell/core.js b/frontend/src/tag/utils/cell.js similarity index 96% rename from frontend/src/tag/utils/cell/core.js rename to frontend/src/tag/utils/cell.js index d6664d85eb..de1d604eb4 100644 --- a/frontend/src/tag/utils/cell/core.js +++ b/frontend/src/tag/utils/cell.js @@ -1,4 +1,4 @@ -import { PRIVATE_COLUMN_KEYS, PRIVATE_COLUMN_KEY } from '../../constants'; +import { PRIVATE_COLUMN_KEYS, PRIVATE_COLUMN_KEY } from '../constants'; /** * @param {object} record eg: { [column_key]: value, [column_name]: value } diff --git a/frontend/src/tag/utils/cell/index.js b/frontend/src/tag/utils/cell/index.js deleted file mode 100644 index 4b0e041376..0000000000 --- a/frontend/src/tag/utils/cell/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './core'; diff --git a/frontend/src/tag/utils/index.js b/frontend/src/tag/utils/index.js deleted file mode 100644 index dc71cc61db..0000000000 --- a/frontend/src/tag/utils/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export * from './cell'; -export * from './row'; -export * from './validate'; -export * from './favicon'; diff --git a/frontend/src/tag/utils/row/core.js b/frontend/src/tag/utils/row.js similarity index 81% rename from frontend/src/tag/utils/row/core.js rename to frontend/src/tag/utils/row.js index 952da6b1de..9ef3e6751e 100644 --- a/frontend/src/tag/utils/row/core.js +++ b/frontend/src/tag/utils/row.js @@ -1,4 +1,4 @@ -import { getTagName } from '../cell'; +import { getTagName } from './cell'; export const getTagByName = (tagsData, tagName) => { if (!tagsData || !tagName) return null; diff --git a/frontend/src/tag/utils/row/index.js b/frontend/src/tag/utils/row/index.js deleted file mode 100644 index 4b0e041376..0000000000 --- a/frontend/src/tag/utils/row/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './core'; diff --git a/frontend/src/tag/utils/tree.js b/frontend/src/tag/utils/tree.js new file mode 100644 index 0000000000..db8ff042f5 --- /dev/null +++ b/frontend/src/tag/utils/tree.js @@ -0,0 +1,61 @@ +import { createTreeNode } from '../../components/sf-table/utils/tree'; +import { getRecordIdFromRecord } from '../../metadata/utils/cell'; +import { getRowsByIds } from '../../metadata/utils/table'; +import { getParentLinks, getSubLinks } from './cell'; + +const setSubNodes = (row, parentDepth, parentKey, idNodeInCurrentTreeMap, idNodeCreatedMap, tree, table) => { + const nodeId = getRecordIdFromRecord(row); + + idNodeCreatedMap[nodeId] = true; + idNodeInCurrentTreeMap[nodeId] = true; // for preventing circular dependencies + + const nodeKey = `${parentKey ? parentKey + '_' : ''}${nodeId}`; // the unique ID of each node + const subLinks = getSubLinks(row); + const subRowsIds = subLinks.map((link) => link.row_id); + const subRows = getRowsByIds(table, subRowsIds); + const validSubRows = subRows.filter((row) => row && !idNodeInCurrentTreeMap[row._id]); + + const node = createTreeNode(nodeId, nodeKey, parentDepth, validSubRows.length > 0); + tree.push(node); + + if (validSubRows) { + const nextNodeDepth = parentDepth + 1; + validSubRows.forEach((subRow) => { + setSubNodes(subRow, nextNodeDepth, nodeKey, { ...idNodeInCurrentTreeMap }, idNodeCreatedMap, tree, table); + }); + } + + delete idNodeInCurrentTreeMap[nodeId]; +}; + +/** + * generate tree for display in table + * @param {array} rows tags + * @returns {array} tree + * tree: [ + * { _id, node_depth, node_key, has_sub_nodes, ... } + * ... + * ] + */ +export const buildTagsTree = (rows, table) => { + const idNodeCreatedMap = {}; // mark each row has created tree node + const tree = []; + rows.forEach((row) => { + const nodeId = getRecordIdFromRecord(row); + const parentLinks = getParentLinks(row); + if (parentLinks.length === 0 && !idNodeCreatedMap[nodeId]) { + setSubNodes(row, 0, '', {}, idNodeCreatedMap, tree, table); + } + }); + + // rows which may be from circular dependencies + const noneCreatedRows = rows.filter((row) => !idNodeCreatedMap[getRecordIdFromRecord(row)]); + noneCreatedRows.forEach((row) => { + const nodeId = getRecordIdFromRecord(row); + if (!idNodeCreatedMap[nodeId]) { + setSubNodes(row, 0, '', {}, idNodeCreatedMap, tree, table); + } + }); + + return tree; +}; diff --git a/frontend/src/tag/utils/validate/index.js b/frontend/src/tag/utils/validate/index.js deleted file mode 100644 index 584cc0afa1..0000000000 --- a/frontend/src/tag/utils/validate/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './tag'; diff --git a/frontend/src/tag/views/all-tags/index.js b/frontend/src/tag/views/all-tags/index.js index cc5bf86544..5de83945d6 100644 --- a/frontend/src/tag/views/all-tags/index.js +++ b/frontend/src/tag/views/all-tags/index.js @@ -8,7 +8,7 @@ import { EVENT_BUS_TYPE, PER_LOAD_NUMBER } from '../../../metadata/constants'; import { Utils } from '../../../utils/utils'; import { PRIVATE_FILE_TYPE } from '../../../constants'; import { getRowById } from '../../../components/sf-table/utils/table'; -import { getTagName } from '../../utils'; +import { getTagName } from '../../utils/cell'; import { ALL_TAGS_ID } from '../../constants'; import './index.css'; @@ -62,7 +62,7 @@ const AllTags = ({ updateCurrentPath, ...params }) => { } }, [isLoading, isReloading, onChangeDisplayTag]); - if (isLoading || isReloading) return (); + if (isReloading) return (); if (displayTag) { return ( diff --git a/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js b/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js index 30fcf62202..32e71828a8 100644 --- a/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js +++ b/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js @@ -4,6 +4,8 @@ import { checkIsNameColumn, getColumnByIndex } from '../../../../components/sf-t import EventBus from '../../../../components/common/event-bus'; import { EVENT_BUS_TYPE } from '../../../../components/sf-table/constants/event-bus-type'; import { PRIVATE_COLUMN_KEY } from '../../../constants'; +import { TreeMetrics } from '../../../../components/sf-table/utils/tree-metrics'; +import { RecordMetrics } from '../../../../components/sf-table/utils/record-metrics'; const OPERATION = { EDIT_TAG: 'edit_tag', @@ -19,6 +21,9 @@ export const createContextMenuOptions = ({ columns, recordMetrics, isGroupView, + showRecordAsTree, + treeMetrics, + treeNodeKeyRecordIdMap, hideMenu, recordGetterByIndex, recordGetterById, @@ -82,7 +87,12 @@ export const createContextMenuOptions = ({ return options; } - const selectedRecordsIds = recordMetrics ? Object.keys(recordMetrics.idSelectedRecordMap) : []; + let selectedRecordsIds = []; + if (showRecordAsTree) { + selectedRecordsIds = (treeMetrics && TreeMetrics.getSelectedIds(treeMetrics, treeNodeKeyRecordIdMap)) || []; + } else { + selectedRecordsIds = (recordMetrics && RecordMetrics.getSelectedIds(recordMetrics)) || []; + } if (selectedRecordsIds.length > 1) { let tagsIds = []; selectedRecordsIds.forEach(id => { @@ -108,7 +118,6 @@ export const createContextMenuOptions = ({ const tag = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex }); const column = getColumnByIndex(idx, columns); if (!tag || !tag._id || !column) return options; - if (checkIsNameColumn(column)) { options.push({ label: gettext('Edit tag'), diff --git a/frontend/src/tag/views/all-tags/tags-table/editors/parent-tags.js b/frontend/src/tag/views/all-tags/tags-table/editors/parent-tags.js index a7b4e815a3..982af7a4e7 100644 --- a/frontend/src/tag/views/all-tags/tags-table/editors/parent-tags.js +++ b/frontend/src/tag/views/all-tags/tags-table/editors/parent-tags.js @@ -1,13 +1,19 @@ import React, { forwardRef, useCallback, useMemo } from 'react'; import TagsEditor from '../../../../../components/sf-table/editors/tags-editor'; import { useTags } from '../../../../hooks'; +import { getRowById } from '../../../../../components/sf-table/utils/table'; +import { getParentLinks } from '../../../../utils/cell'; -const ParentTagsEditor = forwardRef(({ record, column, addTagLinks, deleteTagLinks, ...editorProps }, ref) => { +const ParentTagsEditor = forwardRef(({ editingRowId, column, addTagLinks, deleteTagLinks, ...editorProps }, ref) => { const { tagsData } = useTags(); + const tag = useMemo(() => { + return getRowById(tagsData, editingRowId); + }, [tagsData, editingRowId]); + const parentLinks = useMemo(() => { - return record[column.key] || []; - }, [record, column]); + return getParentLinks(tag); + }, [tag]); const selectTag = useCallback((tagId, recordId) => { addTagLinks(column.key, recordId, [tagId]); @@ -21,7 +27,7 @@ const ParentTagsEditor = forwardRef(({ record, column, addTagLinks, deleteTagLin
{ +const SubTagsEditor = forwardRef(({ editingRowId, column, addTagLinks, deleteTagLinks, ...editorProps }, ref) => { const { tagsData } = useTags(); + const tag = useMemo(() => { + return getRowById(tagsData, editingRowId); + }, [tagsData, editingRowId]); + const subTags = useMemo(() => { - return record[column.key] || []; - }, [record, column]); + return getSubLinks(tag); + }, [tag]); const selectTag = useCallback((tagId, recordId) => { addTagLinks(column.key, recordId, [tagId]); @@ -21,7 +27,7 @@ const SubTagsEditor = forwardRef(({ record, column, addTagLinks, deleteTagLinks,
{ + return table.rows_tree || []; + }, [table]); + + const keyTreeNodeFoldedMap = useMemo(() => { + const strKeyTreeNodeFoldedMap = window.sfTagsDataContext.localStorage.getItem(LOCAL_KEY_TREE_NODE_FOLDED); + if (strKeyTreeNodeFoldedMap) { + try { + return JSON.parse(strKeyTreeNodeFoldedMap); + } catch { + return {}; + } + } + return {}; + }, []); + const gridScroll = useMemo(() => { const strScroll = window.sfTagsDataContext.localStorage.getItem(KEY_STORE_SCROLL); let scroll = null; @@ -95,6 +112,10 @@ const TagsTable = ({ const storeFoldedGroups = useCallback(() => {}, []); + const storeFoldedTreeNodes = useCallback((key, keyFoldedTreeNodesMap) => { + window.sfTagsDataContext.localStorage.setItem(key, JSON.stringify(keyFoldedTreeNodesMap)); + }, []); + const modifyColumnWidth = useCallback((column, newWidth) => { modifyColumnWidthAPI(column.key, newWidth); }, [modifyColumnWidthAPI]); @@ -130,8 +151,11 @@ const TagsTable = ({ <> { + const { isLoading } = useTags(); + if (isLoading) return (); + if (params.tagID === ALL_TAGS_ID) { return (); } diff --git a/frontend/src/tag/views/view.js b/frontend/src/tag/views/view.js index ad4528ec32..e8ac37ed9e 100644 --- a/frontend/src/tag/views/view.js +++ b/frontend/src/tag/views/view.js @@ -1,17 +1,15 @@ import React, { useCallback } from 'react'; -import { CenteredLoading } from '@seafile/sf-metadata-ui-component'; import { useTagView } from '../hooks'; import TagFiles from './tag-files'; const View = () => { - const { isLoading, errorMessage, tagFiles } = useTagView(); + const { errorMessage, tagFiles } = useTagView(); const renderTagView = useCallback(() => { if (!tagFiles) return null; return (); }, [tagFiles]); - if (isLoading) return (); return (