diff --git a/.changeset/tricky-rocks-heal.md b/.changeset/tricky-rocks-heal.md new file mode 100644 index 00000000..3a9d259a --- /dev/null +++ b/.changeset/tricky-rocks-heal.md @@ -0,0 +1,7 @@ +--- +"@itwin/changed-elements-react": patch +--- + +Fix change report generation: +- Properties are now properly populated in the report. +- Changed model report fixed to correctly find filenames via source aspect queries. diff --git a/packages/changed-elements-react/src/api/ModelReportGenerator.ts b/packages/changed-elements-react/src/api/ModelReportGenerator.ts index eb2f5d42..0c99157f 100644 --- a/packages/changed-elements-react/src/api/ModelReportGenerator.ts +++ b/packages/changed-elements-react/src/api/ModelReportGenerator.ts @@ -2,7 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import { QueryRowFormat, type ModelProps } from "@itwin/core-common"; +import { QueryBinder, QueryRowFormat, type ModelProps } from "@itwin/core-common"; import { IModelApp, IModelConnection } from "@itwin/core-frontend"; import type { ChangedElementEntry } from "./ChangedElementEntryCache.js"; @@ -20,18 +20,18 @@ interface ModelInfo { } /** Make the query for getting the source information */ -const makeQuery = (modelProps: ModelProps[]) => { - let queryJsonProps = - "SELECT mea.Element.Id as id, ea.JsonProperties as jsonProps FROM Bis.ExternalSourceAspect ea " + - "JOIN bis.ExternalSourceAspect mea ON mea.Scope.Id = ea.Element.Id " + - "WHERE mea.Kind='Model' AND mea.Element.Id in ("; - for (const prop of modelProps) { - if (prop.id) { - queryJsonProps += prop.id + ","; - } +const makeQuery = (modelIds: string[]): string | undefined => { + if (modelIds.length === 0) { + return undefined; } - queryJsonProps = queryJsonProps.substr(0, queryJsonProps.length - 1) + ")"; - return queryJsonProps; + + return ( + "SELECT DISTINCT model.ECInstanceId as modelId, scopeAspect.JsonProperties as jsonProps FROM Bis.ExternalSourceAspect ea " + + "JOIN Bis.Element element ON ea.Element.Id = element.ECInstanceId " + + "JOIN Bis.Model model ON element.Model.Id = model.ECInstanceId OR ea.Element.Id = model.ModeledElement.Id " + + "JOIN Bis.ExternalSourceAspect scopeAspect ON ea.Scope.Id = scopeAspect.Element.Id " + + "WHERE InVirtualSet(?, model.ECInstanceId)" + ); }; /** Returns a map for the sources of the model (model Id -> file name) */ @@ -41,15 +41,22 @@ const getModelSources = async ( ): Promise> => { // This may fail if the connector doesn't add this information try { - const query = makeQuery(modelProps); const map = new Map(); - for await (const row of iModel.createQueryReader(query, undefined, { - rowFormat: QueryRowFormat.UseJsPropertyNames, + const modelIds = modelProps.flatMap((prop) => prop.id === undefined ? [] : [prop.id]); + const query = makeQuery(modelIds); + if (query === undefined) { + return map; + } + + const queryBinder = new QueryBinder(); + queryBinder.bindIdSet(1, modelIds); + for await (const row of iModel.createQueryReader(query, queryBinder, { + rowFormat: QueryRowFormat.UseECSqlPropertyNames, })) { - if (row.id !== undefined && row.jsonProps !== undefined) { + if (row.modelId !== undefined && row.jsonProps !== undefined) { const jsonProps = JSON.parse(row.jsonProps); if (jsonProps?.fileName !== undefined) { - map.set(row.id, jsonProps.fileName); + map.set(row.modelId, jsonProps.fileName); } } } diff --git a/packages/changed-elements-react/src/api/ReportGenerator.ts b/packages/changed-elements-react/src/api/ReportGenerator.ts index 3b4f1808..2d42df50 100644 --- a/packages/changed-elements-react/src/api/ReportGenerator.ts +++ b/packages/changed-elements-react/src/api/ReportGenerator.ts @@ -5,7 +5,7 @@ import { DbOpcode } from "@itwin/core-bentley"; import { QueryBinder, QueryRowFormat, TypeOfChange } from "@itwin/core-common"; import { IModelApp, IModelConnection } from "@itwin/core-frontend"; -import { DisplayValue, type Field, type InstanceKey, KeySet, type Ruleset } from "@itwin/presentation-common"; +import { DisplayValue, type Field, type InstanceKey, KeySet, type Item, type Ruleset, Value } from "@itwin/presentation-common"; import { Presentation } from "@itwin/presentation-frontend"; import { ChangeElementType, type ChangedElementEntry } from "./ChangedElementEntryCache.js"; @@ -311,10 +311,15 @@ export class ReportGenerator extends ReportGeneratorBase { /** Gets all wanted field descriptors based on the given fields and our wanted properties */ private _getWantedFields(fields: Field[]): Field[] { const wantedFields: Field[] = []; + const fieldDescriptors = new Set(); for (const field of fields) { const currentWantedFields = this._getWantedFieldsFromField(field); - if (currentWantedFields !== undefined) { - wantedFields.push(...currentWantedFields); + for (const wantedField of currentWantedFields) { + const descriptor = JSON.stringify(wantedField.getFieldDescriptor()); + if (!fieldDescriptors.has(descriptor)) { + fieldDescriptors.add(descriptor); + wantedFields.push(wantedField); + } } } return wantedFields; @@ -429,6 +434,29 @@ export class ReportGenerator extends ReportGeneratorBase { return undefined; }; + /** Gets display values for a field, including fields nested beneath related instances */ + private _getFieldDisplayValues = (item: Item, field: Field): DisplayValue[] => { + const fieldPath: Field[] = [field]; + for (let parent = field.parent; parent !== undefined; parent = parent.parent) { + fieldPath.unshift(parent); + } + + let items: Array> = [item]; + for (const parentField of fieldPath.slice(0, -1)) { + const nestedItems: Array> = []; + for (const currentItem of items) { + const nestedValue = currentItem.values[parentField.name]; + if (Value.isNestedContent(nestedValue)) { + nestedItems.push(...nestedValue); + } + } + items = nestedItems; + } + + const propertyField = fieldPath[fieldPath.length - 1]; + return items.map((currentItem) => currentItem.displayValues[propertyField.name]); + }; + /** Loads properties */ private _loadProperties = async ( iModel: IModelConnection, @@ -442,7 +470,16 @@ export class ReportGenerator extends ReportGeneratorBase { // const stubRuleset: Ruleset = { id: "__itwin-changed-elements-react-stub", - rules: [], + rules: [ + { + ruleType: "Content", + specifications: [ + { + specType: "SelectedNodeInstances", + }, + ], + }, + ], }; const keys = this._entriesToKeys(entries); const descriptor = await Presentation.presentation.getContentDescriptor({ @@ -474,8 +511,10 @@ export class ReportGenerator extends ReportGeneratorBase { // Element ids containing the property value const elementIds = singleContent.primaryKeys.map((key: InstanceKey) => key.id); // Value of the field - const propertyValue = singleContent.displayValues[field.name]; - const parsedValue = this._parseDisplayValue(propertyValue); + const parsedValues = this._getFieldDisplayValues(singleContent, field) + .map((propertyValue) => this._parseDisplayValue(propertyValue)) + .filter((propertyValue): propertyValue is string => propertyValue !== undefined); + const parsedValue = parsedValues.length > 0 ? parsedValues.join("; ") : undefined; if (field.isPropertiesField()) { // Property name, normally should only be one property here for (const property of field.properties) { @@ -656,10 +695,10 @@ export class ReportGenerator extends ReportGeneratorBase { // Create string of values separated by commas for (const prop of props) { data += - this._cleanComma(propertyMap?.get(prop.propertyName)?.newValue ?? "") + + this._cleanComma(propertyMap?.get(prop.propertyName)?.newValue ?? "-") + ","; data += - this._cleanComma(propertyMap?.get(prop.propertyName)?.oldValue ?? "") + + this._cleanComma(propertyMap?.get(prop.propertyName)?.oldValue ?? "-") + ","; }