UNPKG

@itwin/presentation-components

Version:

React components based on iTwin.js Presentation library

565 lines 24.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 */ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { if (value !== null && value !== void 0) { if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected."); var dispose, inner; if (async) { if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined."); dispose = value[Symbol.asyncDispose]; } if (dispose === void 0) { if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined."); dispose = value[Symbol.dispose]; if (async) inner = dispose; } if (typeof dispose !== "function") throw new TypeError("Object not disposable."); if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } }; env.stack.push({ value: value, dispose: dispose, async: async }); } else if (async) { env.stack.push({ async: true }); } return value; }; var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) { return function (env) { function fail(e) { env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e; env.hasError = true; } var r, s = 0; function next() { while (r = env.stack.pop()) { try { if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next); if (r.dispose) { var result = r.dispose.call(r.value); if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); } else s |= 1; } catch (e) { fail(e); } } if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve(); if (env.hasError) throw env.error; } return next(); }; })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }); import "./DisposePolyfill.js"; import { assert, Logger } from "@itwin/core-bentley"; import { IModelApp } from "@itwin/core-frontend"; import { Content, DEFAULT_KEYS_BATCH_SIZE, Descriptor, DisplayValue, KeySet, KoqPropertyValueFormatter, Value, } 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 = []; _isContentFormatted = false; /** 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); } #dispose() { for (const removeListener of this._listeners) { removeListener(); } this._listeners = []; } /** 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(); } /** 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; } if ((props.formatting || props.content || props.size) && this._getFormattedContentAndSize) { this._getFormattedContentAndSize.cache.keys.length = 0; this._getFormattedContentAndSize.cache.values.length = 0; this._isContentFormatted = false; } } 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)); IModelApp.formatsProvider && this._listeners.push(IModelApp.formatsProvider.onFormatsChanged.addListener(this.onFormatsChanged)); } /** * 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._getFormattedContentAndSize(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) => { const env_1 = { stack: [], error: void 0, hasError: false }; try { 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, }; // we always get formatted content from presentation manager - ensure // we set `_isContentFormatted = true` when we finish getting content, to // avoid formatting it again unnecessarily const _ = __addDisposableResource(env_1, { [Symbol.dispose]: () => { this._isContentFormatted = true; }, }, false); 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 await 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; } catch (e_1) { env_1.error = e_1; env_1.hasError = true; } finally { __disposeResources(env_1); } }, { isMatchingKey: areContentRequestsEqual }); _getFormattedContentAndSize = memoize(async (pageOptions) => { const result = await this._getContentAndSize(pageOptions); if (result && !this._isContentFormatted) { const formatter = new ContentFormatter(this._imodel); result.content = await formatter.formatContent(result.content); this._isContentFormatted = true; } return result; }, { isMatchingKey: 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({ formatting: true }); }; onFormatsChanged = () => { this.invalidateCache({ formatting: true }); }; } function 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; } /* c8 ignore start */ /** * This is copied from https://github.com/iTwin/itwinjs-core/blob/846393ad2cd49033923f3e7abf100968c2414918/presentation/common/src/presentation-common/content/PropertyValueFormatter.ts#L21 * and should be removed once https://github.com/iTwin/itwinjs-core/issues/8441 is done. */ class ContentFormatter { _propertyValueFormatter; _unitSystem; constructor(imodel) { // note: using deprecated version of `KoqPropertyValueFormatter` constructor because we // support 4.x core, where the updated overload doesn't exist // eslint-disable-next-line @typescript-eslint/no-deprecated this._propertyValueFormatter = new ContentPropertyValueFormatter(new KoqPropertyValueFormatter(imodel.schemaContext, undefined, IModelApp.formatsProvider)); this._unitSystem = IModelApp.quantityFormatter.activeUnitSystem; } async formatContent(content) { const formattedItems = await this.formatContentItems(content.contentSet, content.descriptor); return new Content(content.descriptor, formattedItems); } async formatContentItems(items, descriptor) { return Promise.all(items.map(async (item) => { await this.formatValues(item.values, item.displayValues, descriptor.fields); return item; })); } async formatValues(values, displayValues, fields) { for (const field of fields) { const value = values[field.name]; // do not add undefined value to display values if (value === undefined) { continue; } // format display values of nested content field if (field.isNestedContentField()) { assert(Value.isNestedContent(value)); await this.formatNestedContentDisplayValues(value, field.nestedFields); continue; } // format property items if (field.isPropertiesField()) { displayValues[field.name] = await this.formatPropertyValue(value, displayValues[field.name], field); continue; } displayValues[field.name] = await this._propertyValueFormatter.formatPropertyValue(field, value, this._unitSystem); } } async formatNestedContentDisplayValues(nestedValues, fields) { await Promise.all(nestedValues.map(async (nestedValue) => { await this.formatValues(nestedValue.values, nestedValue.displayValues, fields); })); } async formatPropertyValue(value, fallbackDisplayValue, field) { if (field.isArrayPropertiesField()) { assert(Value.isArray(value)); assert(DisplayValue.isArray(fallbackDisplayValue)); return this.formatArrayItems(value, fallbackDisplayValue, field); } if (field.isStructPropertiesField()) { assert(Value.isMap(value)); assert(DisplayValue.isMap(fallbackDisplayValue)); return this.formatStructMembers(value, fallbackDisplayValue, field); } return this._propertyValueFormatter.formatPropertyValue(field, value, fallbackDisplayValue, this._unitSystem); } async formatArrayItems(itemValues, itemFallbackDisplayValues, field) { return Promise.all(itemValues.map(async (value, index) => this.formatPropertyValue(value, itemFallbackDisplayValues[index], field.itemsField))); } async formatStructMembers(memberValues, memberFallbackDisplayValues, field) { const displayValues = {}; await Promise.all(field.memberFields.map(async (memberField) => { displayValues[memberField.name] = await this.formatPropertyValue(memberValues[memberField.name], memberFallbackDisplayValues[memberField.name], memberField); })); return displayValues; } } class ContentPropertyValueFormatter { _koqValueFormatter; constructor(_koqValueFormatter) { this._koqValueFormatter = _koqValueFormatter; } async formatPropertyValue(field, value, fallbackDisplayValue, unitSystem) { const doubleFormatter = isFieldWithKoq(field) ? async (rawValue) => { const koq = field.properties[0].property.kindOfQuantity; const formattedValue = await this._koqValueFormatter.format(rawValue, { koqName: koq.name, unitSystem }); if (formattedValue !== undefined) { return formattedValue; } return formatDouble(rawValue); } : async (rawValue) => formatDouble(rawValue); return this.formatValue(field, value, fallbackDisplayValue, { doubleFormatter }); } async formatValue(field, value, fallbackDisplayValue, ctx) { if (field.isPropertiesField()) { if (field.isArrayPropertiesField()) { return this.formatArrayValue(field, value, fallbackDisplayValue); } if (field.isStructPropertiesField()) { return this.formatStructValue(field, value, fallbackDisplayValue); } } return this.formatPrimitiveValue(field, value, fallbackDisplayValue, ctx); } /** * Note: This function only handles numeric properties, handling of all other kinds was intentionally removed. We only need to re-format content * to react to changes in property formats, and they only affect numeric properties. Skipping the rest allows us to avoid doing localization afterwards. */ async formatPrimitiveValue(field, value, fallbackDisplayValue, ctx) { if (value === undefined) { return ""; } const formatDoubleValue = async (raw) => (ctx ? ctx.doubleFormatter(raw) : formatDouble(raw)); if (field.type.typeName === "point2d" && isPoint2d(value)) { return `X: ${await formatDoubleValue(value.x)}; Y: ${await formatDoubleValue(value.y)}`; } if (field.type.typeName === "point3d" && isPoint3d(value)) { return `X: ${await formatDoubleValue(value.x)}; Y: ${await formatDoubleValue(value.y)}; Z: ${await formatDoubleValue(value.z)}`; } if (field.type.typeName === "int" || field.type.typeName === "long") { assert(isNumber(value)); return value.toFixed(0); } if (field.type.typeName === "double") { assert(isNumber(value)); return formatDoubleValue(value); } return fallbackDisplayValue; } async formatStructValue(field, value, fallbackDisplayValue) { if (!Value.isMap(value) || !DisplayValue.isMap(fallbackDisplayValue)) { return {}; } const formattedMember = {}; for (const member of field.memberFields) { formattedMember[member.name] = await this.formatValue(member, value[member.name], fallbackDisplayValue[member.name]); } return formattedMember; } async formatArrayValue(field, value, fallbackDisplayValue) { if (!Value.isArray(value) || !DisplayValue.isArray(fallbackDisplayValue)) { return []; } return Promise.all(value.map(async (arrayVal, i) => { return this.formatValue(field.itemsField, arrayVal, fallbackDisplayValue[i]); })); } } function formatDouble(value) { return value.toFixed(2); } function isFieldWithKoq(field) { return field.isPropertiesField() && field.properties.length > 0 && field.properties[0].property.kindOfQuantity !== undefined; } function isPoint2d(obj) { return obj !== undefined && isNumber(obj.x) && isNumber(obj.y); } function isPoint3d(obj) { return isPoint2d(obj) && isNumber(obj.z); } function isNumber(obj) { return !isNaN(Number(obj)); } /* c8 ignore end */ //# sourceMappingURL=ContentDataProvider.js.map