diff --git a/common/api/presentation-common.api.md b/common/api/presentation-common.api.md index 542890bec48..e0781318466 100644 --- a/common/api/presentation-common.api.md +++ b/common/api/presentation-common.api.md @@ -48,6 +48,14 @@ export interface ArrayPropertiesFieldJSON extends Pr itemsField: PropertiesFieldJSON; } +// @public +export interface ArrayPropertyValueConstraints { + // (undocumented) + maxOccurs?: number; + // (undocumented) + minOccurs?: number; +} + // @public export interface ArrayTypeDescription extends BaseTypeDescription { memberType: TypeDescription; @@ -2122,6 +2130,14 @@ export interface NodeUpdateInfoJSON { type: "Update"; } +// @public +export interface NumericPropertyValueConstraints { + // (undocumented) + maximumValue?: number; + // (undocumented) + minimumValue?: number; +} + // @public type Omit_2 = Pick>; export { Omit_2 as Omit } @@ -2464,6 +2480,7 @@ export enum PropertyGroupingValue { // @public export interface PropertyInfo { classInfo: ClassInfo; + constraints?: PropertyValueConstraints; enumerationInfo?: EnumerationInfo; extendedType?: string; kindOfQuantity?: KindOfQuantityInfo; @@ -2538,6 +2555,9 @@ export interface PropertySpecification extends PropertyOverrides { name: string; } +// @public +export type PropertyValueConstraints = StringPropertyValueConstraints | ArrayPropertyValueConstraints | NumericPropertyValueConstraints; + // @public export enum PropertyValueFormat { Array = "Array", @@ -3111,6 +3131,14 @@ export interface StartStructProps { valueType: TypeDescription; } +// @public +export interface StringPropertyValueConstraints { + // (undocumented) + maximumLength?: number; + // (undocumented) + minimumLength?: number; +} + // @public export interface StringQuerySpecification extends QuerySpecificationBase { query: string; diff --git a/common/api/summary/presentation-common.exports.csv b/common/api/summary/presentation-common.exports.csv index c6d4540f9b1..13518217075 100644 --- a/common/api/summary/presentation-common.exports.csv +++ b/common/api/summary/presentation-common.exports.csv @@ -3,6 +3,7 @@ Release Tag;API Item Type;API Item Name public;function;addFieldHierarchy public;class;ArrayPropertiesField public;interface;ArrayPropertiesFieldJSON +public;interface;ArrayPropertyValueConstraints public;interface;ArrayTypeDescription internal;class;AsyncTasksTracker public;interface;BaseFieldJSON @@ -297,6 +298,7 @@ deprecated;interface;NodePathFilteringDataJSON public;interface;NodeUpdateInfo public;interface;NodeUpdateInfoJSON deprecated;interface;NodeUpdateInfoJSON +public;interface;NumericPropertyValueConstraints public;type;Paged public;interface;PagedResponse public;interface;PageOptions @@ -354,6 +356,7 @@ public;interface;PropertyOverrides public;interface;PropertyRangeGroupSpecification public;interface;PropertySortingRule public;interface;PropertySpecification +public;type;PropertyValueConstraints public;enum;PropertyValueFormat public;type;QuerySpecification public;interface;QuerySpecificationBase @@ -428,6 +431,7 @@ public;interface;StartContentProps public;interface;StartFieldProps public;interface;StartItemProps public;interface;StartStructProps +public;interface;StringPropertyValueConstraints public;interface;StringQuerySpecification public;interface;StringRulesetVariable public;interface;StringRulesetVariableJSON diff --git a/common/changes/@itwin/core-backend/JonasD-property-coinstraints_2025-01-15-10-30.json b/common/changes/@itwin/core-backend/JonasD-property-coinstraints_2025-01-15-10-30.json new file mode 100644 index 00000000000..99b35bb89b6 --- /dev/null +++ b/common/changes/@itwin/core-backend/JonasD-property-coinstraints_2025-01-15-10-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-backend", + "comment": "", + "type": "none" + } + ], + "packageName": "@itwin/core-backend" +} \ No newline at end of file diff --git a/common/changes/@itwin/presentation-common/JonasD-property-coinstraints_2025-01-10-13-51.json b/common/changes/@itwin/presentation-common/JonasD-property-coinstraints_2025-01-10-13-51.json new file mode 100644 index 00000000000..9cd8a3f555c --- /dev/null +++ b/common/changes/@itwin/presentation-common/JonasD-property-coinstraints_2025-01-10-13-51.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/presentation-common", + "comment": "Add value constraints to `PropertyInfo`", + "type": "none" + } + ], + "packageName": "@itwin/presentation-common" +} \ No newline at end of file diff --git a/full-stack-tests/presentation/src/frontend/content/PrimitiveProperties.test.ts b/full-stack-tests/presentation/src/frontend/content/PrimitiveProperties.test.ts new file mode 100644 index 00000000000..babd526a1dc --- /dev/null +++ b/full-stack-tests/presentation/src/frontend/content/PrimitiveProperties.test.ts @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from "chai"; +import { assert, Guid, using } from "@itwin/core-bentley"; +import { IModelConnection } from "@itwin/core-frontend"; +import { Content, ContentSpecificationTypes, DefaultContentDisplayTypes, InstanceKey, KeySet, Ruleset, RuleTypes } from "@itwin/presentation-common"; +import { PresentationManager } from "@itwin/presentation-frontend"; +import { + buildTestIModelConnection, + importSchema, + insertPhysicalElement, + insertPhysicalModelWithPartition, + insertSpatialCategory, +} from "../../IModelSetupUtils"; +import { collect, getFieldByLabel } from "../../Utils"; +import { describeContentTestSuite } from "./Utils"; + +describeContentTestSuite("Primitive properties", () => { + it("sets constraints for numeric type properties", async function () { + let elementKey!: InstanceKey; + const imodel = await buildTestIModelConnection(this.test!.title, async (db) => { + const schema = importSchema( + this, + db, + ` + + + bis:PhysicalElement + + + + + + `, + ); + const model = insertPhysicalModelWithPartition({ db, codeValue: "model" }); + const category = insertSpatialCategory({ db, codeValue: "category" }); + elementKey = insertPhysicalElement({ + db, + classFullName: schema.items.X.fullName, + modelId: model.id, + categoryId: category.id, + }); + }); + + const content = await getContent(imodel, elementKey); + const field1 = getFieldByLabel(content.descriptor.fields, "Prop1"); + const field2 = getFieldByLabel(content.descriptor.fields, "Prop2"); + const field3 = getFieldByLabel(content.descriptor.fields, "Prop3"); + const field4 = getFieldByLabel(content.descriptor.fields, "Prop4"); + + assert(field1.isPropertiesField()); + assert(field2.isPropertiesField()); + assert(field3.isPropertiesField()); + assert(field4.isPropertiesField()); + + expect(field1.properties[0].property.constraints).to.be.undefined; + expect(field2.properties[0].property.constraints).to.deep.eq({ minimumValue: 0, maximumValue: 2 }); + expect(field3.properties[0].property.constraints).to.deep.eq({ minimumValue: 123456789876 }); + expect(field4.properties[0].property.constraints).to.deep.eq({ maximumValue: 2.6 }); + }); + + it("sets constraints for string type properties", async function () { + let elementKey!: InstanceKey; + const imodel = await buildTestIModelConnection(this.test!.title, async (db) => { + const schema = importSchema( + this, + db, + ` + + + bis:PhysicalElement + + + + + + `, + ); + const model = insertPhysicalModelWithPartition({ db, codeValue: "model" }); + const category = insertSpatialCategory({ db, codeValue: "category" }); + elementKey = insertPhysicalElement({ + db, + classFullName: schema.items.X.fullName, + modelId: model.id, + categoryId: category.id, + }); + }); + + const content = await getContent(imodel, elementKey); + const field1 = getFieldByLabel(content.descriptor.fields, "Prop1"); + const field2 = getFieldByLabel(content.descriptor.fields, "Prop2"); + const field3 = getFieldByLabel(content.descriptor.fields, "Prop3"); + const field4 = getFieldByLabel(content.descriptor.fields, "Prop4"); + + assert(field1.isPropertiesField()); + assert(field2.isPropertiesField()); + assert(field3.isPropertiesField()); + assert(field4.isPropertiesField()); + + expect(field1.properties[0].property.constraints).to.be.undefined; + expect(field2.properties[0].property.constraints).to.deep.eq({ minimumLength: 1, maximumLength: 5 }); + expect(field3.properties[0].property.constraints).to.deep.eq({ minimumLength: 1 }); + expect(field4.properties[0].property.constraints).to.deep.eq({ maximumLength: 5 }); + }); + + it("sets constraints for array type properties", async function () { + let elementKey!: InstanceKey; + const imodel = await buildTestIModelConnection(this.test!.title, async (db) => { + const schema = importSchema( + this, + db, + ` + + + bis:PhysicalElement + + + + + + `, + ); + const model = insertPhysicalModelWithPartition({ db, codeValue: "model" }); + const category = insertSpatialCategory({ db, codeValue: "category" }); + elementKey = insertPhysicalElement({ + db, + classFullName: schema.items.X.fullName, + modelId: model.id, + categoryId: category.id, + }); + }); + + const content = await getContent(imodel, elementKey); + const field1 = getFieldByLabel(content.descriptor.fields, "Prop1"); + const field2 = getFieldByLabel(content.descriptor.fields, "Prop2"); + const field3 = getFieldByLabel(content.descriptor.fields, "Prop3"); + const field4 = getFieldByLabel(content.descriptor.fields, "Prop4"); + + assert(field1.isPropertiesField()); + assert(field2.isPropertiesField()); + assert(field3.isPropertiesField()); + assert(field4.isPropertiesField()); + + // ECArrayProperty doesn't have a way to determine if minOccurs is defined. By default minOccurs is set to 0. + // If maxOccurs is set to "unbounded" then maxOccurs is set to undefined. + expect(field1.properties[0].property.constraints).to.deep.eq({ minOccurs: 0 }); + expect(field2.properties[0].property.constraints).to.deep.eq({ minOccurs: 1 }); + expect(field3.properties[0].property.constraints).to.deep.eq({ minOccurs: 1 }); + expect(field4.properties[0].property.constraints).to.deep.eq({ minOccurs: 0, maxOccurs: 5 }); + }); +}); + +async function getContent(imodel: IModelConnection, key: InstanceKey): Promise { + const keys = new KeySet([key]); + const ruleset: Ruleset = { + id: Guid.createValue(), + rules: [ + { + ruleType: RuleTypes.Content, + specifications: [{ specType: ContentSpecificationTypes.SelectedNodeInstances }], + }, + ], + }; + + return using(PresentationManager.create(), async (manager) => { + const descriptor = await manager.getContentDescriptor({ + imodel, + rulesetOrId: ruleset, + keys, + displayType: DefaultContentDisplayTypes.Grid, + }); + expect(descriptor).to.not.be.undefined; + const content = await manager + .getContentIterator({ imodel, rulesetOrId: ruleset, keys, descriptor: descriptor! }) + .then(async (x) => x && new Content(x.descriptor, await collect(x.items))); + expect(content).to.not.be.undefined; + return content!; + }); +} diff --git a/presentation/common/src/presentation-common/EC.ts b/presentation/common/src/presentation-common/EC.ts index 36bb3d26d41..7c0b8d51e2e 100644 --- a/presentation/common/src/presentation-common/EC.ts +++ b/presentation/common/src/presentation-common/EC.ts @@ -267,6 +267,41 @@ export interface PropertyInfo { extendedType?: string; /** Navigation property info if the field is navigation type */ navigationPropertyInfo?: NavigationPropertyInfo; + /** Constraints for values of ECProperty */ + constraints?: PropertyValueConstraints; +} + +/** + * Constraints for values of ECProperty + * @public + */ +export type PropertyValueConstraints = StringPropertyValueConstraints | ArrayPropertyValueConstraints | NumericPropertyValueConstraints; + +/** + * Describes constraints for `string` type ECProperty values + * @public + */ +export interface StringPropertyValueConstraints { + minimumLength?: number; + maximumLength?: number; +} + +/** + * Describes constraints for `int` | `double` | `float` type ECProperty values + * @public + */ +export interface NumericPropertyValueConstraints { + minimumValue?: number; + maximumValue?: number; +} + +/** + * Describes constraints for `array` type ECProperty values + * @public + */ +export interface ArrayPropertyValueConstraints { + minOccurs?: number; + maxOccurs?: number; } /** @public */