@itwin/presentation-components
Version:
React components based on iTwin.js Presentation library
305 lines • 14.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-deprecated */
/** @packageDocumentation
* @module Tree
*/
import "../common/DisposePolyfill.js";
import { PropertyFilterRuleGroupOperator } from "@itwin/components-react";
import { Logger } from "@itwin/core-bentley";
import { NodeKey, PresentationError, PresentationStatus, } from "@itwin/presentation-common";
import { Presentation } from "@itwin/presentation-frontend";
import { createDiagnosticsOptions } from "../common/Diagnostics.js";
import { getRulesetId, memoize, translate } from "../common/Utils.js";
import { PresentationComponentsLoggerCategory } from "../ComponentsLoggerCategory.js";
import { createInstanceFilterDefinition } from "../instance-filter-builder/PresentationFilterBuilder.js";
import { InfoTreeNodeItemType, isPresentationTreeNodeItem } from "./PresentationTreeNodeItem.js";
import { createInfoNode, createTreeNodeItem, pageOptionsUiToPresentation } from "./Utils.js";
/**
* Presentation Rules-driven tree data provider.
* @public
* @deprecated in 5.7. All tree-related APIs have been deprecated in favor of the new generation hierarchy
* building APIs (see https://github.com/iTwin/presentation/blob/33e79ee8d77f30580a9bab81a72884bda008db25/README.md#the-packages).
*/
export class PresentationTreeDataProvider {
_unregisterVariablesChangeListener;
_dataSource;
_diagnosticsOptions;
_onHierarchyLimitExceeded;
_props;
hierarchyLevelSizeLimit;
/** Constructor. */
constructor(props) {
this._props = { ...props };
this._dataSource = {
getNodesIterator: async (requestOptions) => {
// we can't just drop support for the `getNodesAndCount` override, so if it's set - need to take data from it
if (props.dataSourceOverrides?.getNodesAndCount) {
return createNodesIteratorFromDeprecatedResponse(await props.dataSourceOverrides.getNodesAndCount(requestOptions));
}
// the `PresentationManager.getNodesIterator` has only been added to @itwin/presentation-frontend in 4.5.1, and our peerDependency is
// set to 4.0.0, so we need to check if the method is really there
if (Presentation.presentation.getNodesIterator) {
return Presentation.presentation.getNodesIterator(requestOptions);
}
return createNodesIteratorFromDeprecatedResponse(await Presentation.presentation.getNodesAndCount(requestOptions));
},
getFilteredNodePaths: async (requestOptions) => Presentation.presentation.getFilteredNodePaths(requestOptions),
...props.dataSourceOverrides,
};
this._diagnosticsOptions = createDiagnosticsOptions(props);
this.hierarchyLevelSizeLimit = props.hierarchyLevelSizeLimit;
this._onHierarchyLimitExceeded = props.onHierarchyLimitExceeded;
}
#dispose() {
this._unregisterVariablesChangeListener?.();
this._unregisterVariablesChangeListener = undefined;
}
/** Destructor. Must be called to clean up. */
[Symbol.dispose]() {
this.#dispose();
}
/** @deprecated in 5.7. Use `[Symbol.dispose]` instead. */
/* c8 ignore next 3 */
dispose() {
this.#dispose();
}
get props() {
return this._props;
}
/** Id of the ruleset used by this data provider */
get rulesetId() {
return getRulesetId(this.props.ruleset);
}
/** [IModelConnection]($core-frontend) used by this data provider */
get imodel() {
return this.props.imodel;
}
/**
* Paging options for obtaining nodes.
* @see `PresentationTreeDataProviderProps.pagingSize`
*/
get pagingSize() {
return this.props.pagingSize;
}
set pagingSize(value) {
this._props.pagingSize = value;
}
/** Called to get base options for requests */
createBaseRequestOptions() {
return {
imodel: this.props.imodel,
rulesetOrId: this.props.ruleset,
...(this._diagnosticsOptions ? { diagnostics: this._diagnosticsOptions } : undefined),
};
}
/** Called to get options for node requests */
createPagedRequestOptions(parentKey, pageOptions, instanceFilter) {
const isPaging = pageOptions && (pageOptions.start || pageOptions.size !== undefined);
return {
...this.createRequestOptions(parentKey, instanceFilter),
...(isPaging ? { paging: pageOptionsUiToPresentation(pageOptions) } : undefined),
};
}
/** Creates options for nodes requests. */
createRequestOptions(parentKey, instanceFilter) {
const isHierarchyLevelLimitingSupported = !!this.hierarchyLevelSizeLimit && parentKey;
return {
...this.createBaseRequestOptions(),
...(parentKey ? { parentKey } : undefined),
...(isHierarchyLevelLimitingSupported ? { sizeLimit: this.hierarchyLevelSizeLimit } : undefined),
...(instanceFilter ? { instanceFilter } : undefined),
};
}
/**
* Returns a [NodeKey]($presentation-common) from given [TreeNodeItem]($components-react).
*
* **Warning**: Returns invalid [NodeKey]($presentation-common) if `node` is not a [[PresentationTreeNodeItem]].
*
* @deprecated in 4.0. Use [[isPresentationTreeNodeItem]] and [[PresentationTreeNodeItem.key]] to get [NodeKey]($presentation-common).
*/
getNodeKey(node) {
const invalidKey = { type: "", pathFromRoot: [], version: 0 };
return isPresentationTreeNodeItem(node) ? node.key : invalidKey;
}
/**
* Returns nodes
* @param parentNode The parent node to return children for.
* @param pageOptions Information about the requested page of data.
*/
async getNodes(parentNode, pageOptions) {
if (undefined !== pageOptions && pageOptions.size !== this.pagingSize) {
const msg = `PresentationTreeDataProvider.pagingSize doesn't match pageOptions in PresentationTreeDataProvider.getNodes call.
Make sure you set PresentationTreeDataProvider.pagingSize to avoid excessive backend requests.`;
Logger.logWarning(PresentationComponentsLoggerCategory.Hierarchy, msg);
}
const instanceFilter = await getFilterDefinition(this.imodel, parentNode);
return (await this._getNodesAndCount(parentNode, pageOptions, instanceFilter)).nodes;
}
/**
* Returns the total number of nodes
* @param parentNode The parent node to return children count for.
*/
async getNodesCount(parentNode) {
const instanceFilter = await getFilterDefinition(this.imodel, parentNode);
return (await this._getNodesAndCount(parentNode, { start: 0, size: this.pagingSize }, instanceFilter)).count;
}
_getNodesAndCount = memoize(async (parentNode, pageOptions, instanceFilter) => {
this.setupRulesetVariablesListener();
const parentKey = parentNode && isPresentationTreeNodeItem(parentNode) ? parentNode.key : undefined;
const requestOptions = this.createPagedRequestOptions(parentKey, pageOptions, instanceFilter);
return createNodesAndCountResult(async () => this._dataSource.getNodesIterator(requestOptions), this.createBaseRequestOptions(), (node, parentId) => createTreeNodeItem(node, parentId, this.props), parentNode, this.hierarchyLevelSizeLimit, this._onHierarchyLimitExceeded);
},
// eslint-disable-next-line @typescript-eslint/unbound-method
{ isMatchingKey: MemoizationHelpers.areNodesRequestsEqual });
/**
* Returns filtered node paths.
* @param filter Filter.
*/
async getFilteredNodePaths(filter) {
return this._dataSource.getFilteredNodePaths({
...this.createBaseRequestOptions(),
filterText: filter,
});
}
setupRulesetVariablesListener() {
if (this._unregisterVariablesChangeListener) {
return;
}
this._unregisterVariablesChangeListener = Presentation.presentation.vars(getRulesetId(this.props.ruleset)).onVariableChanged.addListener(() => {
this._getNodesAndCount.cache.values.length = 0;
this._getNodesAndCount.cache.keys.length = 0;
});
}
}
async function getFilterDefinition(imodel, node) {
if (!node || !isPresentationTreeNodeItem(node) || !node.filtering) {
return undefined;
}
// combine ancestors and current filters
const allFilters = [...node.filtering.ancestorFilters, ...(node.filtering.active ? [node.filtering.active] : [])];
if (allFilters.length === 0) {
return undefined;
}
if (allFilters.length === 1) {
return createInstanceFilterDefinition(allFilters[0], imodel);
}
const appliedFilters = allFilters.map((filterInfo) => filterInfo.filter).filter((filter) => filter !== undefined);
const usedClasses = getConcatenatedDistinctClassInfos(allFilters);
// if there are more than one filter applied, combine them using `AND` operator
// otherwise apply filter directly
const info = {
filter: appliedFilters.length > 0
? {
operator: PropertyFilterRuleGroupOperator.And,
conditions: appliedFilters,
}
: undefined,
usedClasses,
};
return createInstanceFilterDefinition(info, imodel);
}
function getConcatenatedDistinctClassInfos(appliedFilters) {
const concatenatedClassInfos = appliedFilters.reduce((accumulator, value) => [...accumulator, ...value.usedClasses], []);
return [...new Map(concatenatedClassInfos.map((item) => [item.id, item])).values()];
}
async function createNodesAndCountResult(resultFactory, baseOptions, treeItemFactory, parentNode, hierarchyLevelSizeLimit, onHierarchyLimitExceeded) {
try {
const result = await resultFactory();
const { items, total: count } = result;
const isParentFiltered = parentNode && isPresentationTreeNodeItem(parentNode) && parentNode.filtering?.active;
if (count === 0 && isParentFiltered) {
return createStatusNodeResult(parentNode, "tree.no-filtered-children", InfoTreeNodeItemType.NoChildren);
}
return { nodes: await createTreeItems(items, baseOptions, treeItemFactory, parentNode), count };
}
catch (e) {
if (e instanceof Error) {
if (hasErrorNumber(e)) {
switch (e.errorNumber) {
case PresentationStatus.Canceled:
return { nodes: [], count: 0 };
case PresentationStatus.BackendTimeout:
return createStatusNodeResult(parentNode, "tree.timeout", InfoTreeNodeItemType.BackendTimeout);
case PresentationStatus.ResultSetTooLarge:
// ResultSetTooLarge error can't occur if hierarchyLevelSizeLimit is undefined.
onHierarchyLimitExceeded?.();
return {
nodes: [
createInfoNode(parentNode, `${translate("tree.result-limit-exceeded")} ${hierarchyLevelSizeLimit}.`, InfoTreeNodeItemType.ResultSetTooLarge),
],
count: 1,
};
}
}
// eslint-disable-next-line no-console
console.error(`Error creating nodes: ${e.toString()}`);
}
return createStatusNodeResult(parentNode, "tree.unknown-error");
}
}
function createStatusNodeResult(parentNode, labelKey, type) {
return {
nodes: [createInfoNode(parentNode, translate(labelKey), type)],
count: 1,
};
}
async function createTreeItems(nodes, baseOptions, treeItemFactory, parentNode) {
const items = [];
// collect filters for child elements. These filter will be applied for grouping nodes
// if current node has `ancestorFilters` it means it is grouping node and those filter should be forwarded to child grouping nodes alongside current node filter.
// if current node does not have `ancestorFilters` it means it is an instance node and only it's filter should be applied to child grouping nodes.
const ancestorFilters = parentNode && isPresentationTreeNodeItem(parentNode) && parentNode.filtering
? [...parentNode.filtering.ancestorFilters, ...(parentNode.filtering.active ? [parentNode.filtering.active] : [])]
: [];
for await (const node of nodes) {
const item = treeItemFactory(node, parentNode?.id);
if (node.supportsFiltering) {
item.filtering = {
descriptor: async () => {
const descriptor = await Presentation.presentation.getNodesDescriptor({ ...baseOptions, parentKey: node.key });
if (!descriptor) {
throw new PresentationError(PresentationStatus.Error, `Failed to get descriptor for node - ${node.label.displayValue}`);
}
return descriptor;
},
ancestorFilters: NodeKey.isGroupingNodeKey(item.key) ? ancestorFilters : [],
};
}
items.push(item);
}
return items;
}
class MemoizationHelpers {
static areNodesRequestsEqual(lhsArgs, rhsArgs) {
if (lhsArgs[0]?.id !== rhsArgs[0]?.id) {
return false;
}
if ((lhsArgs[1]?.start ?? 0) !== (rhsArgs[1]?.start ?? 0)) {
return false;
}
if ((lhsArgs[1]?.size ?? 0) !== (rhsArgs[1]?.size ?? 0)) {
return false;
}
if (lhsArgs[2]?.expression !== rhsArgs[2]?.expression) {
return false;
}
return true;
}
}
function createNodesIteratorFromDeprecatedResponse({ count, nodes }) {
return {
total: count,
items: (async function* () {
for (const node of nodes) {
yield node;
}
})(),
};
}
function hasErrorNumber(e) {
return "errorNumber" in e && e.errorNumber !== undefined;
}
//# sourceMappingURL=DataProvider.js.map