mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-01 07:01:12 +00:00
feat(tag): support display tags with tree (#7365)
This commit is contained in:
12
frontend/src/components/sf-table/constants/tree.js
Normal file
12
frontend/src/components/sf-table/constants/tree.js
Normal file
@@ -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;
|
@@ -71,6 +71,7 @@ class PopupEditorContainer extends React.Component {
|
||||
editorContainer: document.body,
|
||||
modifyColumnData,
|
||||
editorPosition,
|
||||
editingRowId: this.editingRowId,
|
||||
record,
|
||||
height,
|
||||
columns,
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -85,6 +85,7 @@
|
||||
}
|
||||
|
||||
.sf-table-column-content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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 (
|
||||
<div className={classnames('sf-table-main-container container-fluid p-0', { [`group-level-${groupbysCount + 1}`]: groupbysCount > 0 })}>
|
||||
<div className={classnames('sf-table-main-container container-fluid p-0', { [`group-level-${groupbysCount + 1}`]: groupbysCount > 0, 'sf-table-tree': showRecordAsTree })}>
|
||||
{hasNoRecords && <EmptyTip text={noRecordsTipsText || gettext('No record')} />}
|
||||
{!hasNoRecords &&
|
||||
<Records
|
||||
@@ -94,8 +107,13 @@ const TableMain = ({
|
||||
sequenceColumnWidth={sequenceColumnWidth}
|
||||
hasMoreRecords={hasMoreRecords}
|
||||
showGridFooter={showGridFooter}
|
||||
showRecordAsTree={showRecordAsTree}
|
||||
recordsTree={recordsTree}
|
||||
treeNodesCount={treeNodesCount}
|
||||
treeNodeKeyRecordIdMap={treeNodeKeyRecordIdMap}
|
||||
scrollToLoadMore={loadMore}
|
||||
loadAll={loadAll}
|
||||
getTreeNodeByIndex={getTreeNodeByIndex}
|
||||
recordGetterById={recordGetterById}
|
||||
recordGetterByIndex={recordGetterByIndex}
|
||||
getClientCellValueDisplayString={getInternalClientCellValueDisplayString}
|
||||
@@ -116,6 +134,8 @@ TableMain.propTypes = {
|
||||
groupbys: PropTypes.array,
|
||||
groups: PropTypes.array,
|
||||
noRecordsTipsText: PropTypes.string,
|
||||
recordsTree: PropTypes.array,
|
||||
showRecordAsTree: PropTypes.bool,
|
||||
modifyRecords: PropTypes.func,
|
||||
loadMore: PropTypes.func,
|
||||
loadAll: PropTypes.func,
|
||||
|
@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
|
||||
import { Loading } from '@seafile/sf-metadata-ui-component';
|
||||
import toaster from '../../../toast';
|
||||
import LoadAllTip from '../load-all-tip';
|
||||
import RecordMetrics from '../../utils/record-metrics';
|
||||
import { RecordMetrics } from '../../utils/record-metrics';
|
||||
import { TreeMetrics } from '../../utils/tree-metrics';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import { CANVAS_RIGHT_INTERVAL } from '../../constants/grid';
|
||||
import { GRID_FOOTER as Z_INDEX_GRID_FOOTER } from '../../constants/z-index';
|
||||
@@ -108,9 +109,14 @@ class RecordsFooter extends React.Component {
|
||||
};
|
||||
|
||||
getRecord = () => {
|
||||
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,
|
||||
|
@@ -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 = (
|
||||
<div
|
||||
className={classnames('sf-table-cell column', { 'table-last--frozen': isLastFrozenCell, 'name-column': isNameColumn })}
|
||||
ref={headerCellRef}
|
||||
style={style}
|
||||
id={`sf-metadata-column-${key}`}
|
||||
onClick={() => handleHeaderCellClick(column, frozen)}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<div className="sf-table-column-content sf-table-header-cell-left d-flex align-items-center text-truncate">
|
||||
|
||||
const cellName = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<span className="mr-2" id={`header-icon-${key}`}>
|
||||
{icon_name && <Icon iconName={icon_name} className="sf-table-column-icon" />}
|
||||
</span>
|
||||
@@ -171,11 +167,42 @@ const Cell = ({
|
||||
<div className="header-name d-flex">
|
||||
<span title={display_name} className={classnames('header-name-text', { 'double': height === 56 })}>{display_name}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}, [icon_name, display_name, height, icon_tooltip, key]);
|
||||
|
||||
const cellContent = useMemo(() => {
|
||||
if (showRecordAsTree && isNameColumn) {
|
||||
return (
|
||||
<div className="sf-table-cell-tree-node">
|
||||
<span className="sf-table-record-tree-expand-icon" style={{ left: NODE_ICON_LEFT_INDENT }}></span>
|
||||
<div className="sf-table-cell-tree-node-content text-truncate"style={{ paddingLeft: NODE_CONTENT_LEFT_INDENT }}>
|
||||
{cellName}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return cellName;
|
||||
}, [cellName, isNameColumn, showRecordAsTree]);
|
||||
|
||||
const cell = useMemo(() => {
|
||||
return (
|
||||
<div
|
||||
className={classnames('sf-table-cell column', { 'table-last--frozen': isLastFrozenCell, 'name-column': isNameColumn })}
|
||||
ref={headerCellRef}
|
||||
style={style}
|
||||
id={`sf-metadata-column-${key}`}
|
||||
onClick={() => handleHeaderCellClick(column, frozen)}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<div className="sf-table-column-content sf-table-header-cell-left d-flex align-items-center text-truncate">
|
||||
{cellContent}
|
||||
</div>
|
||||
{isValidElement(ColumnDropdownMenu) && <HeaderDropdownMenu ColumnDropdownMenu={ColumnDropdownMenu} column={column} setDisableDragColumn={setDisableDragColumn} />}
|
||||
{resizable && <ResizeColumnHandle onDrag={onDraggingColumnWidth} onDragEnd={handleDragEndColumnWidth} />}
|
||||
</div>
|
||||
{isValidElement(ColumnDropdownMenu) && <HeaderDropdownMenu ColumnDropdownMenu={ColumnDropdownMenu} column={column} setDisableDragColumn={setDisableDragColumn} />}
|
||||
{resizable && <ResizeColumnHandle onDrag={onDraggingColumnWidth} onDragEnd={handleDragEndColumnWidth} />}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}, [ColumnDropdownMenu, cellContent, key, column, style, frozen, resizable, isLastFrozenCell, isNameColumn, handleDragEndColumnWidth, handleHeaderCellClick, onContextMenu, onDraggingColumnWidth]);
|
||||
|
||||
if (!moveable || isNameColumn) {
|
||||
return (
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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 (
|
||||
<TreeBody
|
||||
onRef={ref => this.bodyRef = ref}
|
||||
{...commonProps}
|
||||
recordsTree={this.props.recordsTree}
|
||||
treeMetrics={this.state.treeMetrics}
|
||||
storeFoldedTreeNodes={this.props.storeFoldedTreeNodes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (this.props.isGroupView) {
|
||||
return (
|
||||
<GroupBody
|
||||
@@ -690,13 +893,13 @@ class Records extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
recordIds, recordsCount, showSequenceColumn, sequenceColumnWidth, isGroupView, groupOffsetLeft,
|
||||
recordsCount, showSequenceColumn, sequenceColumnWidth, isGroupView, groupOffsetLeft,
|
||||
} = this.props;
|
||||
const { recordMetrics, columnMetrics, selectedRange, colOverScanStartIdx, colOverScanEndIdx } = this.state;
|
||||
const { columns, totalWidth, lastFrozenColumnKey } = columnMetrics;
|
||||
const containerWidth = this.getContainerWidth();
|
||||
const hasSelectedRecord = this.checkHasSelectedRecord();
|
||||
const isSelectedAll = showSequenceColumn && RecordMetrics.isSelectedAll(recordIds, recordMetrics);
|
||||
const isSelectedAll = this.checkIsSelectAll();
|
||||
|
||||
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,
|
||||
|
@@ -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 && <div className="sf-table-column-content row-index text-truncate">{index + 1}</div>}
|
||||
{!isSelected && <div className="sf-table-column-content row-index text-truncate">{this.getRecordNo()}</div>}
|
||||
<div className="sf-table-column-content actions-checkbox">
|
||||
<div className="select-cell-checkbox-container" onClick={this.props.onSelectRecord}>
|
||||
<input
|
||||
|
@@ -4,7 +4,8 @@ import classnames from 'classnames';
|
||||
import { Utils } from '../../../../../../utils/utils';
|
||||
import { getCellValueByColumn } from '../../../../utils/cell';
|
||||
import { cellCompare, checkCellValueChanged } from '../../../../utils/cell-comparer';
|
||||
import { checkIsColumnEditable } from '../../../../utils/column';
|
||||
import { checkIsColumnEditable, checkIsNameColumn } from '../../../../utils/column';
|
||||
import { NODE_CONTENT_LEFT_INDENT, NODE_ICON_LEFT_INDENT } from '../../../../constants/tree';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -22,12 +23,21 @@ const Cell = React.memo(({
|
||||
bgColor,
|
||||
frozen,
|
||||
height,
|
||||
showRecordAsTree,
|
||||
nodeDepth,
|
||||
hasSubNodes,
|
||||
isFoldedNode,
|
||||
checkCanModifyRecord,
|
||||
toggleExpandNode,
|
||||
}) => {
|
||||
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 (
|
||||
<div className="sf-table-cell-tree-node">
|
||||
{hasSubNodes && <span className="sf-table-record-tree-expand-icon" style={{ left: nodeDepth * NODE_ICON_LEFT_INDENT }} onClick={toggleExpandNode}><i className={classnames('sf3-font sf3-font-down', { 'rotate-270': isFoldedNode })}></i></span>}
|
||||
<div className="sf-table-cell-tree-node-content" style={{ paddingLeft: NODE_CONTENT_LEFT_INDENT + nodeDepth * NODE_ICON_LEFT_INDENT }}>
|
||||
{columnFormatter}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return columnFormatter;
|
||||
}, [isNameColumn, column, isCellSelected, cellValue, record, showRecordAsTree, nodeDepth, hasSubNodes, isFoldedNode, modifyRecord, toggleExpandNode]);
|
||||
|
||||
return (
|
||||
<div key={`${record._id}-${column.key}`} {...containerProps}>
|
||||
{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 })}
|
||||
</div>
|
||||
);
|
||||
}, (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;
|
||||
|
@@ -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;
|
||||
|
703
frontend/src/components/sf-table/table-main/records/tree-body.js
Normal file
703
frontend/src/components/sf-table/table-main/records/tree-body.js
Normal file
@@ -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 (
|
||||
<Record
|
||||
showRecordAsTree
|
||||
key={`sf-table-tree-node-${node_key}`}
|
||||
ref={ref => {
|
||||
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 = <div key="upper-placeholder" className="d-flex align-items-end" style={style}><Loading /></div>;
|
||||
shownNodes.unshift(upperRow);
|
||||
}
|
||||
|
||||
// add bottom placeholder
|
||||
if (belowHeight > 0) {
|
||||
const style = { height: belowHeight, width: '100%' };
|
||||
const belowRow = <div key="below-placeholder" style={style}><Loading /></div>;
|
||||
shownNodes.push(belowRow);
|
||||
}
|
||||
return shownNodes;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id="canvas"
|
||||
className="sf-table-canvas"
|
||||
ref={this.setResultContentRef}
|
||||
onScroll={this.onScroll}
|
||||
onKeyDown={this.props.onGridKeyDown}
|
||||
onKeyUp={this.props.onGridKeyUp}
|
||||
>
|
||||
<InteractionMasks
|
||||
{...this.props}
|
||||
showRecordAsTree
|
||||
ref={this.setInteractionMaskRef}
|
||||
recordsCount={this.state.nodes.length}
|
||||
treeNodeKeyRecordIdMap={this.props.treeNodeKeyRecordIdMap}
|
||||
treeMetrics={this.props.treeMetrics}
|
||||
rowHeight={this.getRowHeight()}
|
||||
getRowTop={this.getRowTop}
|
||||
scrollTop={this.oldScrollTop}
|
||||
selectNone={this.selectNone}
|
||||
getVisibleIndex={this.getVisibleIndex}
|
||||
onHitBottomBoundary={this.onHitBottomCanvas}
|
||||
onHitTopBoundary={this.onHitTopCanvas}
|
||||
onCellClick={this.onCellClick}
|
||||
scrollToColumn={this.scrollToColumn}
|
||||
/>
|
||||
<div className="sf-table-records-wrapper" style={{ width: this.props.totalWidth + this.props.sequenceColumnWidth }} ref={this.setResultRef}>
|
||||
{this.renderRecords()}
|
||||
</div>
|
||||
</div>
|
||||
<RightScrollbar
|
||||
ref={this.setRightScrollbar}
|
||||
getClientHeight={this.getCanvasClientHeight}
|
||||
getScrollHeight={this.getRecordsWrapperScrollHeight}
|
||||
onScrollbarScroll={this.onScrollbarScroll}
|
||||
onScrollbarMouseUp={this.onScrollbarMouseUp}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
44
frontend/src/components/sf-table/tree.css
Normal file
44
frontend/src/components/sf-table/tree.css
Normal file
@@ -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;
|
||||
}
|
@@ -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
|
||||
);
|
||||
|
@@ -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;
|
||||
|
62
frontend/src/components/sf-table/utils/tree-metrics.js
Normal file
62
frontend/src/components/sf-table/utils/tree-metrics.js
Normal file
@@ -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,
|
||||
};
|
76
frontend/src/components/sf-table/utils/tree.js
Normal file
76
frontend/src/components/sf-table/utils/tree.js
Normal file
@@ -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] : '';
|
||||
};
|
Reference in New Issue
Block a user