diff --git a/common/api/presentation-backend.api.md b/common/api/presentation-backend.api.md index 18226f13e467..31f72e00f53a 100644 --- a/common/api/presentation-backend.api.md +++ b/common/api/presentation-backend.api.md @@ -4,6 +4,7 @@ ```ts +import { ComputeSelectionRequestOptions } from '@itwin/presentation-common'; import { Content } from '@itwin/presentation-common'; import { ContentDescriptorRequestOptions } from '@itwin/presentation-common'; import { ContentRequestOptions } from '@itwin/presentation-common'; @@ -179,6 +180,8 @@ export class PresentationManager { ids: Id64String[]; scopeId: string; }): Promise; + // @alpha (undocumented) + computeSelection(requestOptions: ComputeSelectionRequestOptions): Promise; dispose(): void; getContent(requestOptions: Prioritized>>): Promise; getContentDescriptor(requestOptions: Prioritized>): Promise; diff --git a/common/api/presentation-common.api.md b/common/api/presentation-common.api.md index 54140cabe4e4..5f3fdfb40138 100644 --- a/common/api/presentation-common.api.md +++ b/common/api/presentation-common.api.md @@ -236,6 +236,17 @@ export interface CompressedClassInfoJSON { // @public export type ComputeDisplayValueCallback = (type: string, value: PrimitivePropertyValue, displayValue: string) => Promise; +// @alpha +export interface ComputeSelectionRequestOptions extends RequestOptions { + // (undocumented) + elementIds: Id64String[]; + // (undocumented) + scope: SelectionScopeProps; +} + +// @alpha (undocumented) +export type ComputeSelectionRpcRequestOptions = PresentationRpcRequestOptions>; + // @public @deprecated export interface ConditionContainer { condition?: string; @@ -796,6 +807,14 @@ export interface ElementPropertiesStructPropertyItem extends ElementPropertiesPr type: "struct"; } +// @alpha (undocumented) +export interface ElementSelectionScopeProps { + // (undocumented) + ancestorLevel?: number; + // (undocumented) + id: "element"; +} + // @public export interface EnumerationChoice { label: string; @@ -1301,6 +1320,9 @@ export interface IntsRulesetVariableJSON extends RulesetVariableBaseJSON { value: number[]; } +// @internal (undocumented) +export function isComputeSelectionRequestOptions(options: ComputeSelectionRequestOptions | SelectionScopeRequestOptions): options is ComputeSelectionRequestOptions; + // @beta export function isSingleElementPropertiesRequestOptions(options: ElementPropertiesRequestOptions): options is SingleElementPropertiesRequestOptions; @@ -1915,6 +1937,8 @@ export interface PresentationIpcInterface { export class PresentationRpcInterface extends RpcInterface { // (undocumented) computeSelection(_token: IModelRpcProps, _options: SelectionScopeRpcRequestOptions, _ids: Id64String[], _scopeId: string): PresentationRpcResponse; + // @alpha (undocumented) + computeSelection(_token: IModelRpcProps, _options: ComputeSelectionRpcRequestOptions): PresentationRpcResponse; // (undocumented) getContentDescriptor(_token: IModelRpcProps, _options: ContentDescriptorRpcRequestOptions): PresentationRpcResponse; // @beta (undocumented) @@ -2460,7 +2484,7 @@ export class RpcRequestsHandler implements IDisposable { constructor(props?: RpcRequestsHandlerProps); readonly clientId: string; // (undocumented) - computeSelection(options: SelectionScopeRequestOptions, ids: Id64String[], scopeId: string): Promise; + computeSelection(options: ComputeSelectionRequestOptions): Promise; // (undocumented) dispose(): void; // (undocumented) @@ -2686,6 +2710,11 @@ export interface SelectionScope { label: string; } +// @alpha (undocumented) +export type SelectionScopeProps = ElementSelectionScopeProps | { + id: string; +}; + // @public export interface SelectionScopeRequestOptions extends RequestOptions { } diff --git a/common/api/presentation-frontend.api.md b/common/api/presentation-frontend.api.md index 28425664312c..bc15c71ab7d5 100644 --- a/common/api/presentation-frontend.api.md +++ b/common/api/presentation-frontend.api.md @@ -48,6 +48,7 @@ import { Ruleset } from '@itwin/presentation-common'; import { RulesetVariable } from '@itwin/presentation-common'; import { SelectClassInfo } from '@itwin/presentation-common'; import { SelectionScope } from '@itwin/presentation-common'; +import { SelectionScopeProps } from '@itwin/presentation-common'; import { SetRulesetVariableParams } from '@itwin/presentation-common'; import { SingleElementPropertiesRequestOptions } from '@itwin/presentation-common'; import { UnitSystemKey } from '@itwin/core-quantity'; @@ -89,6 +90,9 @@ export function createFavoritePropertiesStorage(type: DefaultFavoritePropertiesS // @internal (undocumented) export const createFieldOrderInfos: (field: Field) => FavoritePropertiesOrderInfo[]; +// @internal +export function createSelectionScopeProps(scope: SelectionScopeProps | SelectionScope | string | undefined): SelectionScopeProps; + // @public export enum DefaultFavoritePropertiesStorageTypes { BrowserLocalStorage = 1, @@ -152,7 +156,7 @@ export enum FavoritePropertiesScope { // @internal (undocumented) export const getFieldInfos: (field: Field) => Set; -// @public +// @public @deprecated export function getScopeId(scope: SelectionScope | string | undefined): string; // @internal (undocumented) @@ -498,7 +502,7 @@ export class SelectionHelper { export class SelectionManager implements ISelectionProvider { constructor(props: SelectionManagerProps); addToSelection(source: string, imodel: IModelConnection, keys: Keys, level?: number, rulesetId?: string): void; - addToSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScope | string, level?: number, rulesetId?: string): Promise; + addToSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScopeProps | SelectionScope | string, level?: number, rulesetId?: string): Promise; clearSelection(source: string, imodel: IModelConnection, level?: number, rulesetId?: string): void; getHiliteSet(imodel: IModelConnection): Promise; getSelection(imodel: IModelConnection, level?: number): Readonly; @@ -506,9 +510,9 @@ export class SelectionManager implements ISelectionProvider { // @internal (undocumented) getToolSelectionSyncHandler(imodel: IModelConnection): ToolSelectionSyncHandler | undefined; removeFromSelection(source: string, imodel: IModelConnection, keys: Keys, level?: number, rulesetId?: string): void; - removeFromSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScope | string, level?: number, rulesetId?: string): Promise; + removeFromSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScopeProps | SelectionScope | string, level?: number, rulesetId?: string): Promise; replaceSelection(source: string, imodel: IModelConnection, keys: Keys, level?: number, rulesetId?: string): void; - replaceSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScope | string, level?: number, rulesetId?: string): Promise; + replaceSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScopeProps | SelectionScope | string, level?: number, rulesetId?: string): Promise; readonly scopes: SelectionScopesManager; readonly selectionChange: SelectionChangeEvent; setSyncWithIModelToolSelection(imodel: IModelConnection, sync?: boolean): void; @@ -524,9 +528,9 @@ export interface SelectionManagerProps { export class SelectionScopesManager { constructor(props: SelectionScopesManagerProps); get activeLocale(): string | undefined; - get activeScope(): SelectionScope | string | undefined; - set activeScope(scope: SelectionScope | string | undefined); - computeSelection(imodel: IModelConnection, ids: Id64Arg, scope: SelectionScope | string): Promise; + get activeScope(): SelectionScopeProps | SelectionScope | string | undefined; + set activeScope(scope: SelectionScopeProps | SelectionScope | string | undefined); + computeSelection(imodel: IModelConnection, ids: Id64Arg, scope: SelectionScopeProps | SelectionScope | string): Promise; getSelectionScopes(imodel: IModelConnection, locale?: string): Promise; } diff --git a/common/api/summary/presentation-common.exports.csv b/common/api/summary/presentation-common.exports.csv index 51692c7e6262..731e03ef480d 100644 --- a/common/api/summary/presentation-common.exports.csv +++ b/common/api/summary/presentation-common.exports.csv @@ -29,6 +29,8 @@ public;ClassInfoJSON internal;CommonIpcParams public;CompressedClassInfoJSON public;ComputeDisplayValueCallback = (type: string, value: PrimitivePropertyValue, displayValue: string) => Promise +alpha;ComputeSelectionRequestOptions +alpha;ComputeSelectionRpcRequestOptions = PresentationRpcRequestOptions public;ConditionContainer deprecated;ConditionContainer public;Content @@ -111,6 +113,7 @@ beta;ElementPropertiesPropertyValueType = "primitive" | "array" | "struct" beta;ElementPropertiesRequestOptions beta;ElementPropertiesStructArrayPropertyItem beta;ElementPropertiesStructPropertyItem +alpha;ElementSelectionScopeProps public;EnumerationChoice public;EnumerationInfo alpha;ExpandedNodeUpdateRecord @@ -178,6 +181,7 @@ public;IntRulesetVariable public;IntRulesetVariableJSON public;IntsRulesetVariable public;IntsRulesetVariableJSON +internal;isComputeSelectionRequestOptions beta;isSingleElementPropertiesRequestOptions public;Item public;ItemJSON @@ -350,6 +354,7 @@ public;SelectClassInfoJSON public;SelectedNodeInstancesSpecification public;SelectionInfo public;SelectionScope +alpha;SelectionScopeProps = ElementSelectionScopeProps | public;SelectionScopeRequestOptions public;SelectionScopeRpcRequestOptions = PresentationRpcRequestOptions internal;SetRulesetVariableParams diff --git a/common/api/summary/presentation-frontend.exports.csv b/common/api/summary/presentation-frontend.exports.csv index 45702f3aea95..a24126e9fc82 100644 --- a/common/api/summary/presentation-frontend.exports.csv +++ b/common/api/summary/presentation-frontend.exports.csv @@ -6,6 +6,7 @@ alpha;consoleDiagnosticsHandler(scopeLogs: DiagnosticsScopeLogs[]): void alpha;createCombinedDiagnosticsHandler(handlers: DiagnosticsHandler[]): (scopeLogs: DiagnosticsScopeLogs[]) => void public;createFavoritePropertiesStorage(type: DefaultFavoritePropertiesStorageTypes): IFavoritePropertiesStorage internal;createFieldOrderInfos: (field: Field) => FavoritePropertiesOrderInfo[] +internal;createSelectionScopeProps(scope: SelectionScopeProps | SelectionScope | string | undefined): SelectionScopeProps public;DefaultFavoritePropertiesStorageTypes internal;DEPRECATED_PROPERTIES_SETTING_NAMESPACE = "Properties" internal;FAVORITE_PROPERTIES_ORDER_INFO_SETTING_NAME = "FavoritePropertiesOrderInfo" @@ -16,6 +17,7 @@ public;FavoritePropertiesOrderInfo public;FavoritePropertiesScope internal;getFieldInfos: (field: Field) => Set public;getScopeId(scope: SelectionScope | string | undefined): string +deprecated;getScopeId(scope: SelectionScope | string | undefined): string internal;HILITE_RULESET: Ruleset public;HiliteSet public;HiliteSetProvider diff --git a/common/changes/@itwin/presentation-backend/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json b/common/changes/@itwin/presentation-backend/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json new file mode 100644 index 000000000000..ddfa180c7846 --- /dev/null +++ b/common/changes/@itwin/presentation-backend/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/presentation-backend", + "comment": "Add support for nth level element selection scopes", + "type": "none" + } + ], + "packageName": "@itwin/presentation-backend" +} \ No newline at end of file diff --git a/common/changes/@itwin/presentation-common/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json b/common/changes/@itwin/presentation-common/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json new file mode 100644 index 000000000000..29beb6dee1a6 --- /dev/null +++ b/common/changes/@itwin/presentation-common/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/presentation-common", + "comment": "Add support for nth level element selection scopes", + "type": "none" + } + ], + "packageName": "@itwin/presentation-common" +} \ No newline at end of file diff --git a/common/changes/@itwin/presentation-components/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json b/common/changes/@itwin/presentation-components/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json new file mode 100644 index 000000000000..b948b4b931af --- /dev/null +++ b/common/changes/@itwin/presentation-components/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/presentation-components", + "comment": "", + "type": "none" + } + ], + "packageName": "@itwin/presentation-components" +} \ No newline at end of file diff --git a/common/changes/@itwin/presentation-frontend/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json b/common/changes/@itwin/presentation-frontend/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json new file mode 100644 index 000000000000..01e76127d815 --- /dev/null +++ b/common/changes/@itwin/presentation-frontend/presentation-nth-level-element-selection-scope-on-master_2022-06-09-10-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/presentation-frontend", + "comment": "Add support for nth level element selection scopes", + "type": "none" + } + ], + "packageName": "@itwin/presentation-frontend" +} \ No newline at end of file diff --git a/full-stack-tests/presentation/src/frontend/SelectionScopes.test.ts b/full-stack-tests/presentation/src/frontend/SelectionScopes.test.ts index 0152b04bfcf0..ccbe26dd3363 100644 --- a/full-stack-tests/presentation/src/frontend/SelectionScopes.test.ts +++ b/full-stack-tests/presentation/src/frontend/SelectionScopes.test.ts @@ -41,6 +41,14 @@ describe("Selection Scopes", () => { expect(selection.has({ className: elementProps[0].classFullName, id: elementProps[0].id! })); }); + it("sets correct selection with 'element' 1st parent level selection scope", async () => { + const elementProps = await imodel.elements.getProps(Id64.fromUint32Pair(28, 0)); + await Presentation.selection.addToSelectionWithScope("", imodel, elementProps[0].id!, { id: "element", ancestorLevel: 1 }); + const selection = Presentation.selection.getSelection(imodel); + expect(selection.size).to.eq(1); + expect(selection.has({ className: "BisCore:Subject", id: Id64.fromUint32Pair(27, 0) })); + }); + it("sets correct selection with 'assembly' selection scope", async () => { const elementProps = await imodel.elements.getProps(Id64.fromUint32Pair(28, 0)); await Presentation.selection.addToSelectionWithScope("", imodel, elementProps[0].id!, "assembly"); @@ -49,6 +57,22 @@ describe("Selection Scopes", () => { expect(selection.has({ className: "BisCore:Subject", id: Id64.fromUint32Pair(27, 0) })); }); + it("sets correct selection with 'element' 2nd parent level selection scope", async () => { + const elementProps = await imodel.elements.getProps(Id64.fromUint32Pair(28, 0)); + await Presentation.selection.addToSelectionWithScope("", imodel, elementProps[0].id!, { id: "element", ancestorLevel: 2 }); + const selection = Presentation.selection.getSelection(imodel); + expect(selection.size).to.eq(1); + expect(selection.has({ className: "BisCore:Subject", id: Id64.fromUint32Pair(1, 0) })); + }); + + it("sets correct selection with 'element' exceeding parent level selection scope", async () => { + const elementProps = await imodel.elements.getProps(Id64.fromUint32Pair(28, 0)); + await Presentation.selection.addToSelectionWithScope("", imodel, elementProps[0].id!, { id: "element", ancestorLevel: 999 }); + const selection = Presentation.selection.getSelection(imodel); + expect(selection.size).to.eq(1); + expect(selection.has({ className: "BisCore:Subject", id: Id64.fromUint32Pair(1, 0) })); + }); + it("sets correct selection with 'top-assembly' selection scope", async () => { const elementProps = await imodel.elements.getProps(Id64.fromUint32Pair(28, 0)); await Presentation.selection.addToSelectionWithScope("", imodel, elementProps[0].id!, "top-assembly"); diff --git a/presentation/backend/src/presentation-backend/PresentationManager.ts b/presentation/backend/src/presentation-backend/PresentationManager.ts index a67aa716a9c9..faa15e1080c4 100644 --- a/presentation/backend/src/presentation-backend/PresentationManager.ts +++ b/presentation/backend/src/presentation-backend/PresentationManager.ts @@ -12,13 +12,13 @@ import { IModelDb, IModelJsNative, IpcHost } from "@itwin/core-backend"; import { Id64String } from "@itwin/core-bentley"; import { FormatProps, UnitSystemKey } from "@itwin/core-quantity"; import { - Content, ContentDescriptorRequestOptions, ContentFlags, ContentRequestOptions, ContentSourcesRequestOptions, DefaultContentDisplayTypes, Descriptor, - DescriptorOverrides, DiagnosticsOptionsWithHandler, DisplayLabelRequestOptions, DisplayLabelsRequestOptions, DisplayValueGroup, - DistinctValuesRequestOptions, ElementProperties, ElementPropertiesRequestOptions, FilterByInstancePathsHierarchyRequestOptions, + ComputeSelectionRequestOptions, Content, ContentDescriptorRequestOptions, ContentFlags, ContentRequestOptions, ContentSourcesRequestOptions, + DefaultContentDisplayTypes, Descriptor, DescriptorOverrides, DiagnosticsOptionsWithHandler, DisplayLabelRequestOptions, DisplayLabelsRequestOptions, + DisplayValueGroup, DistinctValuesRequestOptions, ElementProperties, ElementPropertiesRequestOptions, FilterByInstancePathsHierarchyRequestOptions, FilterByTextHierarchyRequestOptions, HierarchyCompareInfo, HierarchyCompareOptions, HierarchyRequestOptions, InstanceKey, - isSingleElementPropertiesRequestOptions, Key, KeySet, LabelDefinition, MultiElementPropertiesRequestOptions, Node, NodeKey, NodePathElement, Paged, - PagedResponse, PresentationError, PresentationStatus, Prioritized, Ruleset, SelectClassInfo, SelectionScope, SelectionScopeRequestOptions, - SingleElementPropertiesRequestOptions, + isComputeSelectionRequestOptions, isSingleElementPropertiesRequestOptions, Key, KeySet, LabelDefinition, MultiElementPropertiesRequestOptions, Node, + NodeKey, NodePathElement, Paged, PagedResponse, PresentationError, PresentationStatus, Prioritized, Ruleset, SelectClassInfo, SelectionScope, + SelectionScopeRequestOptions, SingleElementPropertiesRequestOptions, } from "@itwin/presentation-common"; import { PRESENTATION_BACKEND_ASSETS_ROOT, PRESENTATION_COMMON_ASSETS_ROOT } from "./Constants"; import { buildElementsProperties, getElementsCount, iterateElementIds } from "./ElementPropertiesHelper"; @@ -739,9 +739,17 @@ export class PresentationManager { * Computes selection set based on provided selection scope. * @public */ - public async computeSelection(requestOptions: SelectionScopeRequestOptions & { ids: Id64String[], scopeId: string }): Promise { - const { ids, scopeId, ...opts } = requestOptions; // eslint-disable-line @typescript-eslint/no-unused-vars - return SelectionScopesHelper.computeSelection(opts, ids, scopeId); + public async computeSelection(requestOptions: SelectionScopeRequestOptions & { ids: Id64String[], scopeId: string }): Promise; + /** @alpha */ + // eslint-disable-next-line @typescript-eslint/unified-signatures + public async computeSelection(requestOptions: ComputeSelectionRequestOptions): Promise; + public async computeSelection(requestOptions: (SelectionScopeRequestOptions & { ids: Id64String[], scopeId: string }) | ComputeSelectionRequestOptions): Promise { + return SelectionScopesHelper.computeSelection(isComputeSelectionRequestOptions(requestOptions) + ? requestOptions + : (function () { + const { ids, scopeId, ...rest } = requestOptions; + return { ...rest, elementIds: ids, scope: { id: scopeId } }; + })()); } private async request(params: TParams, reviver?: (key: string, value: any) => any): Promise { diff --git a/presentation/backend/src/presentation-backend/PresentationRpcImpl.ts b/presentation/backend/src/presentation-backend/PresentationRpcImpl.ts index 8e016a0366f0..6e3f90e2cf51 100644 --- a/presentation/backend/src/presentation-backend/PresentationRpcImpl.ts +++ b/presentation/backend/src/presentation-backend/PresentationRpcImpl.ts @@ -10,13 +10,14 @@ import { IModelDb } from "@itwin/core-backend"; import { assert, Id64String, IDisposable, Logger } from "@itwin/core-bentley"; import { IModelRpcProps } from "@itwin/core-common"; import { - ContentDescriptorRpcRequestOptions, ContentFlags, ContentInstanceKeysRpcRequestOptions, ContentRpcRequestOptions, ContentSourcesRpcRequestOptions, - ContentSourcesRpcResult, DescriptorJSON, DiagnosticsOptions, DiagnosticsScopeLogs, DisplayLabelRpcRequestOptions, DisplayLabelsRpcRequestOptions, - DisplayValueGroup, DisplayValueGroupJSON, DistinctValuesRpcRequestOptions, ElementProperties, FilterByInstancePathsHierarchyRpcRequestOptions, - FilterByTextHierarchyRpcRequestOptions, HierarchyRpcRequestOptions, InstanceKey, ItemJSON, KeySet, KeySetJSON, LabelDefinition, LabelDefinitionJSON, - Node, NodeJSON, NodeKey, NodeKeyJSON, NodePathElement, NodePathElementJSON, Paged, PagedResponse, PageOptions, PresentationError, - PresentationRpcInterface, PresentationRpcResponse, PresentationRpcResponseData, PresentationStatus, Ruleset, RulesetVariable, RulesetVariableJSON, - SelectClassInfo, SelectionScope, SelectionScopeRpcRequestOptions, SingleElementPropertiesRpcRequestOptions, + ComputeSelectionRpcRequestOptions, ContentDescriptorRpcRequestOptions, ContentFlags, ContentInstanceKeysRpcRequestOptions, ContentRpcRequestOptions, + ContentSourcesRpcRequestOptions, ContentSourcesRpcResult, DescriptorJSON, DiagnosticsOptions, DiagnosticsScopeLogs, DisplayLabelRpcRequestOptions, + DisplayLabelsRpcRequestOptions, DisplayValueGroup, DisplayValueGroupJSON, DistinctValuesRpcRequestOptions, ElementProperties, + FilterByInstancePathsHierarchyRpcRequestOptions, FilterByTextHierarchyRpcRequestOptions, HierarchyRpcRequestOptions, InstanceKey, + isComputeSelectionRequestOptions, ItemJSON, KeySet, KeySetJSON, LabelDefinition, LabelDefinitionJSON, Node, NodeJSON, NodeKey, NodeKeyJSON, + NodePathElement, NodePathElementJSON, Paged, PagedResponse, PageOptions, PresentationError, PresentationRpcInterface, PresentationRpcResponse, + PresentationRpcResponseData, PresentationStatus, Ruleset, RulesetVariable, RulesetVariableJSON, SelectClassInfo, SelectionScope, + SelectionScopeRpcRequestOptions, SingleElementPropertiesRpcRequestOptions, } from "@itwin/presentation-common"; import { PresentationBackendLoggerCategory } from "./BackendLoggerCategory"; import { Presentation } from "./Presentation"; @@ -358,8 +359,15 @@ export class PresentationRpcImpl extends PresentationRpcInterface implements IDi ); } - public override async computeSelection(token: IModelRpcProps, requestOptions: SelectionScopeRpcRequestOptions, ids: Id64String[], scopeId: string): PresentationRpcResponse { - return this.makeRequest(token, "computeSelection", { ...requestOptions, ids, scopeId }, async (options) => { + public override async computeSelection(token: IModelRpcProps, requestOptions: ComputeSelectionRpcRequestOptions | SelectionScopeRpcRequestOptions, ids?: Id64String[], scopeId?: string): PresentationRpcResponse { + return this.makeRequest(token, "computeSelection", requestOptions, async (options) => { + if (!isComputeSelectionRequestOptions(options)) { + options = { + ...options, + elementIds: ids!, + scope: { id: scopeId! }, + }; + } const keys = await this.getManager(requestOptions.clientId).computeSelection(options); return keys.toJSON(); }); diff --git a/presentation/backend/src/presentation-backend/SelectionScopesHelper.ts b/presentation/backend/src/presentation-backend/SelectionScopesHelper.ts index 50aef6fc9291..d10e2fdd3a6b 100644 --- a/presentation/backend/src/presentation-backend/SelectionScopesHelper.ts +++ b/presentation/backend/src/presentation-backend/SelectionScopesHelper.ts @@ -6,10 +6,11 @@ * @module Core */ -import { DbResult, Id64, Id64String } from "@itwin/core-bentley"; import { GeometricElement, GeometricElement3d, IModelDb } from "@itwin/core-backend"; +import { DbResult, Id64, Id64String } from "@itwin/core-bentley"; import { - InstanceKey, KeySet, PresentationError, PresentationStatus, SelectionScope, SelectionScopeRequestOptions, + ComputeSelectionRequestOptions, ElementSelectionScopeProps, InstanceKey, isComputeSelectionRequestOptions, KeySet, PresentationError, + PresentationStatus, SelectionScope, SelectionScopeRequestOptions, } from "@itwin/presentation-common"; import { getElementKey } from "./Utils"; @@ -40,66 +41,32 @@ export class SelectionScopesHelper { ]; } - private static computeElementSelection(requestOptions: SelectionScopeRequestOptions, ids: Id64String[]) { - const keys = new KeySet(); - ids.forEach(skipTransients((id) => { - const key = getElementKey(requestOptions.imodel, id); - if (key) - keys.add(key); - })); - return keys; - } - - private static getParentInstanceKey(imodel: IModelDb, id: Id64String): InstanceKey | undefined { - const elementProps = imodel.elements.tryGetElementProps(id); - if (!elementProps?.parent) - return undefined; - return getElementKey(imodel, elementProps.parent.id); - } - - private static getAssemblyKey(imodel: IModelDb, id: Id64String) { - const parentKey = this.getParentInstanceKey(imodel, id); - if (parentKey) - return parentKey; - return getElementKey(imodel, id); - } - - private static computeAssemblySelection(requestOptions: SelectionScopeRequestOptions, ids: Id64String[]) { - const parentKeys = new KeySet(); - ids.forEach(skipTransients((id) => { - const key = this.getAssemblyKey(requestOptions.imodel, id); - if (key) - parentKeys.add(key); - })); - return parentKeys; - } - - private static getTopAssemblyKey(imodel: IModelDb, id: Id64String) { - let currKey: InstanceKey | undefined; - let parentKey = this.getParentInstanceKey(imodel, id); - while (parentKey) { - currKey = parentKey; - parentKey = this.getParentInstanceKey(imodel, currKey.id); + private static getElementKey(iModel: IModelDb, elementId: Id64String, ancestorLevel: number) { + let currId = elementId; + let parentId = iModel.elements.tryGetElementProps(currId)?.parent?.id; + while (parentId && ancestorLevel !== 0) { + currId = parentId; + parentId = iModel.elements.tryGetElementProps(currId)?.parent?.id; + --ancestorLevel; } - return currKey ?? getElementKey(imodel, id); + return getElementKey(iModel, currId); } - private static computeTopAssemblySelection(requestOptions: SelectionScopeRequestOptions, ids: Id64String[]) { + private static computeElementSelection(iModel: IModelDb, elementIds: Id64String[], ancestorLevel: number) { const parentKeys = new KeySet(); - ids.forEach(skipTransients((id) => { - const key = this.getTopAssemblyKey(requestOptions.imodel, id); - if (key) - parentKeys.add(key); + elementIds.forEach(skipTransients((id) => { + const key = this.getElementKey(iModel, id, ancestorLevel); + key && parentKeys.add(key); })); return parentKeys; } - private static computeCategorySelection(requestOptions: SelectionScopeRequestOptions, ids: Id64String[]) { + private static computeCategorySelection(iModel: IModelDb, ids: Id64String[]) { const categoryKeys = new KeySet(); ids.forEach(skipTransients((id) => { - const el = requestOptions.imodel.elements.tryGetElement(id); + const el = iModel.elements.tryGetElement(id); if (el instanceof GeometricElement) { - const category = requestOptions.imodel.elements.tryGetElementProps(el.category); + const category = iModel.elements.tryGetElementProps(el.category); if (category) categoryKeys.add({ className: category.classFullName, id: category.id! }); } @@ -107,11 +74,11 @@ export class SelectionScopesHelper { return categoryKeys; } - private static computeModelSelection(requestOptions: SelectionScopeRequestOptions, ids: Id64String[]) { + private static computeModelSelection(iModel: IModelDb, ids: Id64String[]) { const modelKeys = new KeySet(); ids.forEach(skipTransients((id) => { - const el = requestOptions.imodel.elements.tryGetElementProps(id); - const model = el ? requestOptions.imodel.models.tryGetModelProps(el.model) : undefined; + const el = iModel.elements.tryGetElementProps(id); + const model = el ? iModel.models.tryGetModelProps(el.model) : undefined; if (model) modelKeys.add({ className: model.classFullName, id: model.id! }); })); @@ -147,7 +114,7 @@ export class SelectionScopesHelper { const relatedFunctionalKey = this.getRelatedFunctionalElementKey(imodel, currId); if (relatedFunctionalKey) return relatedFunctionalKey; - currId = this.getParentInstanceKey(imodel, currId)?.id; + currId = imodel.elements.tryGetElementProps(currId)?.parent?.id; } return undefined; } @@ -155,11 +122,11 @@ export class SelectionScopesHelper { private static elementClassDerivesFrom(imodel: IModelDb, elementId: Id64String, baseClassFullName: string): boolean { const query = ` SELECT 1 - FROM bis.Element e - INNER JOIN meta.ClassHasAllBaseClasses baseClassRels ON baseClassRels.SourceECInstanceId = e.ECClassId - INNER JOIN meta.ECClassDef baseClass ON baseClass.ECInstanceId = baseClassRels.TargetECInstanceId - INNER JOIN meta.ECSchemaDef baseSchema ON baseSchema.ECInstanceId = baseClass.Schema.Id - WHERE e.ECInstanceId = ? AND (baseSchema.Name || ':' || baseClass.Name) = ? + FROM bis.Element e + INNER JOIN meta.ClassHasAllBaseClasses baseClassRels ON baseClassRels.SourceECInstanceId = e.ECClassId + INNER JOIN meta.ECClassDef baseClass ON baseClass.ECInstanceId = baseClassRels.TargetECInstanceId + INNER JOIN meta.ECSchemaDef baseSchema ON baseSchema.ECInstanceId = baseClass.Schema.Id + WHERE e.ECInstanceId = ? AND (baseSchema.Name || ':' || baseClass.Name) = ? `; return imodel.withPreparedStatement(query, (stmt): boolean => { stmt.bindId(1, elementId); @@ -168,13 +135,13 @@ export class SelectionScopesHelper { }); } - private static computeFunctionalElementSelection(requestOptions: SelectionScopeRequestOptions, ids: Id64String[]) { + private static computeFunctionalElementSelection(iModel: IModelDb, ids: Id64String[]) { const keys = new KeySet(); ids.forEach(skipTransients((id): void => { - const is3d = this.elementClassDerivesFrom(requestOptions.imodel, id, GeometricElement3d.classFullName); + const is3d = this.elementClassDerivesFrom(iModel, id, GeometricElement3d.classFullName); if (!is3d) { // if the input is not a 3d element, we try to find the first related functional element - const firstFunctionalKey = this.findFirstRelatedFunctionalElementKey(requestOptions.imodel, id); + const firstFunctionalKey = this.findFirstRelatedFunctionalElementKey(iModel, id); if (firstFunctionalKey) { keys.add(firstFunctionalKey); return; @@ -183,32 +150,32 @@ export class SelectionScopesHelper { let keyToAdd: InstanceKey | undefined; if (is3d) { // if we're computing scope for a 3d element, try to switch to its related functional element - keyToAdd = this.getRelatedFunctionalElementKey(requestOptions.imodel, id); + keyToAdd = this.getRelatedFunctionalElementKey(iModel, id); } if (!keyToAdd) - keyToAdd = getElementKey(requestOptions.imodel, id); + keyToAdd = getElementKey(iModel, id); keyToAdd && keys.add(keyToAdd); })); return keys; } - private static computeFunctionalAssemblySelection(requestOptions: SelectionScopeRequestOptions, ids: Id64String[]) { + private static computeFunctionalAssemblySelection(iModel: IModelDb, ids: Id64String[]) { const keys = new KeySet(); ids.forEach(skipTransients((id): void => { let idToGetAssemblyFor = id; - const is3d = this.elementClassDerivesFrom(requestOptions.imodel, id, GeometricElement3d.classFullName); + const is3d = this.elementClassDerivesFrom(iModel, id, GeometricElement3d.classFullName); if (!is3d) { // if the input is not a 3d element, we try to find the first related functional element - const firstFunctionalKey = this.findFirstRelatedFunctionalElementKey(requestOptions.imodel, id); + const firstFunctionalKey = this.findFirstRelatedFunctionalElementKey(iModel, id); if (firstFunctionalKey) idToGetAssemblyFor = firstFunctionalKey.id; } // find the assembly of either the given element or the functional element - const assemblyKey = this.getAssemblyKey(requestOptions.imodel, idToGetAssemblyFor); + const assemblyKey = this.getElementKey(iModel, idToGetAssemblyFor, 1); let keyToAdd = assemblyKey; if (is3d && keyToAdd) { // if we're computing scope for a 3d element, try to switch to its related functional element - const relatedFunctionalKey = this.getRelatedFunctionalElementKey(requestOptions.imodel, keyToAdd.id); + const relatedFunctionalKey = this.getRelatedFunctionalElementKey(iModel, keyToAdd.id); if (relatedFunctionalKey) keyToAdd = relatedFunctionalKey; } @@ -217,23 +184,23 @@ export class SelectionScopesHelper { return keys; } - private static computeFunctionalTopAssemblySelection(requestOptions: SelectionScopeRequestOptions, ids: Id64String[]) { + private static computeFunctionalTopAssemblySelection(iModel: IModelDb, ids: Id64String[]) { const keys = new KeySet(); ids.forEach(skipTransients((id): void => { let idToGetAssemblyFor = id; - const is3d = this.elementClassDerivesFrom(requestOptions.imodel, id, GeometricElement3d.classFullName); + const is3d = this.elementClassDerivesFrom(iModel, id, GeometricElement3d.classFullName); if (!is3d) { // if the input is not a 3d element, we try to find the first related functional element - const firstFunctionalKey = this.findFirstRelatedFunctionalElementKey(requestOptions.imodel, id); + const firstFunctionalKey = this.findFirstRelatedFunctionalElementKey(iModel, id); if (firstFunctionalKey) idToGetAssemblyFor = firstFunctionalKey.id; } // find the top assembly of either the given element or the functional element - const topAssemblyKey = this.getTopAssemblyKey(requestOptions.imodel, idToGetAssemblyFor); + const topAssemblyKey = this.getElementKey(iModel, idToGetAssemblyFor, Number.MAX_SAFE_INTEGER); let keyToAdd = topAssemblyKey; if (is3d && keyToAdd) { // if we're computing scope for a 3d element, try to switch to its related functional element - const relatedFunctionalKey = this.getRelatedFunctionalElementKey(requestOptions.imodel, keyToAdd.id); + const relatedFunctionalKey = this.getRelatedFunctionalElementKey(iModel, keyToAdd.id); if (relatedFunctionalKey) keyToAdd = relatedFunctionalKey; } @@ -248,18 +215,29 @@ export class SelectionScopesHelper { * @param keys Keys of elements to get the content for. * @param scopeId ID of selection scope to use for computing selection */ - public static async computeSelection(requestOptions: SelectionScopeRequestOptions, ids: Id64String[], scopeId: string): Promise { - switch (scopeId) { - case "element": return this.computeElementSelection(requestOptions, ids); - case "assembly": return this.computeAssemblySelection(requestOptions, ids); - case "top-assembly": return this.computeTopAssemblySelection(requestOptions, ids); - case "category": return this.computeCategorySelection(requestOptions, ids); - case "model": return this.computeModelSelection(requestOptions, ids); + public static async computeSelection(requestOptions: SelectionScopeRequestOptions, ids: Id64String[], scopeId: string): Promise; + /** @alpha */ + public static async computeSelection(requestOptions: ComputeSelectionRequestOptions): Promise; + public static async computeSelection(requestOptions: ComputeSelectionRequestOptions | SelectionScopeRequestOptions, elementIds?: Id64String[], scopeId?: string): Promise { + if (!isComputeSelectionRequestOptions(requestOptions)) { + return this.computeSelection({ + ...requestOptions, + elementIds: elementIds!, + scope: { id: scopeId! }, + }); + } + + switch (requestOptions.scope.id) { + case "element": return this.computeElementSelection(requestOptions.imodel, requestOptions.elementIds, (requestOptions.scope as ElementSelectionScopeProps).ancestorLevel ?? 0); + case "assembly": return this.computeElementSelection(requestOptions.imodel, requestOptions.elementIds, 1); + case "top-assembly": return this.computeElementSelection(requestOptions.imodel, requestOptions.elementIds, Number.MAX_SAFE_INTEGER); + case "category": return this.computeCategorySelection(requestOptions.imodel, requestOptions.elementIds); + case "model": return this.computeModelSelection(requestOptions.imodel, requestOptions.elementIds); case "functional": case "functional-element": - return this.computeFunctionalElementSelection(requestOptions, ids); - case "functional-assembly": return this.computeFunctionalAssemblySelection(requestOptions, ids); - case "functional-top-assembly": return this.computeFunctionalTopAssemblySelection(requestOptions, ids); + return this.computeFunctionalElementSelection(requestOptions.imodel, requestOptions.elementIds); + case "functional-assembly": return this.computeFunctionalAssemblySelection(requestOptions.imodel, requestOptions.elementIds); + case "functional-top-assembly": return this.computeFunctionalTopAssemblySelection(requestOptions.imodel, requestOptions.elementIds); } throw new PresentationError(PresentationStatus.InvalidArgument, "scopeId"); } diff --git a/presentation/backend/src/test/PresentationManager.test.ts b/presentation/backend/src/test/PresentationManager.test.ts index 90386210333b..4030359c411d 100644 --- a/presentation/backend/src/test/PresentationManager.test.ts +++ b/presentation/backend/src/test/PresentationManager.test.ts @@ -14,12 +14,11 @@ import { ArrayTypeDescription, CategoryDescription, Content, ContentDescriptorRequestOptions, ContentFlags, ContentJSON, ContentRequestOptions, ContentSourcesRequestOptions, DefaultContentDisplayTypes, Descriptor, DescriptorJSON, DescriptorOverrides, DiagnosticsOptions, DiagnosticsScopeLogs, DisplayLabelRequestOptions, DisplayLabelsRequestOptions, DistinctValuesRequestOptions, ElementProperties, FieldDescriptor, FieldDescriptorType, - FieldJSON, FilterByInstancePathsHierarchyRequestOptions, FilterByTextHierarchyRequestOptions, HierarchyCompareInfo, - HierarchyCompareInfoJSON, HierarchyCompareOptions, HierarchyRequestOptions, InstanceKey, IntRulesetVariable, ItemJSON, KeySet, KindOfQuantityInfo, - LabelDefinition, MultiElementPropertiesRequestOptions, NestedContentFieldJSON, NodeJSON, NodeKey, Paged, PageOptions, PresentationError, - PrimitiveTypeDescription, PropertiesFieldJSON, PropertyInfoJSON, PropertyJSON, RegisteredRuleset, RelatedClassInfo, Ruleset, SelectClassInfo, - SelectClassInfoJSON, SelectionInfo, SelectionScope, SingleElementPropertiesRequestOptions, StandardNodeTypes, StructTypeDescription, - VariableValueTypes, + FieldJSON, FilterByInstancePathsHierarchyRequestOptions, FilterByTextHierarchyRequestOptions, HierarchyCompareInfo, HierarchyCompareInfoJSON, + HierarchyCompareOptions, HierarchyRequestOptions, InstanceKey, IntRulesetVariable, ItemJSON, KeySet, KindOfQuantityInfo, LabelDefinition, + MultiElementPropertiesRequestOptions, NestedContentFieldJSON, NodeJSON, NodeKey, Paged, PageOptions, PresentationError, PrimitiveTypeDescription, + PropertiesFieldJSON, PropertyInfoJSON, PropertyJSON, RegisteredRuleset, RelatedClassInfo, Ruleset, SelectClassInfo, SelectClassInfoJSON, + SelectionInfo, SelectionScope, SingleElementPropertiesRequestOptions, StandardNodeTypes, StructTypeDescription, VariableValueTypes, } from "@itwin/presentation-common"; import { createRandomECClassInfoJSON, createRandomECInstanceKey, createRandomECInstanceKeyJSON, createRandomECInstancesNodeJSON, @@ -2123,7 +2122,16 @@ describe("PresentationManager", () => { const resultKeys = new KeySet(); const stub = sinon.stub(SelectionScopesHelper, "computeSelection").resolves(resultKeys); const result = await manager.computeSelection({ imodel: imodel.object, ids, scopeId: "test scope" }); - expect(stub).to.be.calledOnceWith({ imodel: imodel.object }, ids, "test scope"); + expect(stub).to.be.calledOnceWith({ imodel: imodel.object, elementIds: ids, scope: { id: "test scope" } }); + expect(result).to.eq(resultKeys); + }); + + it("computes element selection using `SelectionScopesHelper`", async () => { + const elementIds = [createRandomId()]; + const resultKeys = new KeySet(); + const stub = sinon.stub(SelectionScopesHelper, "computeSelection").resolves(resultKeys); + const result = await manager.computeSelection({ imodel: imodel.object, elementIds, scope: { id: "element", ancestorLevel: 123 } }); + expect(stub).to.be.calledOnceWith({ imodel: imodel.object, elementIds, scope: { id: "element", ancestorLevel: 123 } }); expect(result).to.eq(resultKeys); }); diff --git a/presentation/backend/src/test/PresentationRpcImpl.test.ts b/presentation/backend/src/test/PresentationRpcImpl.test.ts index dfcb2cfd061e..f4058cf10f52 100644 --- a/presentation/backend/src/test/PresentationRpcImpl.test.ts +++ b/presentation/backend/src/test/PresentationRpcImpl.test.ts @@ -7,17 +7,17 @@ import * as faker from "faker"; import * as sinon from "sinon"; import * as moq from "typemoq"; import { IModelDb } from "@itwin/core-backend"; -import { Guid, Id64String, using } from "@itwin/core-bentley"; +import { Guid, using } from "@itwin/core-bentley"; import { IModelNotFoundResponse, IModelRpcProps } from "@itwin/core-common"; import { - Content, ContentDescriptorRequestOptions, ContentDescriptorRpcRequestOptions, ContentFlags, ContentInstanceKeysRpcRequestOptions, - ContentRequestOptions, ContentRpcRequestOptions, ContentSourcesRequestOptions, ContentSourcesRpcRequestOptions, ContentSourcesRpcResult, Descriptor, - DescriptorOverrides, DiagnosticsScopeLogs, DisplayLabelRequestOptions, DisplayLabelRpcRequestOptions, DisplayLabelsRequestOptions, - DisplayLabelsRpcRequestOptions, DistinctValuesRequestOptions, DistinctValuesRpcRequestOptions, ElementProperties, FieldDescriptor, - FieldDescriptorType, FilterByInstancePathsHierarchyRequestOptions, FilterByTextHierarchyRequestOptions, HierarchyRequestOptions, - HierarchyRpcRequestOptions, InstanceKey, Item, KeySet, Node, NodeKey, NodePathElement, Paged, PageOptions, PresentationError, - PresentationRpcRequestOptions, PresentationStatus, RulesetVariable, RulesetVariableJSON, SelectClassInfo, SelectionScopeRequestOptions, - SingleElementPropertiesRequestOptions, SingleElementPropertiesRpcRequestOptions, VariableValueTypes, + ComputeSelectionRequestOptions, ComputeSelectionRpcRequestOptions, Content, ContentDescriptorRequestOptions, ContentDescriptorRpcRequestOptions, + ContentFlags, ContentInstanceKeysRpcRequestOptions, ContentRequestOptions, ContentRpcRequestOptions, ContentSourcesRequestOptions, + ContentSourcesRpcRequestOptions, ContentSourcesRpcResult, Descriptor, DescriptorOverrides, DiagnosticsScopeLogs, DisplayLabelRequestOptions, + DisplayLabelRpcRequestOptions, DisplayLabelsRequestOptions, DisplayLabelsRpcRequestOptions, DistinctValuesRequestOptions, + DistinctValuesRpcRequestOptions, ElementProperties, FieldDescriptor, FieldDescriptorType, FilterByInstancePathsHierarchyRequestOptions, + FilterByTextHierarchyRequestOptions, HierarchyRequestOptions, HierarchyRpcRequestOptions, InstanceKey, Item, KeySet, Node, NodeKey, NodePathElement, + Paged, PageOptions, PresentationError, PresentationRpcRequestOptions, PresentationStatus, RulesetVariable, RulesetVariableJSON, SelectClassInfo, + SelectionScopeRequestOptions, SingleElementPropertiesRequestOptions, SingleElementPropertiesRpcRequestOptions, VariableValueTypes, } from "@itwin/presentation-common"; import { createRandomECInstanceKey, createRandomECInstancesNode, createRandomECInstancesNodeKey, createRandomId, createRandomLabelDefinitionJSON, @@ -1490,16 +1490,16 @@ describe("PresentationRpcImpl", () => { describe("computeSelection", () => { - it("calls manager", async () => { + it("[deprecated] calls manager", async () => { const scope = createRandomSelectionScope(); const ids = [createRandomId()]; const rpcOptions: PresentationRpcRequestOptions> = { ...defaultRpcParams, }; - const managerOptions: SelectionScopeRequestOptions & { ids: Id64String[], scopeId: string } = { + const managerOptions: ComputeSelectionRequestOptions = { imodel: testData.imodelMock.object, - ids, - scopeId: scope.id, + elementIds: ids, + scope: { id: scope.id }, }; const result = new KeySet(); presentationManagerMock.setup(async (x) => x.computeSelection(managerOptions)) @@ -1510,6 +1510,35 @@ describe("PresentationRpcImpl", () => { expect(actualResult.result).to.deep.eq(result.toJSON()); }); + it("calls manager", async () => { + const scopeId = "element"; + const ancestorLevel = 123; + const elementIds = [createRandomId()]; + const rpcOptions: ComputeSelectionRpcRequestOptions = { + ...defaultRpcParams, + elementIds, + scope: { + id: scopeId, + ancestorLevel, + }, + }; + const managerOptions: ComputeSelectionRequestOptions = { + imodel: testData.imodelMock.object, + elementIds, + scope: { + id: scopeId, + ancestorLevel, + }, + }; + const result = new KeySet(); + presentationManagerMock.setup(async (x) => x.computeSelection(managerOptions)) + .returns(async () => result) + .verifiable(); + const actualResult = await impl.computeSelection(testData.imodelToken, rpcOptions); + presentationManagerMock.verifyAll(); + expect(actualResult.result).to.deep.eq(result.toJSON()); + }); + }); }); diff --git a/presentation/backend/src/test/SelectionScopesHelper.test.ts b/presentation/backend/src/test/SelectionScopesHelper.test.ts index e2ec68695c19..024fd80d024d 100644 --- a/presentation/backend/src/test/SelectionScopesHelper.test.ts +++ b/presentation/backend/src/test/SelectionScopesHelper.test.ts @@ -5,8 +5,8 @@ import { expect } from "chai"; import * as faker from "faker"; import * as moq from "typemoq"; -import { DbResult, Id64, Id64String } from "@itwin/core-bentley"; import { DrawingGraphic, ECSqlStatement, ECSqlValue, Element, IModelDb, IModelHost } from "@itwin/core-backend"; +import { DbResult, Id64, Id64String } from "@itwin/core-bentley"; import { ElementProps, EntityMetaData, IModelError, ModelProps } from "@itwin/core-common"; import { InstanceKey } from "@itwin/presentation-common"; import { createRandomECInstanceKey, createRandomId } from "@itwin/presentation-common/lib/cjs/test"; @@ -38,7 +38,8 @@ describe("SelectionScopesHelper", () => { const modelsMock = moq.Mock.ofType(); const setupIModelForElementKey = (key: InstanceKey) => { - imodelMock.setup((x) => x.withPreparedStatement(moq.It.isAnyString(), moq.It.isAny())).callback((_q, cb) => { + // this mock simulates the element key query returning a single row with results for the given key (`getElementKey` in Utils.ts) + imodelMock.setup((x) => x.withPreparedStatement(moq.It.is((q) => (typeof q === "string" && q.includes("SELECT ECClassId FROM"))), moq.It.isAny())).callback((_q, cb) => { const valueMock = moq.Mock.ofType(); valueMock.setup((x) => x.getClassNameForClassId()).returns(() => key.className); const stmtMock = moq.Mock.ofType(); @@ -49,8 +50,8 @@ describe("SelectionScopesHelper", () => { }; const setupIModelForInvalidId = () => { - // this mock simulates trying to bind an invalid id - imodelMock.setup((x) => x.withPreparedStatement(moq.It.isAnyString(), moq.It.isAny())).callback((_q, cb) => { + // this mock simulates trying to bind an invalid id to the element key query (`getElementKey` in Utils.ts) + imodelMock.setup((x) => x.withPreparedStatement(moq.It.is((q) => (typeof q === "string" && q.includes("SELECT ECClassId FROM"))), moq.It.isAny())).callback((_q, cb) => { const stmtMock = moq.Mock.ofType(); stmtMock.setup((x) => x.bindId(moq.It.isAnyNumber(), moq.It.isAny())).throws(new IModelError(DbResult.BE_SQLITE_ERROR, "Error binding Id")); stmtMock.setup((x) => x.step()).returns(() => DbResult.BE_SQLITE_ERROR); @@ -59,6 +60,7 @@ describe("SelectionScopesHelper", () => { }; const setupIModelForNoResultStatement = () => { + // this mock simulates any kind of query returning no results imodelMock.setup((x) => x.withPreparedStatement(moq.It.isAnyString(), moq.It.isAny())).callback((_q, cb) => { const stmtMock = moq.Mock.ofType(); stmtMock.setup((x) => x.step()).returns(() => DbResult.BE_SQLITE_DONE); @@ -101,7 +103,8 @@ describe("SelectionScopesHelper", () => { const createTransientElementId = () => Id64.fromLocalAndBriefcaseIds(faker.random.number(), 0xffffff); const setupIModelForFunctionalKeyQuery = (props: { graphicalElementKey: InstanceKey, stepResult?: DbResult, functionalElementKey?: InstanceKey }) => { - imodelMock.setup((x) => x.withPreparedStatement(moq.It.isAnyString(), moq.It.isAny())).returns((_q, cb) => { + const functionalKeyQueryIdentifier = "SELECT funcSchemaDef.Name || '.' || funcClassDef.Name funcElClassName, fe.ECInstanceId funcElId"; + imodelMock.setup((x) => x.withPreparedStatement(moq.It.is((q) => (typeof q === "string" && q.includes(functionalKeyQueryIdentifier))), moq.It.isAny())).returns((_q, cb) => { const stmtMock = moq.Mock.ofType(); stmtMock.setup((x) => x.step()).returns(() => props.stepResult ?? DbResult.BE_SQLITE_ROW); stmtMock.setup((x) => x.getRow()).returns(() => ({ @@ -112,17 +115,16 @@ describe("SelectionScopesHelper", () => { }); }; - const setupIModelForElementProps = (props?: { key?: InstanceKey, parentKey?: InstanceKey }) => { + const setupIModelForElementProps = (props?: { key?: InstanceKey, parentKey?: InstanceKey, isRemoved?: boolean }) => { const key = props?.key ?? createRandomECInstanceKey(); - const elementProps = props?.parentKey ? createRandomElementProps(props.parentKey.id) : createRandomTopmostElementProps(); + const elementProps = props?.isRemoved ? undefined : props?.parentKey ? createRandomElementProps(props.parentKey.id) : createRandomTopmostElementProps(); elementsMock.setup((x) => x.tryGetElementProps(key.id)).returns(() => elementProps); - if (props?.parentKey) - setupIModelForElementKey(props.parentKey); return { key, props: elementProps }; }; const setupIModelDerivesFromClassQuery = (doesDeriveFromSuppliedClass: boolean) => { - imodelMock.setup((x) => x.withPreparedStatement(moq.It.isAnyString(), moq.It.isAny())).returns((_q, cb) => { + const classDerivesFromQueryIdentifier = "SELECT 1"; + imodelMock.setup((x) => x.withPreparedStatement(moq.It.is((q) => (typeof q === "string" && q.includes(classDerivesFromQueryIdentifier))), moq.It.isAny())).returns((_q, cb) => { const stmtMock = moq.Mock.ofType(); stmtMock.setup((x) => x.step()).returns(() => doesDeriveFromSuppliedClass ? DbResult.BE_SQLITE_ROW : DbResult.BE_SQLITE_DONE); return cb(stmtMock.object); @@ -185,6 +187,18 @@ describe("SelectionScopesHelper", () => { validKeys.forEach((key) => expect(result.has(key))); }); + it("returns nth parent key", async () => { + const parent3 = setupIModelForElementProps({ key: createRandomECInstanceKey() }); + const parent2 = setupIModelForElementProps({ key: createRandomECInstanceKey(), parentKey: parent3.key }); + const parent1 = setupIModelForElementProps({ key: createRandomECInstanceKey(), parentKey: parent2.key }); + const element = setupIModelForElementProps({ key: createRandomECInstanceKey(), parentKey: parent1.key }); + setupIModelForElementKey(parent2.key); + + const result = await SelectionScopesHelper.computeSelection({ imodel: imodelMock.object, elementIds: [element.key.id], scope: { id: "element", ancestorLevel: 2 } }); + expect(result.size).to.eq(1); + expect(result.has(parent2.key)).to.be.true; + }); + }); describe("scope: 'assembly'", () => { @@ -546,6 +560,29 @@ describe("SelectionScopesHelper", () => { expect(result.has(graphicalElementKey)).to.be.true; }); + it("skips removed GeometricElement2d parents when looking for closest functional element", async () => { + setupIModelDerivesFromClassQuery(false); + + // set up one element with existing parent that has a related functional element + const functionalElement = setupIModelForElementProps({ key: createRandomECInstanceKey() }); + const existingParent = setupIModelForElementProps({ key: createRandomECInstanceKey() }); + const existingElement = setupIModelForElementProps({ key: createRandomECInstanceKey(), parentKey: existingParent.key }); + setupIModelForFunctionalKeyQuery({ graphicalElementKey: existingElement.key }); + setupIModelForFunctionalKeyQuery({ graphicalElementKey: existingParent.key, functionalElementKey: functionalElement.key }); + + // set up one element with removed parent + const removedParent = setupIModelForElementProps({ key: createRandomECInstanceKey(), isRemoved: true }); + const elementWithRemovedParent = setupIModelForElementProps({ key: createRandomECInstanceKey(), parentKey: removedParent.key }); + setupIModelForFunctionalKeyQuery({ graphicalElementKey: elementWithRemovedParent.key }); + setupIModelForElementKey(elementWithRemovedParent.key); + + // request + const result = await SelectionScopesHelper.computeSelection({ imodel: imodelMock.object }, [existingElement.key.id, elementWithRemovedParent.key.id], "functional-element"); + expect(result.size).to.eq(2); + expect(result.has(functionalElement.key)).to.be.true; + expect(result.has(elementWithRemovedParent.key)).to.be.true; + }); + }); describe("scope: 'functional-assembly'", () => { @@ -566,6 +603,7 @@ describe("SelectionScopesHelper", () => { const graphicalElementKey = createRandomECInstanceKey(); setupIModelDerivesFromClassQuery(true); setupIModelForElementProps({ key: graphicalElementKey, parentKey: graphicalParentElementKey }); + setupIModelForElementKey(graphicalParentElementKey); setupIModelForFunctionalKeyQuery({ graphicalElementKey: graphicalParentElementKey }); const result = await SelectionScopesHelper.computeSelection({ imodel: imodelMock.object }, [graphicalElementKey.id], "functional-assembly"); @@ -580,6 +618,7 @@ describe("SelectionScopesHelper", () => { const graphicalElementKey = createRandomECInstanceKey(); setupIModelDerivesFromClassQuery(true); setupIModelForElementProps({ key: graphicalElementKey, parentKey: graphicalParentElementKey }); + setupIModelForElementKey(graphicalParentElementKey); setupIModelForFunctionalKeyQuery({ graphicalElementKey: graphicalGrandParentElementKey, functionalElementKey }); const result = await SelectionScopesHelper.computeSelection({ imodel: imodelMock.object }, [graphicalElementKey.id], "functional-assembly"); @@ -647,6 +686,7 @@ describe("SelectionScopesHelper", () => { setupIModelForElementProps({ key: graphicalElementKey, parentKey: graphicalParentElementKey }); setupIModelForFunctionalKeyQuery({ graphicalElementKey: graphicalParentElementKey, functionalElementKey }); setupIModelForElementProps({ key: functionalElementKey, parentKey: functionalParentElementKey }); + setupIModelForElementKey(functionalParentElementKey); const result = await SelectionScopesHelper.computeSelection({ imodel: imodelMock.object }, [graphicalElementKey.id], "functional-assembly"); expect(result.size).to.eq(1); @@ -676,6 +716,7 @@ describe("SelectionScopesHelper", () => { setupIModelForElementProps({ key: graphicalElementKey, parentKey: graphicalParentElementKey }); setupIModelForElementProps({ key: graphicalParentElementKey, parentKey: graphicalGrandParentElementKey }); setupIModelForElementProps({ key: graphicalGrandParentElementKey }); + setupIModelForElementKey(graphicalGrandParentElementKey); setupIModelForFunctionalKeyQuery({ graphicalElementKey: graphicalGrandParentElementKey }); const result = await SelectionScopesHelper.computeSelection({ imodel: imodelMock.object }, [graphicalElementKey.id], "functional-top-assembly"); @@ -692,6 +733,7 @@ describe("SelectionScopesHelper", () => { setupIModelForElementProps({ key: graphicalElementKey, parentKey: graphicalParentElementKey }); setupIModelForElementProps({ key: graphicalParentElementKey, parentKey: graphicalGrandParentElementKey }); setupIModelForElementProps({ key: graphicalGrandParentElementKey }); + setupIModelForElementKey(graphicalGrandParentElementKey); setupIModelForFunctionalKeyQuery({ graphicalElementKey: graphicalGrandParentElementKey, functionalElementKey }); const result = await SelectionScopesHelper.computeSelection({ imodel: imodelMock.object }, [graphicalElementKey.id], "functional-top-assembly"); diff --git a/presentation/common/src/presentation-common/PresentationManagerOptions.ts b/presentation/common/src/presentation-common/PresentationManagerOptions.ts index 7febaa38fc54..845743658f2a 100644 --- a/presentation/common/src/presentation-common/PresentationManagerOptions.ts +++ b/presentation/common/src/presentation-common/PresentationManagerOptions.ts @@ -14,6 +14,7 @@ import { DiagnosticsOptionsWithHandler } from "./Diagnostics"; import { InstanceKey } from "./EC"; import { Ruleset } from "./rules/Ruleset"; import { RulesetVariable } from "./RulesetVariables"; +import { SelectionScopeProps } from "./selection/SelectionScope"; /** * A generic request options type used for both hierarchy and content requests. @@ -197,6 +198,19 @@ export interface DisplayLabelsRequestOptions extends Requ */ export interface SelectionScopeRequestOptions extends RequestOptions { } // eslint-disable-line @typescript-eslint/no-empty-interface +/** + * Request options used for calculating selection based on picked instance ksy and selection scope + * @alpha + */ +export interface ComputeSelectionRequestOptions extends RequestOptions { + elementIds: Id64String[]; + scope: SelectionScopeProps; +} +/** @internal */ +export function isComputeSelectionRequestOptions(options: ComputeSelectionRequestOptions | SelectionScopeRequestOptions): options is ComputeSelectionRequestOptions { + return !!(options as ComputeSelectionRequestOptions).elementIds; +} + /** * Data structure for comparing a hierarchy after ruleset or ruleset variable changes. * @public diff --git a/presentation/common/src/presentation-common/PresentationRpcInterface.ts b/presentation/common/src/presentation-common/PresentationRpcInterface.ts index a7ad385950cb..f206a7d16fc6 100644 --- a/presentation/common/src/presentation-common/PresentationRpcInterface.ts +++ b/presentation/common/src/presentation-common/PresentationRpcInterface.ts @@ -21,9 +21,10 @@ import { NodePathElementJSON } from "./hierarchy/NodePathElement"; import { KeySetJSON } from "./KeySet"; import { LabelDefinitionJSON } from "./LabelDefinition"; import { - ContentDescriptorRequestOptions, ContentInstanceKeysRequestOptions, ContentRequestOptions, ContentSourcesRequestOptions, DisplayLabelRequestOptions, - DisplayLabelsRequestOptions, DistinctValuesRequestOptions, FilterByInstancePathsHierarchyRequestOptions, FilterByTextHierarchyRequestOptions, - HierarchyRequestOptions, Paged, SelectionScopeRequestOptions, SingleElementPropertiesRequestOptions, + ComputeSelectionRequestOptions, ContentDescriptorRequestOptions, ContentInstanceKeysRequestOptions, ContentRequestOptions, + ContentSourcesRequestOptions, DisplayLabelRequestOptions, DisplayLabelsRequestOptions, DistinctValuesRequestOptions, + FilterByInstancePathsHierarchyRequestOptions, FilterByTextHierarchyRequestOptions, HierarchyRequestOptions, Paged, SelectionScopeRequestOptions, + SingleElementPropertiesRequestOptions, } from "./PresentationManagerOptions"; import { RulesetVariableJSON } from "./RulesetVariables"; import { SelectionScope } from "./selection/SelectionScope"; @@ -143,6 +144,11 @@ export type DisplayLabelsRpcRequestOptions = PresentationRpcRequestOptions>; +/** + * @alpha + */ +export type ComputeSelectionRpcRequestOptions = PresentationRpcRequestOptions>; + /** * Interface used for communication between Presentation backend and frontend. * @public @@ -152,7 +158,7 @@ export class PresentationRpcInterface extends RpcInterface { public static readonly interfaceName = "PresentationRpcInterface"; // eslint-disable-line @typescript-eslint/naming-convention /** The semantic version of the interface. */ - public static interfaceVersion = "3.0.0"; + public static interfaceVersion = "3.1.0"; /*=========================================================================================== NOTE: Any add/remove/change to the methods below requires an update of the interface version. @@ -188,7 +194,10 @@ export class PresentationRpcInterface extends RpcInterface { public async getSelectionScopes(_token: IModelRpcProps, _options: SelectionScopeRpcRequestOptions): PresentationRpcResponse { return this.forward(arguments); } // TODO: need to enforce paging on this - public async computeSelection(_token: IModelRpcProps, _options: SelectionScopeRpcRequestOptions, _ids: Id64String[], _scopeId: string): PresentationRpcResponse { return this.forward(arguments); } + public async computeSelection(_token: IModelRpcProps, _options: SelectionScopeRpcRequestOptions, _ids: Id64String[], _scopeId: string): PresentationRpcResponse; + /** @alpha */ + public async computeSelection(_token: IModelRpcProps, _options: ComputeSelectionRpcRequestOptions): PresentationRpcResponse; + public async computeSelection(_token: IModelRpcProps, _options: ComputeSelectionRpcRequestOptions | SelectionScopeRpcRequestOptions, _ids?: Id64String[], _scopeId?: string): PresentationRpcResponse { return this.forward(arguments); } } /** @alpha */ diff --git a/presentation/common/src/presentation-common/RpcRequestsHandler.ts b/presentation/common/src/presentation-common/RpcRequestsHandler.ts index 6256557c06b8..3d2c77d10eb8 100644 --- a/presentation/common/src/presentation-common/RpcRequestsHandler.ts +++ b/presentation/common/src/presentation-common/RpcRequestsHandler.ts @@ -6,7 +6,7 @@ * @module RPC */ -import { Guid, Id64String, IDisposable, Logger } from "@itwin/core-bentley"; +import { Guid, IDisposable, Logger } from "@itwin/core-bentley"; import { IModelRpcProps, RpcManager } from "@itwin/core-common"; import { PresentationCommonLoggerCategory } from "./CommonLoggerCategory"; import { DescriptorJSON, DescriptorOverrides } from "./content/Descriptor"; @@ -22,9 +22,10 @@ import { NodePathElementJSON } from "./hierarchy/NodePathElement"; import { KeySetJSON } from "./KeySet"; import { LabelDefinitionJSON } from "./LabelDefinition"; import { - ContentDescriptorRequestOptions, ContentInstanceKeysRequestOptions, ContentRequestOptions, ContentSourcesRequestOptions, DisplayLabelRequestOptions, - DisplayLabelsRequestOptions, DistinctValuesRequestOptions, FilterByInstancePathsHierarchyRequestOptions, FilterByTextHierarchyRequestOptions, - HierarchyRequestOptions, Paged, RequestOptions, RequestOptionsWithRuleset, SelectionScopeRequestOptions, SingleElementPropertiesRequestOptions, + ComputeSelectionRequestOptions, ContentDescriptorRequestOptions, ContentInstanceKeysRequestOptions, ContentRequestOptions, + ContentSourcesRequestOptions, DisplayLabelRequestOptions, DisplayLabelsRequestOptions, DistinctValuesRequestOptions, + FilterByInstancePathsHierarchyRequestOptions, FilterByTextHierarchyRequestOptions, HierarchyRequestOptions, Paged, RequestOptions, + RequestOptionsWithRuleset, SelectionScopeRequestOptions, SingleElementPropertiesRequestOptions, } from "./PresentationManagerOptions"; import { ContentSourcesRpcResult, PresentationRpcInterface, PresentationRpcRequestOptions, PresentationRpcResponse, @@ -199,13 +200,13 @@ export class RpcRequestsHandler implements IDisposable { return this.request>( this.rpcClient.getSelectionScopes.bind(this.rpcClient), options); } - public async computeSelection(options: SelectionScopeRequestOptions, ids: Id64String[], scopeId: string): Promise { - return this.request>( - this.rpcClient.computeSelection.bind(this.rpcClient), options, ids, scopeId); + public async computeSelection(options: ComputeSelectionRequestOptions): Promise { + return this.request>( + this.rpcClient.computeSelection.bind(this.rpcClient), options); } } -function isOptionsWithRuleset(options: Object): options is { rulesetOrId: Ruleset} { +function isOptionsWithRuleset(options: Object): options is { rulesetOrId: Ruleset } { return (typeof (options as RequestOptionsWithRuleset).rulesetOrId === "object"); } diff --git a/presentation/common/src/presentation-common/selection/SelectionScope.ts b/presentation/common/src/presentation-common/selection/SelectionScope.ts index e571c6b7fbbb..e234983d72e0 100644 --- a/presentation/common/src/presentation-common/selection/SelectionScope.ts +++ b/presentation/common/src/presentation-common/selection/SelectionScope.ts @@ -19,3 +19,12 @@ export interface SelectionScope { /** Description */ description?: string; } + +/** @alpha */ +export interface ElementSelectionScopeProps { + id: "element"; + ancestorLevel?: number; +} + +/** @alpha */ +export type SelectionScopeProps = ElementSelectionScopeProps | { id: string }; diff --git a/presentation/common/src/test/PresentationManagerOptions.test.ts b/presentation/common/src/test/PresentationManagerOptions.test.ts index 54586479fe63..9b0751be4c06 100644 --- a/presentation/common/src/test/PresentationManagerOptions.test.ts +++ b/presentation/common/src/test/PresentationManagerOptions.test.ts @@ -3,7 +3,10 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { expect } from "chai"; -import { isSingleElementPropertiesRequestOptions } from "../presentation-common"; +import { + ComputeSelectionRequestOptions, isComputeSelectionRequestOptions, isSingleElementPropertiesRequestOptions, SelectionScopeRequestOptions, +} from "../presentation-common"; +import { createRandomId } from "./_helpers"; describe("isSingleElementPropertiesRequestOptions", () => { it("return correct result for different element properties request options", () => { @@ -11,3 +14,23 @@ describe("isSingleElementPropertiesRequestOptions", () => { expect(isSingleElementPropertiesRequestOptions({ imodel: undefined, elementClasses: ["TestSchema:TestClass"] })).to.be.false; }); }); + +describe("isComputeSelectionRequestOptions ", () => { + + it("returns `false` for `SelectionScopeRequestOptions`", () => { + const opts: SelectionScopeRequestOptions = { + imodel: undefined, + }; + expect(isComputeSelectionRequestOptions(opts)).to.be.false; + }); + + it("returns `true` for `ComputeSelectionRequestOptions`", () => { + const opts: ComputeSelectionRequestOptions = { + imodel: undefined, + elementIds: [createRandomId(), createRandomId()], + scope: { id: "test" }, + }; + expect(isComputeSelectionRequestOptions(opts)).to.be.true; + }); + +}); diff --git a/presentation/common/src/test/PresentationRpcInterface.test.ts b/presentation/common/src/test/PresentationRpcInterface.test.ts index 8481d04e2a3c..2c2260568aca 100644 --- a/presentation/common/src/test/PresentationRpcInterface.test.ts +++ b/presentation/common/src/test/PresentationRpcInterface.test.ts @@ -14,8 +14,8 @@ import { } from "../presentation-common"; import { FieldDescriptorType } from "../presentation-common/content/Fields"; import { - ContentInstanceKeysRpcRequestOptions, FilterByInstancePathsHierarchyRpcRequestOptions, FilterByTextHierarchyRpcRequestOptions, - SingleElementPropertiesRpcRequestOptions, + ComputeSelectionRpcRequestOptions, ContentInstanceKeysRpcRequestOptions, FilterByInstancePathsHierarchyRpcRequestOptions, + FilterByTextHierarchyRpcRequestOptions, SingleElementPropertiesRpcRequestOptions, } from "../presentation-common/PresentationRpcInterface"; import { createTestContentDescriptor } from "./_helpers/Content"; import { createRandomECInstanceKey, createRandomECInstancesNodeKey, createRandomECInstancesNodeKeyJSON } from "./_helpers/random"; @@ -212,7 +212,7 @@ describe("PresentationRpcInterface", () => { expect(spy).to.be.calledOnceWith(toArguments(token, options)); }); - it("forwards computeSelection call", async () => { + it("[deprecated] forwards computeSelection call", async () => { const options: SelectionScopeRpcRequestOptions = { }; const ids = new Array(); @@ -221,6 +221,15 @@ describe("PresentationRpcInterface", () => { expect(spy).to.be.calledOnceWith(toArguments(token, options, ids, scopeId)); }); + it("forwards computeSelection call", async () => { + const options: ComputeSelectionRpcRequestOptions = { + elementIds: new Array(), + scope: { id: faker.random.uuid() }, + }; + await rpcInterface.computeSelection(token, options); + expect(spy).to.be.calledOnceWith(toArguments(token, options)); + }); + }); }); diff --git a/presentation/common/src/test/RpcRequestsHandler.test.ts b/presentation/common/src/test/RpcRequestsHandler.test.ts index 15c0dbb0071f..174e8ad4fa1e 100644 --- a/presentation/common/src/test/RpcRequestsHandler.test.ts +++ b/presentation/common/src/test/RpcRequestsHandler.test.ts @@ -6,7 +6,7 @@ import { expect } from "chai"; import * as faker from "faker"; import * as sinon from "sinon"; import * as moq from "typemoq"; -import { Id64String, Logger } from "@itwin/core-bentley"; +import { Id64, Logger } from "@itwin/core-bentley"; import { IModelRpcProps, RpcInterface, RpcInterfaceDefinition, RpcManager } from "@itwin/core-common"; import { DescriptorOverrides, DistinctValuesRpcRequestOptions, KeySet, KeySetJSON, Paged, PresentationError, PresentationRpcInterface, @@ -19,9 +19,9 @@ import { InstanceKeyJSON } from "../presentation-common/EC"; import { ElementProperties } from "../presentation-common/ElementProperties"; import { NodeKey, NodeKeyJSON } from "../presentation-common/hierarchy/Key"; import { - ContentDescriptorRequestOptions, ContentInstanceKeysRequestOptions, ContentRequestOptions, ContentSourcesRequestOptions, DisplayLabelRequestOptions, - DisplayLabelsRequestOptions, DistinctValuesRequestOptions, FilterByInstancePathsHierarchyRequestOptions, FilterByTextHierarchyRequestOptions, - HierarchyRequestOptions, SingleElementPropertiesRequestOptions, + ComputeSelectionRequestOptions, ContentDescriptorRequestOptions, ContentInstanceKeysRequestOptions, ContentRequestOptions, + ContentSourcesRequestOptions, DisplayLabelRequestOptions, DisplayLabelsRequestOptions, DistinctValuesRequestOptions, + FilterByInstancePathsHierarchyRequestOptions, FilterByTextHierarchyRequestOptions, HierarchyRequestOptions, SingleElementPropertiesRequestOptions, } from "../presentation-common/PresentationManagerOptions"; import { ContentDescriptorRpcRequestOptions, ContentInstanceKeysRpcRequestOptions, ContentRpcRequestOptions, ContentSourcesRpcRequestOptions, @@ -565,17 +565,19 @@ describe("RpcRequestsHandler", () => { }); it("forwards computeSelection call", async () => { - const handlerOptions: SelectionScopeRequestOptions = { + const handlerOptions: ComputeSelectionRequestOptions = { imodel: token, + elementIds: [Id64.invalid], + scope: { id: "test scope" }, }; - const rpcOptions: PresentationRpcRequestOptions> = { + const rpcOptions: PresentationRpcRequestOptions> = { clientId, + elementIds: [Id64.invalid], + scope: { id: "test scope" }, }; - const ids = new Array(); - const scopeId = faker.random.uuid(); const result = new KeySet().toJSON(); - rpcInterfaceMock.setup(async (x) => x.computeSelection(token, rpcOptions, ids, scopeId)).returns(async () => successResponse(result)).verifiable(); - expect(await handler.computeSelection(handlerOptions, ids, scopeId)).to.eq(result); + rpcInterfaceMock.setup(async (x) => x.computeSelection(token, rpcOptions)).returns(async () => successResponse(result)).verifiable(); + expect(await handler.computeSelection(handlerOptions)).to.eq(result); rpcInterfaceMock.verifyAll(); }); diff --git a/presentation/components/src/presentation-components/favorite-properties/DataProvider.ts b/presentation/components/src/presentation-components/favorite-properties/DataProvider.ts index 2b2fcfeddd2a..80c5f4703ef2 100644 --- a/presentation/components/src/presentation-components/favorite-properties/DataProvider.ts +++ b/presentation/components/src/presentation-components/favorite-properties/DataProvider.ts @@ -6,11 +6,11 @@ * @module FavoriteProperties */ +import { PropertyData } from "@itwin/components-react"; import { Id64Arg, using } from "@itwin/core-bentley"; import { IModelConnection } from "@itwin/core-frontend"; import { CategoryDescription, KeySet, Ruleset } from "@itwin/presentation-common"; -import { getScopeId, Presentation } from "@itwin/presentation-frontend"; -import { PropertyData } from "@itwin/components-react"; +import { createSelectionScopeProps, Presentation } from "@itwin/presentation-frontend"; import { translate } from "../common/Utils"; import { PresentationPropertyDataProvider } from "../propertygrid/DataProvider"; @@ -112,7 +112,7 @@ export class FavoritePropertiesDataProvider implements IFavoritePropertiesDataPr }); } - const keys = await Presentation.selection.scopes.computeSelection(imodel, elementIds, getScopeId(Presentation.selection.scopes.activeScope)); + const keys = await Presentation.selection.scopes.computeSelection(imodel, elementIds, createSelectionScopeProps(Presentation.selection.scopes.activeScope)); return this.getData(imodel, keys); } } diff --git a/presentation/frontend/src/presentation-frontend/selection/SelectionManager.ts b/presentation/frontend/src/presentation-frontend/selection/SelectionManager.ts index e0d1c2174644..5e35797c4fb1 100644 --- a/presentation/frontend/src/presentation-frontend/selection/SelectionManager.ts +++ b/presentation/frontend/src/presentation-frontend/selection/SelectionManager.ts @@ -8,11 +8,11 @@ import { Id64, Id64Arg, Id64Array, IDisposable, using } from "@itwin/core-bentley"; import { IModelConnection, SelectionSetEvent, SelectionSetEventType } from "@itwin/core-frontend"; -import { AsyncTasksTracker, Keys, KeySet, SelectionScope } from "@itwin/presentation-common"; +import { AsyncTasksTracker, Keys, KeySet, SelectionScope, SelectionScopeProps } from "@itwin/presentation-common"; import { HiliteSet, HiliteSetProvider } from "./HiliteSetProvider"; import { ISelectionProvider } from "./ISelectionProvider"; import { SelectionChangeEvent, SelectionChangeEventArgs, SelectionChangeType } from "./SelectionChangeEvent"; -import { getScopeId, SelectionScopesManager } from "./SelectionScopesManager"; +import { createSelectionScopeProps, SelectionScopesManager } from "./SelectionScopesManager"; /** * Properties for creating [[SelectionManager]]. @@ -238,7 +238,7 @@ export class SelectionManager implements ISelectionProvider { * @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels)) * @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control */ - public async addToSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScope | string, level: number = 0, rulesetId?: string): Promise { + public async addToSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScopeProps | SelectionScope | string, level: number = 0, rulesetId?: string): Promise { const scopedKeys = await this.scopes.computeSelection(imodel, ids, scope); this.addToSelection(source, imodel, scopedKeys, level, rulesetId); } @@ -252,7 +252,7 @@ export class SelectionManager implements ISelectionProvider { * @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels)) * @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control */ - public async removeFromSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScope | string, level: number = 0, rulesetId?: string): Promise { + public async removeFromSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScopeProps | SelectionScope | string, level: number = 0, rulesetId?: string): Promise { const scopedKeys = await this.scopes.computeSelection(imodel, ids, scope); this.removeFromSelection(source, imodel, scopedKeys, level, rulesetId); } @@ -266,7 +266,7 @@ export class SelectionManager implements ISelectionProvider { * @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels)) * @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control */ - public async replaceSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScope | string, level: number = 0, rulesetId?: string): Promise { + public async replaceSelectionWithScope(source: string, imodel: IModelConnection, ids: Id64Arg, scope: SelectionScopeProps | SelectionScope | string, level: number = 0, rulesetId?: string): Promise { const scopedKeys = await this.scopes.computeSelection(imodel, ids, scope); this.replaceSelection(source, imodel, scopedKeys, level, rulesetId); } @@ -376,13 +376,11 @@ export class ToolSelectionSyncHandler implements IDisposable { break; } - const scopeId = getScopeId(this._logicalSelection.scopes.activeScope); - // we're always using scoped selection changer even if the scope is set to "element" - that // makes sure we're adding to selection keys with concrete classes and not "BisCore:Element", which // we can't because otherwise our keys compare fails (presentation components load data with // concrete classes) - const changer = new ScopedSelectionChanger(this._selectionSourceName, this._imodel, this._logicalSelection, scopeId); + const changer = new ScopedSelectionChanger(this._selectionSourceName, this._imodel, this._logicalSelection, createSelectionScopeProps(this._logicalSelection.scopes.activeScope)); // we know what to do immediately on `clear` events if (SelectionSetEventType.Clear === ev.type) { @@ -451,8 +449,8 @@ class ScopedSelectionChanger { public readonly name: string; public readonly imodel: IModelConnection; public readonly manager: SelectionManager; - public readonly scope: SelectionScope | string; - public constructor(name: string, imodel: IModelConnection, manager: SelectionManager, scope: SelectionScope | string) { + public readonly scope: SelectionScopeProps | SelectionScope | string; + public constructor(name: string, imodel: IModelConnection, manager: SelectionManager, scope: SelectionScopeProps | SelectionScope | string) { this.name = name; this.imodel = imodel; this.manager = manager; diff --git a/presentation/frontend/src/presentation-frontend/selection/SelectionScopesManager.ts b/presentation/frontend/src/presentation-frontend/selection/SelectionScopesManager.ts index 53588de64504..6488ff5bb237 100644 --- a/presentation/frontend/src/presentation-frontend/selection/SelectionScopesManager.ts +++ b/presentation/frontend/src/presentation-frontend/selection/SelectionScopesManager.ts @@ -8,7 +8,7 @@ import { Id64Arg } from "@itwin/core-bentley"; import { IModelConnection } from "@itwin/core-frontend"; -import { DEFAULT_KEYS_BATCH_SIZE, KeySet, RpcRequestsHandler, SelectionScope } from "@itwin/presentation-common"; +import { DEFAULT_KEYS_BATCH_SIZE, KeySet, RpcRequestsHandler, SelectionScope, SelectionScopeProps } from "@itwin/presentation-common"; /** * Properties for creating [[SelectionScopesManager]]. @@ -32,7 +32,7 @@ export class SelectionScopesManager { private _rpcRequestsHandler: RpcRequestsHandler; private _getLocale: () => string | undefined; - private _activeScope: SelectionScope | string | undefined; + private _activeScope: SelectionScopeProps | SelectionScope | string | undefined; public constructor(props: SelectionScopesManagerProps) { this._rpcRequestsHandler = props.rpcRequestsHandler; @@ -44,7 +44,7 @@ export class SelectionScopesManager { /** The active selection scope or its id */ public get activeScope() { return this._activeScope; } - public set activeScope(scope: SelectionScope | string | undefined) { this._activeScope = scope; } + public set activeScope(scope: SelectionScopeProps | SelectionScope | string | undefined) { this._activeScope = scope; } /** * Get available selection scopes. @@ -62,8 +62,8 @@ export class SelectionScopesManager { * @param ids Element IDs to compute selection for * @param scope Selection scope to apply */ - public async computeSelection(imodel: IModelConnection, ids: Id64Arg, scope: SelectionScope | string): Promise { - const scopeId = getScopeId(scope); + public async computeSelection(imodel: IModelConnection, ids: Id64Arg, scope: SelectionScopeProps | SelectionScope | string): Promise { + const scopeProps = createSelectionScopeProps(scope); // convert ids input to array if (typeof ids === "string") @@ -80,7 +80,7 @@ export class SelectionScopesManager { const batchStart = batchSize * batchIndex; const batchEnd = (batchStart + batchSize > ids.length) ? ids.length : (batchStart + batchSize); const batchIds = (0 === batchIndex && ids.length <= batchEnd) ? ids : ids.slice(batchStart, batchEnd); - batchKeyPromises.push(this._rpcRequestsHandler.computeSelection({ imodel: imodel.getRpcProps() }, batchIds, scopeId)); + batchKeyPromises.push(this._rpcRequestsHandler.computeSelection({ imodel: imodel.getRpcProps(), elementIds: batchIds, scope: scopeProps })); } const batchKeys = (await Promise.all(batchKeyPromises)).map(KeySet.fromJSON); batchKeys.forEach((bk) => keys.add(bk)); @@ -88,15 +88,27 @@ export class SelectionScopesManager { } } +/** + * Normalizes given scope options and returns [[ComputeSelectionScopeProps]] that can be used for + * calculating selection with scope. + * + * @internal + */ +export function createSelectionScopeProps(scope: SelectionScopeProps | SelectionScope | string | undefined): SelectionScopeProps { + if (!scope) + return { id: "element" }; + if (typeof scope === "string") + return { id: scope }; + return scope; +} + /** * Determines the scope id * @param scope Selection scope * @public + * @deprecated This is an internal utility that should've never become public. */ +// istanbul ignore next export function getScopeId(scope: SelectionScope | string | undefined): string { - if (!scope) - return "element"; - if (typeof scope === "string") - return scope; - return scope.id; + return createSelectionScopeProps(scope).id; } diff --git a/presentation/frontend/src/test/selection/SelectionManager.test.ts b/presentation/frontend/src/test/selection/SelectionManager.test.ts index 2b68a5f4d93d..659282c6fea3 100644 --- a/presentation/frontend/src/test/selection/SelectionManager.test.ts +++ b/presentation/frontend/src/test/selection/SelectionManager.test.ts @@ -9,7 +9,9 @@ import * as moq from "typemoq"; import { Id64, Id64Arg, Id64String, using } from "@itwin/core-bentley"; import { IModelApp, IModelConnection, SelectionSet, SelectionSetEventType } from "@itwin/core-frontend"; import { InstanceKey, KeySet, SelectionScope } from "@itwin/presentation-common"; -import { createRandomECInstanceKey, createRandomId, createRandomSelectionScope, createRandomTransientId, waitForPendingAsyncs } from "@itwin/presentation-common/lib/cjs/test"; +import { + createRandomECInstanceKey, createRandomId, createRandomSelectionScope, createRandomTransientId, waitForPendingAsyncs, +} from "@itwin/presentation-common/lib/cjs/test"; import { HiliteSetProvider, SelectionManager, SelectionScopesManager } from "../../presentation-frontend"; import { ToolSelectionSyncHandler, TRANSIENT_ELEMENT_CLASSNAME } from "../../presentation-frontend/selection/SelectionManager"; @@ -508,7 +510,7 @@ describe("SelectionManager", () => { it("uses \"element\" scope when `activeScope = undefined`", async () => { scopesMock.setup((x) => x.activeScope).returns(() => undefined); - scopesMock.setup(async (x) => x.computeSelection(imodelMock.object, moq.It.isAny(), "element")) + scopesMock.setup(async (x) => x.computeSelection(imodelMock.object, moq.It.isAny(), { id: "element" })) .returns(async () => new KeySet([createRandomECInstanceKey()])) .verifiable(); ss.add(createRandomId()); @@ -519,7 +521,7 @@ describe("SelectionManager", () => { it("uses \"element\" scope when `activeScope = \"element\"`", async () => { scopesMock.setup((x) => x.activeScope).returns(() => "element"); - scopesMock.setup(async (x) => x.computeSelection(imodelMock.object, moq.It.isAny(), "element")) + scopesMock.setup(async (x) => x.computeSelection(imodelMock.object, moq.It.isAny(), { id: "element" })) .returns(async () => new KeySet([createRandomECInstanceKey()])) .verifiable(); ss.add(createRandomId()); @@ -552,9 +554,9 @@ describe("SelectionManager", () => { selectionManager.selectionChange.addListener(logicalSelectionChangesListener); scopesMock.setup((x) => x.activeScope).returns(() => scope); - scopesMock.setup(async (x) => x.computeSelection(imodelMock.object, [], moq.It.isAnyString())) + scopesMock.setup(async (x) => x.computeSelection(imodelMock.object, [], moq.It.isAny())) .returns(async () => new KeySet()); - scopesMock.setup(async (x) => x.computeSelection(imodelMock.object, moq.It.is((v) => equalId64Arg(v, [persistentElementId])), moq.It.isAnyString())) + scopesMock.setup(async (x) => x.computeSelection(imodelMock.object, moq.It.is((v) => equalId64Arg(v, [persistentElementId])), moq.It.isAny())) .returns(async () => new KeySet([scopedKey])); }); @@ -737,7 +739,7 @@ describe("SelectionManager", () => { imodelMock.setup((x) => x.selectionSet).returns(() => ss); scopesMock.setup((x) => x.activeScope).returns(() => undefined); - scopesMock.setup(async (x) => x.computeSelection(imodelMock.object, [], moq.It.isAnyString())).returns(async () => new KeySet()); + scopesMock.setup(async (x) => x.computeSelection(imodelMock.object, [], moq.It.isAny())).returns(async () => new KeySet()); selectionManager.setSyncWithIModelToolSelection(imodelMock.object, true); }); diff --git a/presentation/frontend/src/test/selection/SelectionScopesManager.test.ts b/presentation/frontend/src/test/selection/SelectionScopesManager.test.ts index b3d5fa9d84e3..f18287fee5a0 100644 --- a/presentation/frontend/src/test/selection/SelectionScopesManager.test.ts +++ b/presentation/frontend/src/test/selection/SelectionScopesManager.test.ts @@ -8,7 +8,7 @@ import * as moq from "typemoq"; import { Id64String } from "@itwin/core-bentley"; import { IModelRpcProps } from "@itwin/core-common"; import { IModelConnection } from "@itwin/core-frontend"; -import { DEFAULT_KEYS_BATCH_SIZE, KeySet, RpcRequestsHandler } from "@itwin/presentation-common"; +import { DEFAULT_KEYS_BATCH_SIZE, ElementSelectionScopeProps, KeySet, RpcRequestsHandler } from "@itwin/presentation-common"; import { createRandomECInstanceKey, createRandomId, createRandomSelectionScope } from "@itwin/presentation-common/lib/cjs/test"; import { SelectionScopesManager, SelectionScopesManagerProps } from "../../presentation-frontend/selection/SelectionScopesManager"; @@ -102,7 +102,11 @@ describe("SelectionScopesManager", () => { const scope = createRandomSelectionScope(); const result = new KeySet(); rpcRequestsHandlerMock - .setup(async (x) => x.computeSelection(moq.It.isObjectWith({ imodel: imodelToken }), ids, scope.id)) + .setup(async (x) => x.computeSelection(moq.It.is((options) => { + return options.imodel === imodelToken + && options.elementIds.length === 1 && options.elementIds[0] === ids[0] + && options.scope.id === scope.id; + }))) .returns(async () => result.toJSON()) .verifiable(); const computedResult = await getManager().computeSelection(imodelMock.object, ids, scope); @@ -116,7 +120,11 @@ describe("SelectionScopesManager", () => { const scope = createRandomSelectionScope(); const result = new KeySet(); rpcRequestsHandlerMock - .setup(async (x) => x.computeSelection(moq.It.isObjectWith({ imodel: imodelToken }), ids, scope.id)) + .setup(async (x) => x.computeSelection(moq.It.is((options) => { + return options.imodel === imodelToken + && options.elementIds.length === 1 && options.elementIds[0] === ids[0] + && options.scope.id === scope.id; + }))) .returns(async () => result.toJSON()) .verifiable(); const computedResult = await getManager().computeSelection(imodelMock.object, ids, scope.id); @@ -125,6 +133,28 @@ describe("SelectionScopesManager", () => { expect(computedResult.hasAll(result)).to.be.true; }); + it("forwards request to RpcRequestsHandler with element scope and params", async () => { + const elementIds = [createRandomId()]; + const scope: ElementSelectionScopeProps = { + id: "element", + ancestorLevel: 123, + }; + const result = new KeySet(); + rpcRequestsHandlerMock + .setup(async (x) => x.computeSelection(moq.It.is((options) => { + return options.imodel === imodelToken + && options.elementIds.length === 1 && options.elementIds[0] === elementIds[0] + && options.scope.id === scope.id + && (options.scope as ElementSelectionScopeProps).ancestorLevel === scope.ancestorLevel; + }))) + .returns(async () => result.toJSON()) + .verifiable(); + const computedResult = await getManager().computeSelection(imodelMock.object, elementIds, scope); + rpcRequestsHandlerMock.verifyAll(); + expect(computedResult.size).to.eq(result.size); + expect(computedResult.hasAll(result)).to.be.true; + }); + it("forwards multiple requests to RpcRequestsHandler when ids count exceeds max batch size", async () => { const ids = new Array(); for (let i = 0; i < (DEFAULT_KEYS_BATCH_SIZE + 1); ++i) @@ -133,11 +163,19 @@ describe("SelectionScopesManager", () => { const result1 = new KeySet([createRandomECInstanceKey()]); const result2 = new KeySet([createRandomECInstanceKey()]); rpcRequestsHandlerMock - .setup(async (x) => x.computeSelection(moq.It.isObjectWith({ imodel: imodelToken }), moq.It.is((inIds: string[]): boolean => (inIds.length === DEFAULT_KEYS_BATCH_SIZE)), scope.id)) + .setup(async (x) => x.computeSelection(moq.It.is((options) => { + return options.imodel === imodelToken + && options.elementIds.length === DEFAULT_KEYS_BATCH_SIZE + && options.scope.id === scope.id; + }))) .returns(async () => result1.toJSON()) .verifiable(); rpcRequestsHandlerMock - .setup(async (x) => x.computeSelection(moq.It.isObjectWith({ imodel: imodelToken }), moq.It.is((inIds: string[]): boolean => (inIds.length === 1)), scope.id)) + .setup(async (x) => x.computeSelection(moq.It.is((options) => { + return options.imodel === imodelToken + && options.elementIds.length === 1 + && options.scope.id === scope.id; + }))) .returns(async () => result2.toJSON()) .verifiable(); const computedResult = await getManager().computeSelection(imodelMock.object, ids, scope.id); @@ -152,7 +190,11 @@ describe("SelectionScopesManager", () => { const scope = createRandomSelectionScope(); const result = new KeySet(); rpcRequestsHandlerMock - .setup(async (x) => x.computeSelection(moq.It.isObjectWith({ imodel: imodelToken }), moq.It.is((a) => a.length === 1 && a[0] === id), scope.id)) + .setup(async (x) => x.computeSelection(moq.It.is((options) => { + return options.imodel === imodelToken + && options.elementIds.length === 1 && options.elementIds[0] === id + && options.scope.id === scope.id; + }))) .returns(async () => result.toJSON()) .verifiable(); const computedResult = await getManager().computeSelection(imodelMock.object, id, scope); @@ -166,7 +208,11 @@ describe("SelectionScopesManager", () => { const scope = createRandomSelectionScope(); const result = new KeySet(); rpcRequestsHandlerMock - .setup(async (x) => x.computeSelection(moq.It.isObjectWith({ imodel: imodelToken }), moq.It.is((a) => a.length === 1 && a[0] === id), scope.id)) + .setup(async (x) => x.computeSelection(moq.It.is((options) => { + return options.imodel === imodelToken + && options.elementIds.length === 1 && options.elementIds[0] === id + && options.scope.id === scope.id; + }))) .returns(async () => result.toJSON()) .verifiable(); const computedResult = await getManager().computeSelection(imodelMock.object, new Set([id]), scope);