@itwin/presentation-components
Version:
React components based on iTwin.js Presentation library
311 lines • 12.2 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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