Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/tricky-rocks-heal.md
Original file line number Diff line number Diff line change
@@ -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.
41 changes: 24 additions & 17 deletions packages/changed-elements-react/src/api/ModelReportGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) */
Expand All @@ -41,15 +41,22 @@ const getModelSources = async (
): Promise<Map<string, string>> => {
// This may fail if the connector doesn't add this information
try {
const query = makeQuery(modelProps);
const map = new Map<string, string>();
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);
}
}
}
Expand Down
55 changes: 47 additions & 8 deletions packages/changed-elements-react/src/api/ReportGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string>();
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;
Expand Down Expand Up @@ -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<Pick<Item, "values" | "displayValues">> = [item];
for (const parentField of fieldPath.slice(0, -1)) {
const nestedItems: Array<Pick<Item, "values" | "displayValues">> = [];
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,
Expand All @@ -442,7 +470,16 @@ export class ReportGenerator extends ReportGeneratorBase {
// </ContentRule>
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({
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 ?? "-") +
",";
}

Expand Down
Loading