UNPKG

@itwin/presentation-components

Version:

React components based on iTwin.js Presentation library

305 lines 14.5 kB
/*--------------------------------------------------------------------------------------------- * 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