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