Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unified-selection: Doc improvements #867

Merged
merged 5 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 (
<>
<IModelComponent selectionStorage={selectionStorage} />
<SelectedItemsWidget selectionStorage={selectionStorage} />
</>
);
}

// __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 <button onClick={() => iModelConnection.selectionSet.add(geometricElementId)}>Select element</button>;
}
// __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(<App />);
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;
}
3 changes: 3 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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#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.

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:
Expand Down
86 changes: 72 additions & 14 deletions packages/unified-selection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<!-- [[include: [Presentation.UnifiedSelection.IModelSelectionSync.Imports, Presentation.UnifiedSelection.IModelSelectionSync.Example], ts]] -->
<!-- BEGIN EXTRACTION -->

```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 <button onClick={() => iModelConnection.selectionSet.add(geometricElementId)}>Select element</button>;
}
```

<!-- END EXTRACTION -->

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:

<!-- [[include: [Presentation.UnifiedSelection.LegacySelectionManagerSelectionSync.Imports, Presentation.LegacySelectionManagerSelectionSync.Example], ts]] -->
<!-- BEGIN EXTRACTION -->

```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]);
},
});
```

<!-- END EXTRACTION -->
2 changes: 2 additions & 0 deletions packages/unified-selection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,7 +40,7 @@ export interface EnableUnifiedSelectionSyncWithIModelProps {
* hiliteSet: imodel.hilited,
* selectionSet: imodel.selectionSet,
* };
* ```.
* ```
*/
imodelAccess: ECSqlQueryExecutor &
ECClassHierarchyInspector & {
Expand All @@ -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"];

/**
Expand Down