UNPKG

@itwin/presentation-common

Version:

Common pieces for iModel.js presentation packages

415 lines 18.5 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 { RelatedClassInfo, RelatedClassInfoWithOptionalRelationship, } from "../EC.js"; import { omitUndefined } from "../Utils.js"; import { CategoryDescription } from "./Category.js"; import { Field, getFieldByDescriptor, getFieldByName, NestedContentField } from "./Fields.js"; /** @public */ export var SelectClassInfo; (function (SelectClassInfo) { /** Deserialize [[SelectClassInfo]] from compressed JSON */ function fromCompressedJSON(json, classesMap) { assert(classesMap.hasOwnProperty(json.selectClassInfo)); return { selectClassInfo: { id: json.selectClassInfo, ...classesMap[json.selectClassInfo] }, isSelectPolymorphic: json.isSelectPolymorphic, ...(json.navigationPropertyClasses ? { navigationPropertyClasses: json.navigationPropertyClasses.map((item) => RelatedClassInfo.fromCompressedJSON(item, classesMap)) } : undefined), ...(json.relatedInstancePaths ? { relatedInstancePaths: json.relatedInstancePaths.map((rip) => rip.map((item) => RelatedClassInfo.fromCompressedJSON(item, classesMap))) } : undefined), ...(json.pathFromInputToSelectClass ? { pathFromInputToSelectClass: json.pathFromInputToSelectClass.map((item) => RelatedClassInfoWithOptionalRelationship.fromCompressedJSON(item, classesMap)), } : undefined), ...(json.relatedPropertyPaths ? { relatedPropertyPaths: json.relatedPropertyPaths.map((path) => path.map((item) => RelatedClassInfo.fromCompressedJSON(item, classesMap))) } : undefined), }; } SelectClassInfo.fromCompressedJSON = fromCompressedJSON; /** Serialize [[SelectClassInfo]] to compressed JSON */ function toCompressedJSON(selectClass, classesMap) { const { id, ...leftOverClassInfo } = selectClass.selectClassInfo; classesMap[id] = leftOverClassInfo; return { selectClassInfo: id, isSelectPolymorphic: selectClass.isSelectPolymorphic, ...(selectClass.relatedInstancePaths ? { relatedInstancePaths: selectClass.relatedInstancePaths.map((rip) => rip.map((item) => RelatedClassInfo.toCompressedJSON(item, classesMap))) } : undefined), ...(selectClass.navigationPropertyClasses ? { navigationPropertyClasses: selectClass.navigationPropertyClasses.map((propertyClass) => RelatedClassInfo.toCompressedJSON(propertyClass, classesMap)), } : undefined), ...(selectClass.pathFromInputToSelectClass ? { pathFromInputToSelectClass: selectClass.pathFromInputToSelectClass.map((item) => RelatedClassInfoWithOptionalRelationship.toCompressedJSON(item, classesMap)), } : undefined), ...(selectClass.relatedPropertyPaths ? { relatedPropertyPaths: selectClass.relatedPropertyPaths.map((path) => path.map((relatedClass) => RelatedClassInfo.toCompressedJSON(relatedClass, classesMap))), } : undefined), }; } SelectClassInfo.toCompressedJSON = toCompressedJSON; })(SelectClassInfo || (SelectClassInfo = {})); /** * Flags that control content format. * @public */ export var ContentFlags; (function (ContentFlags) { /** Each content record only has [[InstanceKey]] and no data */ ContentFlags[ContentFlags["KeysOnly"] = 1] = "KeysOnly"; /** Each content record additionally has a display label */ ContentFlags[ContentFlags["ShowLabels"] = 4] = "ShowLabels"; /** All content records are merged into a single record (see [Merging values]($docs/presentation/content/terminology#value-merging)) */ ContentFlags[ContentFlags["MergeResults"] = 8] = "MergeResults"; /** Content has only distinct values */ ContentFlags[ContentFlags["DistinctValues"] = 16] = "DistinctValues"; /** Doesn't create property or calculated fields. Can be used in conjunction with [[ShowLabels]]. */ ContentFlags[ContentFlags["NoFields"] = 32] = "NoFields"; /** * Set related input keys on [[Item]] objects when creating content. This helps identify which [[Item]] is associated to which * given input key at the cost of performance creating those items. */ ContentFlags[ContentFlags["IncludeInputKeys"] = 256] = "IncludeInputKeys"; })(ContentFlags || (ContentFlags = {})); /** * Data sorting direction * @public */ export var SortDirection; (function (SortDirection) { SortDirection[SortDirection["Ascending"] = 0] = "Ascending"; SortDirection[SortDirection["Descending"] = 1] = "Descending"; })(SortDirection || (SortDirection = {})); /** * Data structure that describes content: fields, sorting, filtering, format, etc. * Descriptor may be changed to control how content is created. * * @public */ export class Descriptor { /** Id of the connection used to create the descriptor */ connectionId; /** Hash of the input keys used to create the descriptor */ inputKeysHash; /** Selection info used to create the descriptor */ selectionInfo; /** Display type used to create the descriptor */ displayType; /** A list of classes that will be selected when creating content with this descriptor */ selectClasses; /** A list of content field categories used in this descriptor */ categories; /** A list of fields contained in the descriptor */ fields; /** [[ContentFlags]] used to create the descriptor */ contentFlags; /** * A ruleset used to create this descriptor. * Only set if descriptor is created using a ruleset different from the input ruleset, e.g. when creating a hierarchy level descriptor. */ ruleset; /** Field used to sort the content */ sortingField; /** Sorting direction */ sortDirection; /** * [ECExpression]($docs/presentation/advanced/ECExpressions.md) for filtering content by * select fields. * * This is different from [[instanceFilter]] as filtering is applied on the union of all selects, * which removes access to content instance property values. Instead of referencing properties * through `this.PropertyName` alias, the expression should reference them by field names. In cases * when properties field merges multiple properties, this allows applying the filter on all of them * at once. This is useful for filtering table rows by column value, when content is displayed in * table format. */ fieldsFilterExpression; /** * Instances filter that allows filtering content by class, properties of specific class * or properties of instances related to the content instance. * * This is different from [[fieldsFilterExpression]] as filter is applied at a lower level - on * specific select class rather than a union of multiple select classes. This means the filter has * access to properties of that class and they can be referenced using symbols like `this.Property`. * This is useful for filtering instances of specific class. */ instanceFilter; #fieldsSelector; #selectedFields; /** Construct a new Descriptor using a [[DescriptorSource]] */ constructor(source) { this.connectionId = source.connectionId; this.inputKeysHash = source.inputKeysHash; this.selectionInfo = source.selectionInfo; this.displayType = source.displayType; this.contentFlags = source.contentFlags; this.selectClasses = [...source.selectClasses]; this.categories = [...source.categories]; this.fields = [...source.fields]; this.sortingField = source.sortingField; this.sortDirection = source.sortDirection; this.fieldsFilterExpression = source.fieldsFilterExpression; this.instanceFilter = source.instanceFilter; this.ruleset = source.ruleset; this.#fieldsSelector = source.fieldsSelector; } /** Serialize [[Descriptor]] to JSON */ toJSON() { const classesMap = {}; const selectClasses = this.selectClasses.map((selectClass) => SelectClassInfo.toCompressedJSON(selectClass, classesMap)); const fields = this.fields.map((field) => field.toCompressedJSON(classesMap)); return omitUndefined({ displayType: this.displayType, contentFlags: this.contentFlags, categories: this.categories.map(CategoryDescription.toJSON), fields, selectClasses, classesMap, connectionId: this.connectionId ?? "", inputKeysHash: this.inputKeysHash ?? "", sortingFieldName: this.sortingField?.name, sortDirection: this.sortDirection, fieldsFilterExpression: this.fieldsFilterExpression, instanceFilter: this.instanceFilter, selectionInfo: this.selectionInfo, ruleset: this.ruleset, }); } /** Deserialize [[Descriptor]] from JSON */ static fromJSON(json) { if (!json) { return undefined; } const { categories: jsonCategories, selectClasses: jsonSelectClasses, fields: jsonFields, connectionId, inputKeysHash, classesMap, sortingFieldName, ...leftOverJson } = json; const categories = CategoryDescription.listFromJSON(jsonCategories); const selectClasses = jsonSelectClasses.map((jsc) => SelectClassInfo.fromCompressedJSON(jsc, classesMap)); const fields = this.getFieldsFromJSON(jsonFields, (fieldJson) => Field.fromCompressedJSON(fieldJson, classesMap, categories)); return new Descriptor({ ...leftOverJson, ...(connectionId ? /* c8 ignore next */ { connectionId } : undefined), ...(inputKeysHash ? /* c8 ignore next */ { inputKeysHash } : undefined), selectClasses, categories, fields, sortingField: getFieldByName(fields, sortingFieldName, true), }); } static getFieldsFromJSON(json, factory) { return json .map((fieldJson) => { const field = factory(fieldJson); if (field) { field.rebuildParentship(); } return field; }) .filter((field) => !!field); } /** * 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.fields, name, recurse); } /** * Get field by its descriptor. */ getFieldByDescriptor(fieldDescriptor, recurse) { return getFieldByDescriptor(this.fields, fieldDescriptor, recurse); } /** * Fields selector that allows excluding or including only specified fields. When set, the `selectedFields` * property will return a subset of `fields` based on the selector configuration. */ get fieldsSelector() { return this.#fieldsSelector; } set fieldsSelector(selector) { this.#fieldsSelector = selector; this.#selectedFields = undefined; // reset cached selected fields } /** Get selected fields based on `fields` in this descriptor and `fieldsSelector`. */ get selectedFields() { if (!this.#fieldsSelector) { return this.fields; } if (!this.#selectedFields) { const { type, fields: selectedFields } = this.#fieldsSelector; switch (type) { case "include": this.#selectedFields = exclusivelyIncludeFields(this.fields, selectedFields); break; case "exclude": this.#selectedFields = excludeFields(this.fields, selectedFields); break; } } return this.#selectedFields; } /** * Create descriptor overrides object from this descriptor. * @public */ createDescriptorOverrides() { const overrides = {}; if (this.displayType) { overrides.displayType = this.displayType; } if (this.contentFlags !== 0) { overrides.contentFlags = this.contentFlags; } if (this.fieldsFilterExpression) { overrides.fieldsFilterExpression = this.fieldsFilterExpression; } if (this.instanceFilter) { overrides.instanceFilter = this.instanceFilter; } if (this.sortingField) { overrides.sorting = { field: this.sortingField.getFieldDescriptor(), direction: this.sortDirection ?? SortDirection.Ascending }; } if (this.fieldsSelector) { overrides.fieldsSelector = this.fieldsSelector; } return overrides; } } /** * Creates a shallow clone of a `NestedContentField` - copies its own properties but does not * recursively deep-clone `nestedFields`. Instead, the children array is shallow-copied so it * can be independently mutated without affecting the original. * * This avoids the cost of `NestedContentField.clone()` which deep-clones the entire subtree * and calls `rebuildParentship` recursively - work that is wasted when callers immediately * clear or modify the children. */ function shallowCloneNestedContentField(field) { return new NestedContentField({ ...field, nestedFields: [...field.nestedFields], }); } function exclusivelyIncludeFields(descriptorFields, targetFieldDescriptors) { const rootFields = []; const clones = new Map(); for (const targetFieldDescriptor of targetFieldDescriptors) { const includeField = getFieldByDescriptor(descriptorFields, targetFieldDescriptor, true); if (!includeField) { continue; } let clone; let curr = includeField; while (curr) { const prev = clone; const existingClone = clones.get(curr); if (existingClone) { // `curr` is already cloned, which means the fields hierarchy from root to `curr` is already // built - we only need to add `prev` and that's it if (prev) { prev.rebuildParentship(existingClone); existingClone.nestedFields.push(prev); clone = undefined; } break; } // `curr` is not cloned yet if (curr.isNestedContentField()) { const nestedClone = shallowCloneNestedContentField(curr); clones.set(curr, nestedClone); nestedClone.nestedFields = []; if (prev) { prev.rebuildParentship(nestedClone); nestedClone.nestedFields.push(prev); } clone = nestedClone; } else { clone = curr.clone(); } curr = curr.parent; } if (clone) { rootFields.push(clone); } } return rootFields; } function excludeFields(descriptorFields, targetFieldDescriptors) { // Maps an original root field to its replacement (cloned field or `undefined` if root should be removed) const rootReplacements = new Map(); // Tracks already-cloned NestedContentFields so we reuse the same clone when multiple excluded fields share a parent const clones = new Map(); for (const targetFieldDescriptor of targetFieldDescriptors) { const excludeField = getFieldByDescriptor(descriptorFields, targetFieldDescriptor, true); if (!excludeField) { continue; } // `curr` tracks the pair: (original field, its replacement clone or undefined if removed) let curr = { original: excludeField, replacement: undefined }; let parent = excludeField.parent; while (parent) { let parentClone = clones.get(parent); if (!parentClone) { parentClone = shallowCloneNestedContentField(parent); clones.set(parent, parentClone); } // Find the child in the parent clone's nestedFields that corresponds to curr.original const childIndex = parentClone.nestedFields.findIndex((f) => f.name === curr.original.name); if (childIndex !== -1) { if (curr.replacement && curr.replacement.isNestedContentField() && curr.replacement.nestedFields.length > 0) { // Replace the child with the modified clone parentClone.nestedFields[childIndex] = curr.replacement; curr.replacement.rebuildParentship(parentClone); } else { // Remove the child parentClone.nestedFields.splice(childIndex, 1); } } curr = { original: parent, replacement: parentClone }; parent = parent.parent; } rootReplacements.set(curr.original, curr.replacement); } if (rootReplacements.size === 0) { return descriptorFields; } const result = []; for (const field of descriptorFields) { if (!rootReplacements.has(field)) { // Field is not affected by exclusions — keep as-is result.push(field); continue; } const replacement = rootReplacements.get(field); if (replacement === undefined) { // No replacement — field is removed entirely continue; } if (replacement.isNestedContentField() && replacement.nestedFields.length === 0) { // Replacement is an empty nested content field — remove it continue; } // Valid replacement — use the clone result.push(replacement); } return result; } //# sourceMappingURL=Descriptor.js.map