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
12 changes: 12 additions & 0 deletions .changeset/ready-days-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@itwin/presentation-components": patch
"@itwin/presentation-core-interop": patch
"@itwin/presentation-hierarchies": patch
"@itwin/presentation-hierarchies-react": patch
"@itwin/presentation-opentelemetry": patch
"@itwin/presentation-shared": patch
"@itwin/presentation-testing": patch
"@itwin/unified-selection": patch
---

Bump iTwin.js dependencies to `^5.5.0`.
7 changes: 7 additions & 0 deletions .changeset/smooth-states-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@itwin/presentation-components": patch
---

Fix `ContentDataProvider.getFieldByPropertyDescription` not finding array item and struct member fields.

The fix requires the `@itwin/presentation-common` peer dependency to be at least version `5.6.0`.
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import {
insertSpatialCategory,
} from "presentation-test-utilities";
import * as sinon from "sinon";
import { PrimitiveValue, PropertyRecord } from "@itwin/appui-abstract";
import { PrimitiveValue, PropertyDescription, PropertyRecord, PropertyValue, PropertyValueFormat } from "@itwin/appui-abstract";
import { PropertyCategory } from "@itwin/components-react";
import { InstanceKey, KeySet, RuleTypes } from "@itwin/presentation-common";
import { assert } from "@itwin/core-bentley";
import { ArrayPropertiesField, combineFieldNames, InstanceKey, KeySet, PropertiesField, RuleTypes, StructPropertiesField } from "@itwin/presentation-common";
import { DEFAULT_PROPERTY_GRID_RULESET, PresentationPropertyDataProvider, PresentationPropertyDataProviderProps } from "@itwin/presentation-components";
import { Presentation } from "@itwin/presentation-frontend";
import { buildTestIModel } from "@itwin/presentation-testing";
import { buildIModel, importSchema } from "../../IModelUtils.js";
import { initialize, terminate } from "../../IntegrationTests.js";

describe("PropertyDataProvider", async () => {
Expand Down Expand Up @@ -75,19 +77,35 @@ describe("PropertyDataProvider", async () => {
validateRecords(properties.records["/selected-item/"], [
{
propName: "CodeValue",
valueComparer: (value) => expect(value.value).to.be.undefined,
valueComparer: (value) =>
expect(value).to.containSubset({
valueFormat: PropertyValueFormat.Primitive,
value: undefined,
}),
},
{
propName: "UserLabel",
valueComparer: (value) => expect(value.value).to.be.eq("My Element"),
valueComparer: (value) =>
expect(value).to.containSubset({
valueFormat: PropertyValueFormat.Primitive,
value: "My Element",
}),
},
{
propName: "Model",
valueComparer: (value) => expect((value.value as InstanceKey).id).to.be.eq(modelKey.id),
valueComparer: (value) =>
expect(value).to.containSubset({
valueFormat: PropertyValueFormat.Primitive,
value: { id: modelKey.id },
}),
},
{
propName: "Category",
valueComparer: (value) => expect((value.value as InstanceKey).id).to.be.eq(categoryKey.id),
valueComparer: (value) =>
expect(value).to.containSubset({
valueFormat: PropertyValueFormat.Primitive,
value: { id: categoryKey.id },
}),
},
]);
});
Expand Down Expand Up @@ -115,19 +133,35 @@ describe("PropertyDataProvider", async () => {
validateRecords(properties.records["/selected-item/"], [
{
propName: "CodeValue",
valueComparer: (value) => expect(value.value).to.be.undefined,
valueComparer: (value) =>
expect(value).to.containSubset({
valueFormat: PropertyValueFormat.Primitive,
value: undefined,
}),
},
{
propName: "UserLabel",
valueComparer: (value) => expect(value.value).to.be.eq("My Element"),
valueComparer: (value) =>
expect(value).to.containSubset({
valueFormat: PropertyValueFormat.Primitive,
value: "My Element",
}),
},
{
propName: "Model",
valueComparer: (value) => expect((value.value as InstanceKey).id).to.be.eq(modelKey.id),
valueComparer: (value) =>
expect(value).to.containSubset({
valueFormat: PropertyValueFormat.Primitive,
value: { id: modelKey.id },
}),
},
{
propName: "Category",
valueComparer: (value) => expect((value.value as InstanceKey).id).to.be.eq(categoryKey.id),
valueComparer: (value) =>
expect(value).to.containSubset({
valueFormat: PropertyValueFormat.Primitive,
value: { id: categoryKey.id },
}),
},
]);
});
Expand Down Expand Up @@ -289,6 +323,190 @@ describe("PropertyDataProvider", async () => {
runTests("with flat property categories", (provider) => (provider.isNestedPropertyCategoryGroupingEnabled = false));
runTests("with nested property categories", (provider) => (provider.isNestedPropertyCategoryGroupingEnabled = true));

it("finds array item & struct member fields", async function () {
const { imodel, ...keys } = await buildIModel(this, async (builder, mochaContext) => {
const schema = await importSchema(
mochaContext,
builder,
`
<ECSchemaReference name="BisCore" version="01.00.16" alias="bis" />
<ECStructClass typeName="TestStruct">
<ECProperty propertyName="StringMember" typeName="string" />
<ECProperty propertyName="NumericMember" typeName="int" />
</ECStructClass>
<ECEntityClass typeName="TestPhysicalObject">
<BaseClass>bis:PhysicalElement</BaseClass>
<ECArrayProperty propertyName="ArrayProperty" typeName="string" />
<ECStructProperty propertyName="StructProperty" typeName="TestStruct" />
<ECStructArrayProperty propertyName="StructArrayProperty" typeName="TestStruct" />
</ECEntityClass>
`,
);
const categoryKey = insertSpatialCategory({ builder, fullClassNameSeparator: ":", codeValue: "My Category" });
const modelKey = insertPhysicalModelWithPartition({ builder, fullClassNameSeparator: ":", codeValue: "My Model" });
const elementKey = insertPhysicalElement({
builder,
classFullName: `${schema.schemaAlias}:TestPhysicalObject`,
userLabel: "Test element",
modelId: modelKey.id,
categoryId: categoryKey.id,
ArrayProperty: ["Item 1", "Item 2"],
StructProperty: { StringMember: "Test string", NumericMember: 123 },
StructArrayProperty: [
{ StringMember: "Item 1", NumericMember: 456 },
{ StringMember: "Item 2", NumericMember: 789 },
],
});
return { element: elementKey };
});

using provider = new PresentationPropertyDataProvider({ imodel });
provider.keys = new KeySet([keys.element]);
const properties = await provider.getData();

// ensure we get what we expect
validateRecords(properties.records["/selected-item/"], [
{
propName: "ArrayProperty",
valueComparer: (value, property) => {
assert(value.valueFormat === PropertyValueFormat.Array);
expect(value.itemsTypeName).to.eq("string");
expect(value.items)
.to.have.lengthOf(2)
.and.to.containSubset([
{ property: { name: combineFieldNames("[*]", property.name), typename: "string" }, value: { value: "Item 1" } },
{ property: { name: combineFieldNames("[*]", property.name), typename: "string" }, value: { value: "Item 2" } },
]);
},
},
{
propName: "StructProperty",
valueComparer: (value, property) => {
assert(value.valueFormat === PropertyValueFormat.Struct);
expect(value.members).and.to.containSubset({
StringMember: {
property: { name: combineFieldNames("StringMember", property.name), typename: "string" },
value: { value: "Test string" },
},
NumericMember: {
property: { name: combineFieldNames("NumericMember", property.name), typename: "int" },
value: { value: 123 },
},
});
},
},
{
propName: "StructArrayProperty",
valueComparer: (value, property) => {
assert(value.valueFormat === PropertyValueFormat.Array);
expect(value.itemsTypeName).to.eq("TestStruct");
expect(value.items)
.to.have.lengthOf(2)
.and.to.containSubset([
{
property: { name: combineFieldNames("[*]", property.name), typename: "TestStruct" },
value: {
valueFormat: PropertyValueFormat.Struct,
members: {
StringMember: {
property: { name: combineFieldNames("StringMember", combineFieldNames("[*]", property.name)), typename: "string" },
value: { value: "Item 1" },
},
NumericMember: {
property: { name: combineFieldNames("NumericMember", combineFieldNames("[*]", property.name)), typename: "int" },
value: { value: 456 },
},
},
},
},
{
property: { name: combineFieldNames("[*]", property.name), typename: "TestStruct" },
value: {
valueFormat: PropertyValueFormat.Struct,
members: {
StringMember: {
property: { name: combineFieldNames("StringMember", combineFieldNames("[*]", property.name)), typename: "string" },
value: { value: "Item 2" },
},
NumericMember: {
property: { name: combineFieldNames("NumericMember", combineFieldNames("[*]", property.name)), typename: "int" },
value: { value: 789 },
},
},
},
},
]);
},
},
]);

// test retrieving array items field
const arrayRecord = properties.records["/selected-item/"].find((r) => r.property.name.endsWith("ArrayProperty"));
assert(arrayRecord?.value.valueFormat === PropertyValueFormat.Array);
const arrayItemRecord = arrayRecord.value.items[0];
const arrayItemField = (await provider.getFieldByPropertyDescription(arrayItemRecord.property)) as PropertiesField;
expect(arrayItemField).to.containSubset({
name: "[*]",
label: "ArrayProperty",
});
expect(arrayItemField.parentArrayField)
.to.be.instanceOf(ArrayPropertiesField)
.and.to.containSubset({
label: "ArrayProperty",
type: {
valueFormat: "Array",
typeName: "string[]",
},
});

// test retrieving struct member field
const structRecord = properties.records["/selected-item/"].find((r) => r.property.name.endsWith("StructProperty"));
assert(structRecord?.value.valueFormat === PropertyValueFormat.Struct);
const structMemberRecord = structRecord.value.members.StringMember;
const structMemberField = (await provider.getFieldByPropertyDescription(structMemberRecord.property)) as PropertiesField;
expect(structMemberField).to.containSubset({
name: "StringMember",
});
expect(structMemberField.parentStructField)
.to.be.instanceOf(StructPropertiesField)
.and.to.containSubset({
label: "StructProperty",
type: {
valueFormat: "Struct",
typeName: "TestStruct",
},
});

// test retrieving struct array member field
const structArrayRecord = properties.records["/selected-item/"].find((r) => r.property.name.endsWith("StructArrayProperty"));
assert(structArrayRecord?.value.valueFormat === PropertyValueFormat.Array);
const structArrayItemRecord = structArrayRecord.value.items[0];
assert(structArrayItemRecord?.value.valueFormat === PropertyValueFormat.Struct);
const structArrayItemMemberRecord = structArrayItemRecord.value.members.StringMember;
const structArrayMemberField = (await provider.getFieldByPropertyDescription(structArrayItemMemberRecord.property)) as PropertiesField;
expect(structArrayMemberField).to.containSubset({
name: "StringMember",
});
expect(structArrayMemberField.parentStructField)
.to.be.instanceOf(StructPropertiesField)
.and.to.containSubset({
label: "StructArrayProperty",
type: {
valueFormat: "Struct",
typeName: "TestStruct",
},
});
expect((structArrayMemberField.parentStructField as StructPropertiesField).parentArrayField)
.to.be.instanceOf(ArrayPropertiesField)
.and.to.containSubset({
label: "StructArrayProperty",
type: {
valueFormat: "Array",
typeName: "TestStruct[]",
},
});
});

it("gets property data after re-initializing Presentation", async function () {
let categoryKey: InstanceKey;
// eslint-disable-next-line @typescript-eslint/no-deprecated
Expand Down Expand Up @@ -318,12 +536,15 @@ describe("PropertyDataProvider", async () => {
});
});

function validateRecords(records: PropertyRecord[], expectations: Array<{ propName: string; valueComparer?: (value: PrimitiveValue) => void }>) {
function validateRecords(
records: PropertyRecord[],
expectations: Array<{ propName: string; valueComparer?: (value: PropertyValue, property: PropertyDescription) => void }>,
) {
for (const { propName, valueComparer } of expectations) {
const record = records.find((rec) => rec.property.name.endsWith(propName));
if (!record) {
throw new Error(`Failed to find PropertyRecord for property - ${propName}`);
}
valueComparer?.(record.value as PrimitiveValue);
valueComparer?.(record.value, record.property);
}
}
19 changes: 15 additions & 4 deletions packages/components/src/presentation-components/common/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,25 @@ export const getDisplayName = <P>(component: React.ComponentType<P>): string =>
* @internal
*/
export const findField = (descriptor: Descriptor, recordPropertyName: string): Field | undefined => {
let fieldsSource: { getFieldByName: (name: string) => Field | undefined } | undefined = descriptor;
// note: define `fieldsSource` as an object with optional `getFieldByName` method, because some field sources received this
// method later than our minimum required version of `@itwin/presentation-common`
let fieldsSource: { getFieldByName?: (name: string) => Field | undefined } = descriptor;
const fieldNames = parseCombinedFieldNames(recordPropertyName);
while (fieldsSource && fieldNames.length) {
const field: Field | undefined = fieldsSource.getFieldByName(fieldNames.shift()!);
fieldsSource = field && field.isNestedContentField() ? field : undefined;
while (fieldNames.length) {
const field: Field | undefined = fieldsSource.getFieldByName?.(fieldNames.shift()!);
if (!fieldNames.length) {
return field;
}
if (!field) {
return undefined;
}
if (field.isNestedContentField()) {
fieldsSource = field;
} else if (field.isPropertiesField() && (field.isStructPropertiesField?.() || field.isArrayPropertiesField?.())) {
fieldsSource = field;
} else {
return undefined;
}
}
return undefined;
};
Expand Down
Loading