UNPKG

@itwin/presentation-common

Version:

Common pieces for iModel.js presentation packages

718 lines • 27.1 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 Content */ import { assert } from "@itwin/core-bentley"; import { NavigationPropertyInfo, RelatedClassInfo, RelationshipPath, } from "../EC.js"; import { PresentationError, PresentationStatus } from "../Error.js"; import { RelationshipMeaning } from "../rules/content/modifiers/RelatedPropertiesSpecification.js"; import { omitUndefined } from "../Utils.js"; import { Property } from "./Property.js"; function isPropertiesField(field) { return !!field.properties; } function isArrayPropertiesField(field) { return !!field.itemsField; } function isStructPropertiesField(field) { return !!field.memberFields; } function isNestedContentField(field) { return !!field.nestedFields; } /** * Describes a single content field. A field is usually represented as a grid column * or a property pane row. * * @public */ export class Field { /** Category information */ category; /** Unique name */ name; /** Display label */ label; /** Description of this field's values data type */ type; /** Are values in this field read-only */ isReadonly; /** Priority of the field. Higher priority fields should appear first in the UI */ priority; /** Property renderer used to render values of this field */ renderer; /** Property editor used to edit values of this field */ editor; /** Extended data associated with this field */ extendedData; /** Parent field */ _parent; constructor(categoryOrProps, name, label, type, isReadonly, priority, editor, renderer, extendedData) { /* c8 ignore next 14 */ const props = "category" in categoryOrProps ? categoryOrProps : { category: categoryOrProps, name: name, label: label, type: type, isReadonly: isReadonly, priority: priority, editor, renderer, extendedData, }; this.category = props.category; this.name = props.name; this.label = props.label; this.type = props.type; this.isReadonly = props.isReadonly; this.priority = props.priority; this.editor = props.editor; this.renderer = props.renderer; this.extendedData = props.extendedData; } /** * Is this a [[PropertiesField]] */ isPropertiesField() { return isPropertiesField(this); } /** * Is this a [[NestedContentField]] */ isNestedContentField() { return isNestedContentField(this); } /** * Get parent */ get parent() { return this._parent; } clone() { const clone = new Field(this); clone.rebuildParentship(this.parent); return clone; } /** * Serialize this object to JSON. * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[toCompressedJSON]] instead. */ toJSON() { return this.toCompressedJSON({}); } /** Serialize this object to compressed JSON */ toCompressedJSON(_classesMap) { return omitUndefined({ category: this.category.name, name: this.name, label: this.label, type: this.type, isReadonly: this.isReadonly, priority: this.priority, renderer: this.renderer, editor: this.editor, extendedData: this.extendedData, }); } /** * Deserialize [[Field]] from JSON. * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[fromCompressedJSON]] instead. */ static fromJSON(json, categories) { if (!json) { return undefined; } if (isPropertiesField(json)) { // eslint-disable-next-line @typescript-eslint/no-deprecated return PropertiesField.fromJSON(json, categories); } if (isNestedContentField(json)) { return new NestedContentField({ ...json, ...fromNestedContentFieldJSON(json, categories), nestedFields: json.nestedFields // eslint-disable-next-line @typescript-eslint/no-deprecated .map((nestedFieldJson) => Field.fromJSON(nestedFieldJson, categories)) .filter((nestedField) => !!nestedField), }); } return new Field({ ...json, category: this.getCategoryFromFieldJson(json, categories), }); } /** Deserialize a [[Field]] from compressed JSON. */ static fromCompressedJSON(json, classesMap, categories) { if (!json) { return undefined; } if (isPropertiesField(json)) { return PropertiesField.fromCompressedJSON(json, classesMap, categories); } if (isNestedContentField(json)) { return NestedContentField.fromCompressedJSON(json, classesMap, categories); } return new Field({ ...json, category: this.getCategoryFromFieldJson(json, categories), }); } static getCategoryFromFieldJson(fieldJson, categories) { return getCategoryFromFieldJson(fieldJson, categories); } /** Resets field's parent. */ resetParentship() { this._parent = undefined; } /** Sets provided [[NestedContentField]] as parent of this field. */ rebuildParentship(parentField) { this._parent = parentField; } /** * Get descriptor for this field. * @public */ getFieldDescriptor() { return { type: FieldDescriptorType.Name, fieldName: this.name, }; } /** * Checks if this field matches given field descriptor * @see [[getFieldDescriptor]] */ matchesDescriptor(descriptor) { return FieldDescriptor.isNamed(descriptor) && descriptor.fieldName === this.name; } } /** * Describes a content field that's based on one or more similar * EC properties. * * @public */ export class PropertiesField extends Field { /** A list of properties this field is created from */ properties; constructor(categoryOrProps, name, label, type, isReadonly, priority, properties, editor, renderer) { /* c8 ignore next 14 */ const props = "category" in categoryOrProps ? categoryOrProps : { category: categoryOrProps, name: name, label: label, type: type, isReadonly: isReadonly, priority: priority, editor, renderer, properties: properties, }; super(props); this.properties = props.properties; } /** Is this a an array property field */ isArrayPropertiesField() { return false; } /** Is this a an struct property field */ isStructPropertiesField() { return false; } clone() { const clone = new PropertiesField(this); clone.rebuildParentship(this.parent); return clone; } /** * Serialize this object to JSON * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[toCompressedJSON]] instead. */ toJSON() { return { // eslint-disable-next-line @typescript-eslint/no-deprecated ...super.toJSON(), properties: this.properties, }; } /** Serialize this object to compressed JSON */ toCompressedJSON(classesMap) { return { ...super.toCompressedJSON(classesMap), properties: this.properties.map((property) => Property.toCompressedJSON(property, classesMap)), }; } /** * Deserialize [[PropertiesField]] from JSON. * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[fromCompressedJSON]] instead. */ static fromJSON(json, categories) { if (!json) { return undefined; } if (isArrayPropertiesField(json)) { // eslint-disable-next-line @typescript-eslint/no-deprecated return ArrayPropertiesField.fromJSON(json, categories); } if (isStructPropertiesField(json)) { // eslint-disable-next-line @typescript-eslint/no-deprecated return StructPropertiesField.fromJSON(json, categories); } return new PropertiesField({ ...json, category: this.getCategoryFromFieldJson(json, categories), }); } /** * Deserialize a [[PropertiesField]] from compressed JSON. * @public */ static fromCompressedJSON(json, classesMap, categories) { if (isArrayPropertiesField(json)) { return ArrayPropertiesField.fromCompressedJSON(json, classesMap, categories); } if (isStructPropertiesField(json)) { return StructPropertiesField.fromCompressedJSON(json, classesMap, categories); } return new PropertiesField({ ...json, category: this.getCategoryFromFieldJson(json, categories), properties: json.properties.map((propertyJson) => fromCompressedPropertyJSON(propertyJson, classesMap)), }); } /** * Get descriptor for this field. * @public */ getFieldDescriptor() { const pathFromPropertyToSelectClass = new Array(); let currAncestor = this.parent; while (currAncestor) { pathFromPropertyToSelectClass.push(...currAncestor.pathToPrimaryClass); currAncestor = currAncestor.parent; } return { type: FieldDescriptorType.Properties, pathFromSelectToPropertyClass: RelationshipPath.strip(RelationshipPath.reverse(pathFromPropertyToSelectClass)), properties: this.properties.map((p) => ({ class: p.property.classInfo.name, name: p.property.name, })), }; } /** * Checks if this field matches given field descriptor * @see [[getFieldDescriptor]] */ matchesDescriptor(descriptor) { if (!FieldDescriptor.isProperties(descriptor)) { return false; } // ensure at least one descriptor property matches at least one property of this field if (!this.properties.some(({ property: fieldProperty }) => descriptor.properties.some((descriptorProperty) => fieldProperty.name === descriptorProperty.name && fieldProperty.classInfo.name === descriptorProperty.class))) { return false; } // ensure path from select to property in field and in descriptor matches let stepsCount = 0; let currAncestor = this.parent; while (currAncestor) { const pathFromCurrentToItsParent = RelationshipPath.reverse(currAncestor.pathToPrimaryClass); for (const step of pathFromCurrentToItsParent) { if (descriptor.pathFromSelectToPropertyClass.length < stepsCount + 1) { return false; } if (!RelatedClassInfo.equals(step, descriptor.pathFromSelectToPropertyClass[descriptor.pathFromSelectToPropertyClass.length - stepsCount - 1])) { return false; } ++stepsCount; } currAncestor = currAncestor.parent; } return true; } } /** * Describes a content field that's based on one or more similar EC array properties. * @public */ export class ArrayPropertiesField extends PropertiesField { itemsField; constructor(categoryOrProps, name, label, type, itemsField, isReadonly, priority, properties, editor, renderer) { /* c8 ignore next 15 */ const props = "category" in categoryOrProps ? categoryOrProps : { category: categoryOrProps, name: name, label: label, type: type, isReadonly: isReadonly, priority: priority, editor, renderer, properties: properties, itemsField: itemsField, }; super(props); this.itemsField = props.itemsField; } isArrayPropertiesField() { return true; } clone() { const clone = new ArrayPropertiesField(this); clone.rebuildParentship(this.parent); return clone; } /** * Serialize this object to JSON. * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[toCompressedJSON]] instead. */ toJSON() { return { // eslint-disable-next-line @typescript-eslint/no-deprecated ...super.toJSON(), // eslint-disable-next-line @typescript-eslint/no-deprecated itemsField: this.itemsField.toJSON(), }; } /** Serialize this object to compressed JSON */ toCompressedJSON(classesMap) { return { ...super.toCompressedJSON(classesMap), itemsField: this.itemsField.toCompressedJSON(classesMap), }; } /** * Deserialize [[ArrayPropertiesField]] from JSON. * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[fromCompressedJSON]] instead. */ static fromJSON(json, categories) { return new ArrayPropertiesField({ ...json, category: this.getCategoryFromFieldJson(json, categories), // eslint-disable-next-line @typescript-eslint/no-deprecated itemsField: PropertiesField.fromJSON(json.itemsField, categories), }); } /** * Deserialize an [[ArrayPropertiesField]] from compressed JSON. * @public */ static fromCompressedJSON(json, classesMap, categories) { return new ArrayPropertiesField({ ...json, category: this.getCategoryFromFieldJson(json, categories), properties: json.properties.map((propertyJson) => fromCompressedPropertyJSON(propertyJson, classesMap)), itemsField: PropertiesField.fromCompressedJSON(json.itemsField, classesMap, categories), }); } } /** * Describes a content field that's based on one or more similar EC struct properties. * @public */ export class StructPropertiesField extends PropertiesField { memberFields; constructor(categoryOrProps, name, label, type, memberFields, isReadonly, priority, properties, editor, renderer) { /* c8 ignore next 15 */ const props = "category" in categoryOrProps ? categoryOrProps : { category: categoryOrProps, name: name, label: label, type: type, isReadonly: isReadonly, priority: priority, editor, renderer, properties: properties, memberFields: memberFields, }; super(props); this.memberFields = props.memberFields; } isStructPropertiesField() { return true; } clone() { const clone = new StructPropertiesField(this); clone.rebuildParentship(this.parent); return clone; } /** * Serialize this object to JSON. * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[toCompressedJSON]] instead. */ toJSON() { return { // eslint-disable-next-line @typescript-eslint/no-deprecated ...super.toJSON(), // eslint-disable-next-line @typescript-eslint/no-deprecated memberFields: this.memberFields.map((m) => m.toJSON()), }; } /** Serialize this object to compressed JSON */ toCompressedJSON(classesMap) { return { ...super.toCompressedJSON(classesMap), memberFields: this.memberFields.map((m) => m.toCompressedJSON(classesMap)), }; } /** * Deserialize [[StructPropertiesField]] from JSON. * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[fromCompressedJSON]] instead. */ static fromJSON(json, categories) { return new StructPropertiesField({ ...json, category: this.getCategoryFromFieldJson(json, categories), // eslint-disable-next-line @typescript-eslint/no-deprecated memberFields: json.memberFields.map((m) => PropertiesField.fromJSON(m, categories)), }); } /** * Deserialize a [[StructPropertiesField]] from compressed JSON. * @public */ static fromCompressedJSON(json, classesMap, categories) { return new StructPropertiesField({ ...json, category: this.getCategoryFromFieldJson(json, categories), properties: json.properties.map((propertyJson) => fromCompressedPropertyJSON(propertyJson, classesMap)), memberFields: json.memberFields.map((m) => PropertiesField.fromCompressedJSON(m, classesMap, categories)), }); } } /** * Describes a content field that contains [Nested content]($docs/presentation/content/Terminology#nested-content). * * @public */ export class NestedContentField extends Field { /** Information about an ECClass whose properties are nested inside this field */ contentClassInfo; /** Relationship path to [Primary class]($docs/presentation/content/Terminology#primary-class) */ pathToPrimaryClass; /** * Meaning of the relationship between the [primary class]($docs/presentation/content/Terminology#primary-class) * and content class of this field. * * The value is set up through [[RelatedPropertiesSpecification.relationshipMeaning]] attribute when setting up * presentation rules for creating the content. */ relationshipMeaning; /** * When content descriptor is requested in a polymorphic fashion, fields get created if at least one of the concrete classes * has it. In certain situations it's necessary to know which concrete classes caused that and this attribute is * here to help. * * **Example:** There's a base class `A` and it has two derived classes `B` and `C` and class `B` has a relationship to class `D`. * When content descriptor is requested for class `A` polymorphically, it's going to contain fields for all properties of class `B`, * class `C` and a nested content field for the `B -> D` relationship. The nested content field's `actualPrimaryClassIds` attribute * will contain ID of class `B`, identifying that only this specific class has the relationship. */ actualPrimaryClassIds; /** Contained nested fields */ nestedFields; /** Flag specifying whether field should be expanded */ autoExpand; constructor(categoryOrProps, name, label, type, isReadonly, priority, contentClassInfo, pathToPrimaryClass, nestedFields, editor, autoExpand, renderer) { /* c8 ignore next 17 */ const props = "category" in categoryOrProps ? categoryOrProps : { category: categoryOrProps, name: name, label: label, type: type, isReadonly: isReadonly, priority: priority, editor, renderer, contentClassInfo: contentClassInfo, pathToPrimaryClass: pathToPrimaryClass, nestedFields: nestedFields, autoExpand, }; super(props); this.contentClassInfo = props.contentClassInfo; this.pathToPrimaryClass = props.pathToPrimaryClass; this.relationshipMeaning = props.relationshipMeaning ?? RelationshipMeaning.RelatedInstance; this.nestedFields = props.nestedFields; this.autoExpand = props.autoExpand; this.actualPrimaryClassIds = props.actualPrimaryClassIds ?? []; } clone() { const clone = new NestedContentField({ ...this, nestedFields: this.nestedFields.map((n) => n.clone()), }); clone.rebuildParentship(this.parent); return clone; } /** * Get field by its name * @param name Name of the field to find * @param recurse Recurse into nested fields */ getFieldByName(name, recurse) { return getFieldByName(this.nestedFields, name, recurse); } /** * Serialize this object to JSON. * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[toCompressedJSON]] instead. */ toJSON() { return { // eslint-disable-next-line @typescript-eslint/no-deprecated ...super.toJSON(), contentClassInfo: this.contentClassInfo, pathToPrimaryClass: this.pathToPrimaryClass, relationshipMeaning: this.relationshipMeaning, actualPrimaryClassIds: this.actualPrimaryClassIds, // eslint-disable-next-line @typescript-eslint/no-deprecated nestedFields: this.nestedFields.map((field) => field.toJSON()), ...(this.autoExpand ? { autoExpand: true } : undefined), }; } /** Serialize this object to compressed JSON */ toCompressedJSON(classesMap) { const { id, ...leftOverInfo } = this.contentClassInfo; classesMap[id] = leftOverInfo; return { ...super.toCompressedJSON(classesMap), contentClassInfo: id, relationshipMeaning: this.relationshipMeaning, actualPrimaryClassIds: this.actualPrimaryClassIds, pathToPrimaryClass: this.pathToPrimaryClass.map((classInfo) => RelatedClassInfo.toCompressedJSON(classInfo, classesMap)), nestedFields: this.nestedFields.map((field) => field.toCompressedJSON(classesMap)), ...(this.autoExpand ? { autoExpand: true } : undefined), }; } /** Deserialize a [[NestedContentField]] from compressed JSON. */ static fromCompressedJSON(json, classesMap, categories) { assert(classesMap.hasOwnProperty(json.contentClassInfo)); return new NestedContentField({ ...json, ...fromNestedContentFieldJSON(json, categories), category: this.getCategoryFromFieldJson(json, categories), nestedFields: json.nestedFields .map((nestedFieldJson) => Field.fromCompressedJSON(nestedFieldJson, classesMap, categories)) .filter((nestedField) => !!nestedField), contentClassInfo: { id: json.contentClassInfo, ...classesMap[json.contentClassInfo] }, pathToPrimaryClass: json.pathToPrimaryClass.map((stepJson) => RelatedClassInfo.fromCompressedJSON(stepJson, classesMap)), }); } /** Resets parent of this field and all nested fields. */ resetParentship() { super.resetParentship(); for (const nestedField of this.nestedFields) { nestedField.resetParentship(); } } /** * Sets provided [[NestedContentField]] as parent of this fields and recursively updates * all nested fields parents. */ rebuildParentship(parentField) { super.rebuildParentship(parentField); for (const nestedField of this.nestedFields) { nestedField.rebuildParentship(this); } } } /** @internal */ export const getFieldByName = (fields, name, recurse) => { if (name) { for (const field of fields) { if (field.name === name) { return field; } if (recurse && field.isNestedContentField()) { const nested = getFieldByName(field.nestedFields, name, recurse); if (nested) { return nested; } } } } return undefined; }; /** @internal */ export const getFieldByDescriptor = (fields, fieldDescriptor, recurse) => { for (const field of fields) { if (field.matchesDescriptor(fieldDescriptor)) { return field; } if (recurse && field.isNestedContentField()) { const nested = getFieldByDescriptor(field.nestedFields, fieldDescriptor, recurse); if (nested) { return nested; } } } return undefined; }; /** * Types of different field descriptors. * @public */ export var FieldDescriptorType; (function (FieldDescriptorType) { FieldDescriptorType["Name"] = "name"; FieldDescriptorType["Properties"] = "properties"; })(FieldDescriptorType || (FieldDescriptorType = {})); /** @public */ // eslint-disable-next-line @typescript-eslint/no-redeclare export var FieldDescriptor; (function (FieldDescriptor) { /** Is this a named field descriptor */ function isNamed(d) { return d.type === FieldDescriptorType.Name; } FieldDescriptor.isNamed = isNamed; /** Is this a properties field descriptor */ function isProperties(d) { return d.type === FieldDescriptorType.Properties; } FieldDescriptor.isProperties = isProperties; })(FieldDescriptor || (FieldDescriptor = {})); function fromCompressedPropertyJSON(compressedPropertyJSON, classesMap) { return { property: fromCompressedPropertyInfoJSON(compressedPropertyJSON.property, classesMap), }; } function fromCompressedPropertyInfoJSON(compressedPropertyJSON, classesMap) { assert(classesMap.hasOwnProperty(compressedPropertyJSON.classInfo)); const { navigationPropertyInfo, ...leftOverPropertyJSON } = compressedPropertyJSON; return { ...leftOverPropertyJSON, classInfo: { id: compressedPropertyJSON.classInfo, ...classesMap[compressedPropertyJSON.classInfo] }, ...(navigationPropertyInfo ? { navigationPropertyInfo: NavigationPropertyInfo.fromCompressedJSON(navigationPropertyInfo, classesMap) } : undefined), }; } function getCategoryFromFieldJson(fieldJson, categories) { const category = categories.find((c) => c.name === fieldJson.category); if (!category) { throw new PresentationError(PresentationStatus.InvalidArgument, `Invalid content field category`); } return category; } function fromNestedContentFieldJSON(json, categories) { return { category: getCategoryFromFieldJson(json, categories), relationshipMeaning: json.relationshipMeaning ?? RelationshipMeaning.RelatedInstance, actualPrimaryClassIds: json.actualPrimaryClassIds ?? [], autoExpand: json.autoExpand, }; } //# sourceMappingURL=Fields.js.map