UNPKG

@itwin/presentation-hierarchies-react

Version:

React components based on `@itwin/presentation-hierarchies`

307 lines 12.9 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import "./DisposePolyfill.js"; import { enableMapSet, produce } from "immer"; import { buffer, debounceTime, EMPTY, groupBy, map, mergeMap, reduce, Subject, switchMap, takeUntil, tap } from "rxjs"; import { HierarchyNode } from "@itwin/presentation-hierarchies"; import { TreeLoader } from "./TreeLoader.js"; import { isTreeModelHierarchyNode, isTreeModelInfoNode, TreeModel } from "./TreeModel.js"; import { createNodeId, sameNodes } from "./Utils.js"; enableMapSet(); /** @internal */ export class TreeActions { _onModelChanged; _onLoad; _onHierarchyLimitExceeded; _onHierarchyLoadError; _loader; _nodeIdFactory; _currentModel; _reset = new Subject(); _nodeLoader; constructor(_onModelChanged, _onLoad, _onHierarchyLimitExceeded, _onHierarchyLoadError, nodeIdFactory, seed) { this._onModelChanged = _onModelChanged; this._onLoad = _onLoad; this._onHierarchyLimitExceeded = _onHierarchyLimitExceeded; this._onHierarchyLoadError = _onHierarchyLoadError; this._loader = new NoopTreeLoader(); this._nodeIdFactory = nodeIdFactory ?? createNodeId; this._currentModel = seed ?? /* c8 ignore next */ { idToNode: new Map(), parentChildMap: new Map(), rootNode: { id: undefined, nodeData: undefined }, }; this._nodeLoader = this.createNodeLoader(); } createNodeLoader() { const subject = new Subject(); subject .pipe(groupBy((item) => item.parentId), mergeMap((group) => group.pipe(buffer(group.pipe(debounceTime(0))), map((groupArr) => { let returnIndex = groupArr.length - 1; groupArr.forEach((member, index) => { if (member.discardState) { returnIndex = index; } }); groupArr.forEach((member, index) => { if (index === returnIndex) { return; } member.timeTracker?.[Symbol.dispose]; member.onComplete(); }); return groupArr[returnIndex]; }), switchMap((props) => this._loader.loadNodes(props.loadOptions).pipe(collectTreePartsUntil(this._reset, props.initialRootNode), tap({ next: (newModel) => { const childNodes = newModel.parentChildMap.get(props.parentId); const firstChildNode = childNodes?.length ? newModel.idToNode.get(childNodes[0]) : undefined; this.handleLoadedHierarchy(props.parentId, newModel); // only report load duration if no error occurs if (!(firstChildNode && isTreeModelInfoNode(firstChildNode))) { props.timeTracker?.finish(); } }, complete: () => { this.onLoadingComplete(props.parentId); props.timeTracker?.[Symbol.dispose](); props.onComplete(); }, })))))) .subscribe(); return subject; } updateTreeModel(updater) { const newModel = produce(this._currentModel, updater); if (this._currentModel === newModel) { return; } this._currentModel = newModel; this._onModelChanged(this._currentModel); } handleLoadedHierarchy(parentId, loadedHierarchy) { this.updateTreeModel((model) => { TreeModel.addHierarchyPart(model, parentId, loadedHierarchy); }); } onLoadingComplete(parentId) { this.updateTreeModel((model) => { TreeModel.setIsLoading(model, parentId, false); }); } getLoadAction(parentId) { return this._currentModel.idToNode.size === 0 ? "initial-load" : parentId === undefined ? "reload" : "hierarchy-level-load"; } loadSubTree(options, initialRootNode, discardState) { const loadAction = this.getLoadAction(options.parent.id); const timeTracker = new TimeTracker((time) => this._onLoad(loadAction, time)); return { complete: new Promise((resolve) => { this._nodeLoader.next({ loadOptions: options, onComplete: resolve, timeTracker, parentId: options.parent.id, initialRootNode, discardState }); }), }; } loadNodes(parentId, ignoreCache) { const parentNode = this._currentModel.idToNode.get(parentId); /* c8 ignore next 3 */ if (!parentNode || !isTreeModelHierarchyNode(parentNode)) { return { complete: Promise.resolve() }; } return this.loadSubTree({ parent: parentNode, getHierarchyLevelOptions: (node) => createHierarchyLevelOptions(this._currentModel, getNonGroupedParentId(node, this._nodeIdFactory)), shouldLoadChildren: (node) => !!node.nodeData.autoExpand, ignoreCache, }); } reloadSubTree(parentId, oldModel, options) { const currModel = this._currentModel; const expandedNodes = !!options?.discardState ? [] : collectNodes(parentId, oldModel, (node) => node.isExpanded === true); const collapsedNodes = !!options?.discardState ? [] : collectNodes(parentId, oldModel, (node) => node.isExpanded === false); const getHierarchyLevelOptions = (node) => { if (!!options?.discardState) { return { instanceFilter: undefined, hierarchyLevelSizeLimit: undefined }; } const filteredNodeId = getNonGroupedParentId(node, this._nodeIdFactory); return createHierarchyLevelOptions(filteredNodeId === parentId ? currModel : oldModel, filteredNodeId); }; const shouldLoadChildren = (node) => { if (expandedNodes.findIndex((expandedNode) => sameNodes(expandedNode.nodeData, node.nodeData)) !== -1) { return true; } if (collapsedNodes.findIndex((collapsedNode) => sameNodes(collapsedNode.nodeData, node.nodeData)) !== -1) { return false; } return !!node.nodeData.autoExpand; }; const buildNode = (node) => (!!options?.discardState || node.id === parentId ? node : addAttributes(node, oldModel)); const rootNode = parentId !== undefined ? this.getNode(parentId) : currModel.rootNode; /* c8 ignore next 3 */ if (!rootNode || isTreeModelInfoNode(rootNode)) { return { complete: Promise.resolve() }; } if (parentId === undefined) { // cancel all ongoing requests this._reset.next(); } return this.loadSubTree({ parent: rootNode, getHierarchyLevelOptions, shouldLoadChildren, buildNode, ignoreCache: options?.ignoreCache }, !!options?.discardState ? undefined : { ...currModel.rootNode }, options?.discardState); } reset() { this._reset.next(); } setHierarchyProvider(provider) { this._loader = provider ? new TreeLoader(provider, this._onHierarchyLimitExceeded, ({ parentId, type, error }) => { if (type === "timeout") { const loadAction = this.getLoadAction(parentId); this._onLoad(loadAction, Number.MAX_SAFE_INTEGER); } this._onHierarchyLoadError({ parentId, type, error }); }, this._nodeIdFactory) : /* c8 ignore next */ new NoopTreeLoader(); } getNode(nodeId) { return TreeModel.getNode(this._currentModel, nodeId); } selectNodes(nodeIds, changeType) { this.updateTreeModel((model) => { TreeModel.selectNodes(model, nodeIds, changeType); }); } expandNode(nodeId, isExpanded) { let childrenAction = "none"; this.updateTreeModel((model) => { childrenAction = TreeModel.expandNode(model, nodeId, isExpanded); }); if (childrenAction === "none") { return { complete: Promise.resolve() }; } return this.loadNodes(nodeId, childrenAction === "reloadChildren"); } setHierarchyLimit(nodeId, limit) { const oldModel = this._currentModel; let loadChildren = false; this.updateTreeModel((model) => { loadChildren = TreeModel.setHierarchyLimit(model, nodeId, limit); }); if (!loadChildren) { return { complete: Promise.resolve() }; } return this.reloadSubTree(nodeId, oldModel); } setInstanceFilter(nodeId, filter) { const oldModel = this._currentModel; let loadChildren = false; this.updateTreeModel((model) => { loadChildren = TreeModel.setInstanceFilter(model, nodeId, filter); }); if (!loadChildren) { return { complete: Promise.resolve() }; } return this.reloadSubTree(nodeId, oldModel); } reloadTree(options) { const oldModel = this._currentModel; this.updateTreeModel((model) => { TreeModel.setIsLoading(model, options?.parentNodeId, true); if (options?.state === "reset") { TreeModel.removeSubTree(model, options?.parentNodeId); } }); const discardState = options?.state === "discard" || options?.state === "reset"; const ignoreCache = options?.state === "reset"; return this.reloadSubTree(options?.parentNodeId, oldModel, { discardState, ignoreCache }); } } function collectTreePartsUntil(untilNotifier, rootNode) { return (source) => source.pipe(reduce((treeModel, loadedPart) => { addNodesToModel(treeModel, loadedPart); return treeModel; }, { idToNode: new Map(), parentChildMap: new Map(), rootNode: rootNode ?? { id: undefined, nodeData: undefined }, }), takeUntil(untilNotifier)); } function addNodesToModel(model, hierarchyPart) { model.parentChildMap.set(hierarchyPart.parentId, hierarchyPart.loadedNodes.map((node) => node.id)); for (const node of hierarchyPart.loadedNodes) { model.idToNode.set(node.id, node); } const parentNode = hierarchyPart.parentId ? model.idToNode.get(hierarchyPart.parentId) : undefined; if (parentNode && isTreeModelHierarchyNode(parentNode)) { parentNode.isExpanded = true; } } function addAttributes(node, oldModel) { const oldNode = oldModel.idToNode.get(node.id); if (oldNode && isTreeModelHierarchyNode(oldNode)) { node.hierarchyLimit = oldNode.hierarchyLimit; node.instanceFilter = oldNode.instanceFilter; node.isSelected = oldNode.isSelected; } return node; } function collectNodes(parentId, model, pred) { const currentChildren = model.parentChildMap.get(parentId); if (!currentChildren) { return []; } if (parentId === undefined) { return currentChildren.flatMap((child) => collectNodes(child, model, pred)); } const currNode = model.idToNode.get(parentId); if (!currNode || !isTreeModelHierarchyNode(currNode) || !pred(currNode)) { return []; } return [currNode, ...currentChildren.flatMap((child) => collectNodes(child, model, pred))]; } function getNonGroupedParentId(node, nodeIdFactory) { if (!node.nodeData || !HierarchyNode.isGroupingNode(node.nodeData)) { return node.id; } if (!node.nodeData.nonGroupingAncestor) { return undefined; } return nodeIdFactory(node.nodeData.nonGroupingAncestor); } function createHierarchyLevelOptions(model, nodeId) { if (nodeId === undefined) { return { instanceFilter: model.rootNode.instanceFilter, hierarchyLevelSizeLimit: model.rootNode.hierarchyLimit }; } const modelNode = model.idToNode.get(nodeId); if (!modelNode || isTreeModelInfoNode(modelNode)) { return { instanceFilter: undefined, hierarchyLevelSizeLimit: undefined }; } return { instanceFilter: modelNode.instanceFilter, hierarchyLevelSizeLimit: modelNode.hierarchyLimit }; } /* c8 ignore start */ class NoopTreeLoader { loadNodes() { return EMPTY; } } /* c8 ignore end */ class TimeTracker { _onFinish; _start; _stopped = false; constructor(_onFinish) { this._onFinish = _onFinish; this._start = Date.now(); } [Symbol.dispose]() { this._stopped = true; } finish() { /* c8 ignore next 3 */ if (this._stopped) { return; } this._stopped = true; const elapsedTime = Date.now() - this._start; this._onFinish(elapsedTime); } } //# sourceMappingURL=TreeActions.js.map