@itwin/presentation-hierarchies-react
Version:
React components based on `@itwin/presentation-hierarchies`
307 lines • 12.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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