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.