1
0
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:
Jerry Ren
2025-01-15 16:47:12 +08:00
committed by GitHub
parent 5574252dca
commit 7fc2f193e6
48 changed files with 1571 additions and 167 deletions

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

View File

@@ -71,6 +71,7 @@ class PopupEditorContainer extends React.Component {
editorContainer: document.body,
modifyColumnData,
editorPosition,
editingRowId: this.editingRowId,
record,
height,
columns,

View File

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

View File

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

View File

@@ -85,6 +85,7 @@
}
.sf-table-column-content {
position: relative;
height: 100%;
width: 100%;
text-align: left;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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;
}

View File

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

View File

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

View 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,
};

View 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] : '';
};