UNPKG

@itwin/presentation-components

Version:

React components based on iTwin.js Presentation library

311 lines 12.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Core */ import "./DisposePolyfill.js"; import { Logger } from "@itwin/core-bentley"; import { IModelApp } from "@itwin/core-frontend"; import { Content, DEFAULT_KEYS_BATCH_SIZE, Descriptor, KeySet, } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { PresentationComponentsLoggerCategory } from "../ComponentsLoggerCategory.js"; import { createDiagnosticsOptions } from "./Diagnostics.js"; import { findField, getRulesetId, memoize } from "./Utils.js"; /** @public */ export var CacheInvalidationProps; (function (CacheInvalidationProps) { /** * Create CacheInvalidationProps to fully invalidate all caches. */ CacheInvalidationProps.full = () => ({ descriptor: true, descriptorConfiguration: true, size: true, content: true }); })(CacheInvalidationProps || (CacheInvalidationProps = {})); /** * Base class for all presentation-driven content providers. * @public */ export class ContentDataProvider { _imodel; _ruleset; _displayType; _keys; _previousKeysGuid; _selectionInfo; _pagingSize; _diagnosticsOptions; _listeners = []; /** Constructor. */ constructor(props) { this._displayType = props.displayType; this._imodel = props.imodel; this._ruleset = props.ruleset; this._keys = new KeySet(); this._previousKeysGuid = this._keys.guid; this._pagingSize = props.pagingSize; this._diagnosticsOptions = createDiagnosticsOptions(props); } /** Destructor. Must be called to clean up. */ [Symbol.dispose]() { for (const removeListener of this._listeners) { removeListener(); } this._listeners = []; } /** @deprecated in 5.7. Use `[Symbol.dispose]` instead. */ /* c8 ignore next 3 */ dispose() { this[Symbol.dispose](); } /** Display type used to format content */ get displayType() { return this._displayType; } /** * Paging options for obtaining content. * @see `ContentDataProviderProps.pagingSize` */ get pagingSize() { return this._pagingSize; } set pagingSize(value) { this._pagingSize = value; } /** IModel to pull data from */ get imodel() { return this._imodel; } set imodel(imodel) { if (this._imodel === imodel) { return; } this._imodel = imodel; this.invalidateCache(CacheInvalidationProps.full()); } /** Id of the ruleset to use when requesting content */ get rulesetId() { return getRulesetId(this._ruleset); } set rulesetId(value) { if (this.rulesetId === value) { return; } this._ruleset = value; this.invalidateCache(CacheInvalidationProps.full()); } /** Keys defining what to request content for */ get keys() { return this._keys; } set keys(keys) { if (keys.guid === this._previousKeysGuid) { return; } this._keys = keys; this._previousKeysGuid = this._keys.guid; this.invalidateCache(CacheInvalidationProps.full()); } /** Information about selection event that results in content change */ get selectionInfo() { return this._selectionInfo; } set selectionInfo(info) { if (this._selectionInfo === info) { return; } this._selectionInfo = info; this.invalidateCache(CacheInvalidationProps.full()); } /** * Invalidates cached content. */ invalidateCache(props) { if (props.descriptor && this.getDefaultContentDescriptor) { this.getDefaultContentDescriptor.cache.keys.length = 0; this.getDefaultContentDescriptor.cache.values.length = 0; } if (props.descriptorConfiguration && this.getContentDescriptor) { this.getContentDescriptor.cache.keys.length = 0; this.getContentDescriptor.cache.values.length = 0; } if ((props.content || props.size) && this._getContentAndSize) { this._getContentAndSize.cache.keys.length = 0; this._getContentAndSize.cache.values.length = 0; } } createRequestOptions() { return { imodel: this._imodel, rulesetOrId: this._ruleset, ...(this._diagnosticsOptions ? { diagnostics: this._diagnosticsOptions } : undefined), }; } setupListeners() { if (this._listeners.length > 0) { return; } this._listeners.push(Presentation.presentation.onIModelContentChanged.addListener(this.onIModelContentChanged)); this._listeners.push(Presentation.presentation.rulesets().onRulesetModified.addListener(this.onRulesetModified)); this._listeners.push(Presentation.presentation.vars(getRulesetId(this._ruleset)).onVariableChanged.addListener(this.onRulesetVariableChanged)); this._listeners.push(IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener(this.onUnitSystemChanged)); } /** * Called to check if content should be requested even when `keys` is empty. If this * method returns `false`, then content is not requested and this saves a trip * to the backend. */ shouldRequestContentForEmptyKeyset() { return false; } /** * Get the content descriptor overrides. * * The method may be overriden to configure the content based on content descriptor. If necessary, * it may use [[getContentDescriptor]] to get the descriptor first. */ async getDescriptorOverrides() { return { displayType: this.displayType }; } getDefaultContentDescriptor = memoize(async () => { this.setupListeners(); /* c8 ignore next 5 */ if (this.keys.size > DEFAULT_KEYS_BATCH_SIZE) { const msg = `ContentDataProvider.getContentDescriptor requesting descriptor with ${this.keys.size} keys which exceeds the suggested size of ${DEFAULT_KEYS_BATCH_SIZE}. Possible "HTTP 413 Payload Too Large" error.`; Logger.logWarning(PresentationComponentsLoggerCategory.Content, msg); } return Presentation.presentation.getContentDescriptor({ ...this.createRequestOptions(), displayType: this._displayType, keys: this.keys, selection: this.selectionInfo, }); }); /** * Get the content descriptor. * * The method may return `undefined ` descriptor if: * - [[shouldRequestContentForEmptyKeyset]] returns `false` and `this.keys` is empty * - there is no content based on the ruleset and input */ getContentDescriptor = memoize(async () => { if (!this.shouldRequestContentForEmptyKeyset() && this.keys.isEmpty) { return undefined; } const descriptor = await this.getDefaultContentDescriptor(); if (!descriptor) { return undefined; } return new Descriptor({ ...descriptor }); }); /** * Get the number of content records. */ async getContentSetSize() { const paging = undefined !== this.pagingSize ? { start: 0, size: this.pagingSize } : undefined; const contentAndSize = await this._getContentAndSize(paging); return contentAndSize?.size ?? 0; } /** * Get the content. * @param pageOptions Paging options. */ async getContent(pageOptions) { if (undefined !== pageOptions && pageOptions.size !== this.pagingSize) { const msg = `ContentDataProvider.pagingSize doesn't match pageOptions in ContentDataProvider.getContent call. Make sure you set provider's pagingSize to avoid excessive backend requests.`; Logger.logWarning(PresentationComponentsLoggerCategory.Content, msg); } const contentAndSize = await this._getContentAndSize(pageOptions); return contentAndSize?.content; } /** * Get field using PropertyRecord. * @deprecated in 4.0. Use [[getFieldByPropertyDescription]] instead. */ async getFieldByPropertyRecord(propertyRecord) { return this.getFieldByPropertyDescription(propertyRecord.property); } /** Get field that was used to create a property record with given property description. */ async getFieldByPropertyDescription(descr) { const descriptor = await this.getContentDescriptor(); return descriptor ? findField(descriptor, descr.name) : undefined; } _getContentAndSize = memoize(async (pageOptions) => { if (!this.shouldRequestContentForEmptyKeyset() && this.keys.isEmpty) { return undefined; } this.setupListeners(); const descriptorOverrides = await this.getDescriptorOverrides(); /* c8 ignore next 5 */ if (this.keys.size > DEFAULT_KEYS_BATCH_SIZE) { const msg = `ContentDataProvider.getContent requesting with ${this.keys.size} keys which exceeds the suggested size of ${DEFAULT_KEYS_BATCH_SIZE}. Possible "HTTP 413 Payload Too Large" error.`; Logger.logWarning(PresentationComponentsLoggerCategory.Content, msg); } const options = { ...this.createRequestOptions(), descriptor: descriptorOverrides, keys: this.keys, paging: pageOptions, }; if (Presentation.presentation.getContentIterator) { const result = await Presentation.presentation.getContentIterator(options); return result ? { size: result.total, content: new Content(result.descriptor, await (async () => { const items = []; for await (const item of result.items) { items.push(item); } return items; })()), } : undefined; } const requestSize = undefined !== pageOptions && 0 === pageOptions.start && undefined !== pageOptions.size; if (requestSize) { // eslint-disable-next-line @typescript-eslint/no-deprecated return Presentation.presentation.getContentAndSize(options); } // eslint-disable-next-line @typescript-eslint/no-deprecated const content = await Presentation.presentation.getContent(options); return content ? { content, size: content.contentSet.length } : undefined; }, { isMatchingKey: MemoizationHelpers.areContentRequestsEqual }); onContentUpdate() { // note: subclasses are expected to override `invalidateCache` and notify components about // the changed content so components know to reload this.invalidateCache(CacheInvalidationProps.full()); } onIModelContentChanged = (args) => { if (args.rulesetId === this.rulesetId && args.imodelKey === this.imodel.key) { this.onContentUpdate(); } }; onRulesetModified = (curr) => { if (curr.id === this.rulesetId) { this.onContentUpdate(); } }; onRulesetVariableChanged = () => { this.onContentUpdate(); }; onUnitSystemChanged = () => { this.invalidateCache({ content: true }); }; } class MemoizationHelpers { static areContentRequestsEqual(lhsArgs, rhsArgs) { /* c8 ignore next 3 */ if ((lhsArgs[0]?.start ?? 0) !== (rhsArgs[0]?.start ?? 0)) { return false; } /* c8 ignore next 3 */ if ((lhsArgs[0]?.size ?? 0) !== (rhsArgs[0]?.size ?? 0)) { return false; } return true; } } //# sourceMappingURL=ContentDataProvider.js.map