From 34db43f90c34dd5a3f7334a7226622a15fb6ec87 Mon Sep 17 00:00:00 2001 From: Grigas Petraitis <35135765+grigasp@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:35:58 +0200 Subject: [PATCH 1/4] Improve `enableUnifiedSelectionSyncWithIModel` docs --- .../EnableUnifiedSelectionSyncWithIModel.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/unified-selection/src/unified-selection/EnableUnifiedSelectionSyncWithIModel.ts b/packages/unified-selection/src/unified-selection/EnableUnifiedSelectionSyncWithIModel.ts index 10767dfdf..5fdc5e6a1 100644 --- a/packages/unified-selection/src/unified-selection/EnableUnifiedSelectionSyncWithIModel.ts +++ b/packages/unified-selection/src/unified-selection/EnableUnifiedSelectionSyncWithIModel.ts @@ -22,8 +22,9 @@ import { safeDispose } from "./Utils.js"; export interface EnableUnifiedSelectionSyncWithIModelProps { /** * Provides access to different iModel's features: query executing, class hierarchy, selection and hilite sets. - * It's recommended to use `@itwin/presentation-core-interop` to create `ECSqlQueryExecutor` and `ECSchemaProvider` from - * [IModelConnection](https://www.itwinjs.org/reference/core-frontend/imodelconnection/imodelconnection/) and map its `key`, + * + * It's recommended to use `@itwin/presentation-core-interop` to create `key`, `ECSqlQueryExecutor` and `ECSchemaProvider` from + * [IModelConnection](https://www.itwinjs.org/reference/core-frontend/imodelconnection/imodelconnection/), and map its * `hilited` and `selectionSet` attributes like this: * * ```ts @@ -39,7 +40,7 @@ export interface EnableUnifiedSelectionSyncWithIModelProps { * hiliteSet: imodel.hilited, * selectionSet: imodel.selectionSet, * }; - * ```. + * ``` */ imodelAccess: ECSqlQueryExecutor & ECClassHierarchyInspector & { @@ -51,10 +52,13 @@ export interface EnableUnifiedSelectionSyncWithIModelProps { readonly selectionSet: CoreIModelSelectionSet; }; - /** Selection storage to synchronize IModel's tool selection with. */ + /** + * Unified selection storage to synchronize IModel's tool selection with. The storage should be shared + * across all components in the application to ensure unified selection experience. + */ selectionStorage: SelectionStorage; - /** Active scope provider. */ + /** Active selection scope provider. */ activeScopeProvider: () => ComputeSelectionProps["scope"]; /** From b36d49ba70a948bf69f779c479267f0819425beb Mon Sep 17 00:00:00 2001 From: Grigas Petraitis <35135765+grigasp@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:35:02 +0200 Subject: [PATCH 2/4] doc improvements --- .../learning-snippets/ReadmeExample.test.tsx | 165 ++++++++++++++++++ packages/unified-selection/README.md | 86 +++++++-- packages/unified-selection/package.json | 2 + 3 files changed, 239 insertions(+), 14 deletions(-) create mode 100644 apps/full-stack-tests/src/unified-selection/learning-snippets/ReadmeExample.test.tsx diff --git a/apps/full-stack-tests/src/unified-selection/learning-snippets/ReadmeExample.test.tsx b/apps/full-stack-tests/src/unified-selection/learning-snippets/ReadmeExample.test.tsx new file mode 100644 index 000000000..44e0a0aac --- /dev/null +++ b/apps/full-stack-tests/src/unified-selection/learning-snippets/ReadmeExample.test.tsx @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/* eslint-disable no-duplicate-imports */ + +import { expect } from "chai"; +import { insertPhysicalElement, insertPhysicalModelWithPartition, insertSpatialCategory } from "presentation-test-utilities"; +import { useEffect, useState } from "react"; +import { ECSchemaRpcLocater } from "@itwin/ecschema-rpcinterface-common"; +import { KeySet } from "@itwin/presentation-common"; +import { Selectables } from "@itwin/unified-selection"; +// __PUBLISH_EXTRACT_START__ Presentation.UnifiedSelection.IModelSelectionSync.Imports +import { IModelConnection } from "@itwin/core-frontend"; +import { SchemaContext } from "@itwin/ecschema-metadata"; +import { createECSchemaProvider, createECSqlQueryExecutor, createIModelKey } from "@itwin/presentation-core-interop"; +import { createCachingECClassHierarchyInspector } from "@itwin/presentation-shared"; +import { enableUnifiedSelectionSyncWithIModel, SelectionStorage } from "@itwin/unified-selection"; +// __PUBLISH_EXTRACT_END__ +// __PUBLISH_EXTRACT_START__ Presentation.UnifiedSelection.LegacySelectionManagerSelectionSync.Imports +import { createStorage } from "@itwin/unified-selection"; +import { Presentation } from "@itwin/presentation-frontend"; +// __PUBLISH_EXTRACT_END__ +import { buildIModel } from "../../IModelUtils.js"; +import { initialize, terminate } from "../../IntegrationTests.js"; +import { render, waitFor } from "../../RenderUtils.js"; +import { stubGetBoundingClientRect } from "../../Utils.js"; + +describe("Unified selection", () => { + describe("Learning snippets", () => { + describe("Readme example", () => { + before(async () => { + await initialize(); + }); + + after(async () => { + await terminate(); + }); + + stubGetBoundingClientRect(); + + it("Unified selection sync with iModel selection", async function () { + const { + imodel, + elementKey: { id: geometricElementId }, + } = await buildIModel(this, async (builder) => { + const modelKey = insertPhysicalModelWithPartition({ builder, codeValue: "test model" }); + const categoryKey = insertSpatialCategory({ builder, codeValue: "test category" }); + const elementKey = insertPhysicalElement({ builder, userLabel: "root element", modelId: modelKey.id, categoryId: categoryKey.id }); + return { modelKey, categoryKey, elementKey }; + }); + function useActiveIModelConnection() { + return imodel; + } + + /** + * A top-level component that creates the selection storage and renders multiple selection-enabled components, one of them + * also being iModel-based. + */ + function App() { + const [selectionStorage] = useState(() => createStorage()); + return ( + <> + + + + ); + } + + // __PUBLISH_EXTRACT_START__ Presentation.UnifiedSelection.IModelSelectionSync.Example + /** An iModel-based component that handles iModel selection directly, through its `SelectionSet` */ + function IModelComponent({ selectionStorage }: { selectionStorage: SelectionStorage }) { + // get the active iModel connection (implementation is outside the scope of this example) + const iModelConnection: IModelConnection = useActiveIModelConnection(); + + // enable unified selection sync with the iModel + useEffect(() => { + // iModel's schema context should be shared between all components using the iModel (implementation + // of the getter is outside the scope of this example) + const imodelSchemaContext: SchemaContext = getSchemaContext(iModelConnection); + + return enableUnifiedSelectionSyncWithIModel({ + // Unified selection storage to synchronize iModel's tool selection with. The storage should be shared + // across all components in the application to ensure unified selection experience. + selectionStorage, + + // `imodelAccess` provides access to different iModel's features: query executing, class hierarchy, + // selection and hilite sets + imodelAccess: { + ...createECSqlQueryExecutor(iModelConnection), + ...createCachingECClassHierarchyInspector({ schemaProvider: createECSchemaProvider(imodelSchemaContext) }), + key: createIModelKey(iModelConnection), + hiliteSet: iModelConnection.hilited, + selectionSet: iModelConnection.selectionSet, + }, + + // a function that returns the active selection scope (see "Selection scopes" section in README) + activeScopeProvider: () => "model", + }); + }, [iModelConnection, selectionStorage]); + + return ; + } + // __PUBLISH_EXTRACT_END__ + + /** A simple component that listens to selection changes and prints selected items count */ + function SelectedItemsWidget({ selectionStorage }: { selectionStorage: SelectionStorage }) { + function getSelectedElementsCount(storage: SelectionStorage) { + return Selectables.size(storage.getSelection({ imodelKey: createIModelKey(imodel) })); + } + + const [selectedElementsCount, setSelectedElementsCount] = useState(() => getSelectedElementsCount(selectionStorage)); + useEffect(() => { + return selectionStorage.selectionChangeEvent.addListener(() => { + setSelectedElementsCount(getSelectedElementsCount(selectionStorage)); + }); + }, [selectionStorage]); + return `Number of selected elements: ${selectedElementsCount}`; + } + + const { getByRole, getByText, user } = render(); + await waitFor(() => expect(getByText("Number of selected elements: 0")).to.not.be.null); + + await user.click(getByRole("button")); + await waitFor(() => expect(getByText("Number of selected elements: 1")).to.not.be.null); + }); + + it("Unified selection sync with legacy SelectionManager", async function () { + Presentation.terminate(); + + const { imodel, ...keys } = await buildIModel(this, async (builder) => { + const modelKey = insertPhysicalModelWithPartition({ builder, codeValue: "test model" }); + const categoryKey = insertSpatialCategory({ builder, codeValue: "test category" }); + const elementKey = insertPhysicalElement({ builder, userLabel: "root element", modelId: modelKey.id, categoryId: categoryKey.id }); + return { modelKey, categoryKey, elementKey }; + }); + + // __PUBLISH_EXTRACT_START__ Presentation.LegacySelectionManagerSelectionSync.Example + const selectionStorage = createStorage(); + + // Initialize Presentation with our selection storage, to make sure that any components, using `Presentation.selection`, + // use the same underlying selection store. + await Presentation.initialize({ + selection: { + selectionStorage, + }, + }); + // __PUBLISH_EXTRACT_END__ + + expect(Selectables.isEmpty(selectionStorage.getSelection({ imodelKey: imodel.key }))).to.be.true; + + Presentation.selection.addToSelection("test", imodel, new KeySet([keys.elementKey])); + await waitFor(() => { + expect(Selectables.size(selectionStorage.getSelection({ imodelKey: imodel.key }))).to.eq(1); + }); + }); + }); + }); +}); + +function getSchemaContext(imodel: IModelConnection) { + const schemas = new SchemaContext(); + schemas.addLocater(new ECSchemaRpcLocater(imodel.getRpcProps())); + return schemas; +} diff --git a/packages/unified-selection/README.md b/packages/unified-selection/README.md index 31b6bb9e1..f4a746d68 100644 --- a/packages/unified-selection/README.md +++ b/packages/unified-selection/README.md @@ -174,21 +174,79 @@ const selection = computeSelection({ queryExecutor, elementIds, scope: { id: "el ## iModel selection synchronization with unified selection -The `@itwin/unified-selection` package delivers a `enableUnifiedSelectionSyncWithIModel` function to enable selection synchronization between an iModel and a `SelectionStorage`. When called, it returns a cleanup function that should be used to disable the synchronization. There should only be one active synchronization between a single iModel and a `SelectionStorage` at a given time. For example, this function could be used inside a `useEffect` hook in a component that holds an iModel: +The `@itwin/unified-selection` package delivers an `enableUnifiedSelectionSyncWithIModel` function to enable selection synchronization between an iModel and a `SelectionStorage`. When called, it returns a cleanup function that should be used to disable the synchronization. + +For example, this function could be used inside a `useEffect` hook in a component that maintains an iModel: + + + ```ts -import { createECSqlQueryExecutor, createECSchemaProvider, createIModelKey } from "@itwin/presentation-core-interop"; -useEffect(() => { - return enableUnifiedSelectionSyncWithIModel({ - imodelAccess: { - ...createECSqlQueryExecutor(imodel), - ...createECSchemaProvider(imodel), - key: createIModelKey(imodel), - hiliteSet: imodel.hilited, - selectionSet: imodel.selectionSet, - }, +import { IModelConnection } from "@itwin/core-frontend"; +import { SchemaContext } from "@itwin/ecschema-metadata"; +import { createECSchemaProvider, createECSqlQueryExecutor, createIModelKey } from "@itwin/presentation-core-interop"; +import { createCachingECClassHierarchyInspector } from "@itwin/presentation-shared"; +import { enableUnifiedSelectionSyncWithIModel, SelectionStorage } from "@itwin/unified-selection"; + +/** An iModel-based component that handles iModel selection directly, through its `SelectionSet` */ +function IModelComponent({ selectionStorage }: { selectionStorage: SelectionStorage }) { + // get the active iModel connection (implementation is outside the scope of this example) + const iModelConnection: IModelConnection = useActiveIModelConnection(); + + // enable unified selection sync with the iModel + useEffect(() => { + // iModel's schema context should be shared between all components using the iModel (implementation + // of the getter is outside the scope of this example) + const imodelSchemaContext: SchemaContext = getSchemaContext(iModelConnection); + + return enableUnifiedSelectionSyncWithIModel({ + // Unified selection storage to synchronize iModel's tool selection with. The storage should be shared + // across all components in the application to ensure unified selection experience. + selectionStorage, + + // `imodelAccess` provides access to different iModel's features: query executing, class hierarchy, + // selection and hilite sets + imodelAccess: { + ...createECSqlQueryExecutor(iModelConnection), + ...createCachingECClassHierarchyInspector({ schemaProvider: createECSchemaProvider(imodelSchemaContext) }), + key: createIModelKey(iModelConnection), + hiliteSet: iModelConnection.hilited, + selectionSet: iModelConnection.selectionSet, + }, + + // a function that returns the active selection scope (see "Selection scopes" section in README) + activeScopeProvider: () => "model", + }); + }, [iModelConnection, selectionStorage]); + + return ; +} +``` + + + +There should only be one active synchronization between a single iModel and a `SelectionStorage` at a given time. + +## Using with legacy components + +To ensure unified selection experience across the whole application, it's important that the `SelectionStorage` is shared across all components. When used with legacy components that use unified selection APIs from `@itwin/presentation-frontend` package, the application should ensure that `Presentation` is initialized with the same selection storage: + + + + +```ts +import { createStorage } from "@itwin/unified-selection"; +import { Presentation } from "@itwin/presentation-frontend"; + +const selectionStorage = createStorage(); + +// Initialize Presentation with our selection storage, to make sure that any components, using `Presentation.selection`, +// use the same underlying selection store. +await Presentation.initialize({ + selection: { selectionStorage, - activeScopeProvider: () => "element", - }); -}, [imodel]); + }, +}); ``` + + diff --git a/packages/unified-selection/package.json b/packages/unified-selection/package.json index c650b21b7..d851eadd3 100644 --- a/packages/unified-selection/package.json +++ b/packages/unified-selection/package.json @@ -46,6 +46,8 @@ "test": "npm run test:dev", "extract-api": "extract-api --entry=unified-selection --apiReportFolder=./api --apiReportTempFolder=./api/temp --apiSummaryFolder=./api", "check-internal": "node ../../scripts/checkInternal.js --apiSummary ./api/unified-selection.api.md", + "update-extractions": "node ../../scripts/updateExtractions.js --targets=./README.md", + "check-extractions": "node ../../scripts/updateExtractions.js --targets=./README.md --check", "validate-markdowns": "node ../../scripts/validateMarkdowns.js README.md" }, "dependencies": { From 8093b370716d567b0427c08cddc38499c030810b Mon Sep 17 00:00:00 2001 From: Grigas Petraitis <35135765+grigasp@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:41:47 +0200 Subject: [PATCH 3/4] Add documentation links to `presentation-components` changelog --- packages/components/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index e4217eb81..0b6c648d0 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -51,6 +51,9 @@ As the new generation hierarchy building APIs are now available, the old tree-related APIs are now deprecated. See reasoning and migration guide [here](https://github.com/iTwin/presentation/blob/33e79ee8d77f30580a9bab81a72884bda008db25/packages/hierarchies/learning/PresentationRulesMigrationGuide.md). - [#800](https://github.com/iTwin/presentation/pull/800): Deprecate `viewWithUnifiedSelection` in favor of `enableUnifiedSelectionSyncWithIModel` from `@itwin/unified-selection` package. + + See [iModel selection synchronization with unified selection](https://github.com/iTwin/presentation/blob/master/packages/unified-selection/README.md#imodel-selection-synchronization-with-unified-selection) and [Using with legacy components](https://github.com/iTwin/presentation/blob/master/packages/unified-selection/README.md#imodel-selection-synchronization-with-unified-selection) for details on how to use `enableUnifiedSelectionSyncWithIModel` in React apps. + - [#802](https://github.com/iTwin/presentation/pull/802): Prefer `Symbol.dispose` over `dispose` for disposable objects. The package contained a number of types for disposable objects, that had a requirement of `dispose` method being called on them after they are no longer needed. In conjunction with the `using` utility from `@itwin/core-bentley`, usage of such objects looked like this: From 8a9314e058c0e9676c11ab133d17487bccda2542 Mon Sep 17 00:00:00 2001 From: Grigas Petraitis <35135765+grigasp@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:43:03 +0200 Subject: [PATCH 4/4] fix changelog link --- packages/components/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 0b6c648d0..99d4e3fe2 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -52,7 +52,7 @@ - [#800](https://github.com/iTwin/presentation/pull/800): Deprecate `viewWithUnifiedSelection` in favor of `enableUnifiedSelectionSyncWithIModel` from `@itwin/unified-selection` package. - See [iModel selection synchronization with unified selection](https://github.com/iTwin/presentation/blob/master/packages/unified-selection/README.md#imodel-selection-synchronization-with-unified-selection) and [Using with legacy components](https://github.com/iTwin/presentation/blob/master/packages/unified-selection/README.md#imodel-selection-synchronization-with-unified-selection) for details on how to use `enableUnifiedSelectionSyncWithIModel` in React apps. + See [iModel selection synchronization with unified selection](https://github.com/iTwin/presentation/blob/master/packages/unified-selection/README.md#imodel-selection-synchronization-with-unified-selection) and [Using with legacy components](https://github.com/iTwin/presentation/blob/master/packages/unified-selection/README.md#imodel-selection-synchronization-with-unified-selection#using-with-legacy-components) for details on how to use `enableUnifiedSelectionSyncWithIModel` in React apps. - [#802](https://github.com/iTwin/presentation/pull/802): Prefer `Symbol.dispose` over `dispose` for disposable objects.