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

Add unified-selection-react package #841

Merged
merged 22 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
13 changes: 13 additions & 0 deletions .changeset/afraid-rocks-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@itwin/presentation-components": minor
---

Changed how unified selection-enabled components access unified selection storage.

- Added `selectionStorage` prop to `usePresentationTableWithUnifiedSelection` and `usePropertyDataProviderWithUnifiedSelection`.

When the prop is provided, the hooks will use the provided selection storage instead of `Presentation.selection` global storage from `@itwin/presentation-frontend` package. This makes the dependencies clear and hooks ready for deprecation of the selection APIs in the `@itwin/presentation-frontend` package. At the moment the prop is optional, but will be made required in the next major release of the package.

- Deprecated `UnifiedSelectionContext`, `UnifiedSelectionContextProvider`, `UnifiedSelectionContextProviderProps`, `UnifiedSelectionState` and `useUnifiedSelectionContext`. All of them are being replaced by the APIs in the new `@itwin/unified-selection-react` package, which now is an optional peer dependency of this package.

One of the property renderers - `InstanceKeyValueRenderer` was relying on the deprecated context to access unified selection storage. It now prefers the context provided with `UnifiedSelectionContextProvider` from `@itwin/unified-selection-react` package. If the context is not provided, the renderer falls back to the deprecated context.
40 changes: 40 additions & 0 deletions .changeset/early-glasses-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
"@itwin/presentation-hierarchies-react": minor
---

Changed how tree state hooks access unified selection storage.

- The tree state hooks that hook into unified selection system now accept a `selectionStorage` prop. At the moment the prop is optional, but will be made required in the next major release of the package.
- The `UnifiedSelectionProvider` React context provider is now deprecated. The context is still used by tree state hooks if the selection storage is not provided through prop.

Example of how to migrate to the new API:

```tsx
const selectionStorage = createStorage();

// before
function MyTreeComponent() {
// the hook takes selection storage from context, set up by the App component
const treeState = useUnifiedSelectionTree({ ... });
// ...
}
function App() {
return (
<UnifiedSelectionProvider storage={selectionStorage}>
<MyTreeComponent />
</UnifiedSelectionProvider>
);
}

// after
function MyTreeComponent({ selectionStorage }: { selectionStorage: SelectionStorage }) {
// the hook takes selection storage from props
const treeState = useUnifiedSelectionTree({ selectionStorage, ... });
// ...
}
function App() {
return (
<MyTreeComponent selectionStorage={selectionStorage} />
);
}
```
27 changes: 27 additions & 0 deletions .changeset/light-terms-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@itwin/unified-selection": minor
---

Add `Selectables.load` function to load instance keys from the `Selectables` object.

Example usage:

```ts
const selectables = Selectables.create([
// add instance key
{ className: "BisCore:Element", id: "0x1" },

// add custom selectable
{
identifier: "custom",
async * loadInstanceKeys() {
yield { className: "BisCore:Element", id: "0x2" };
}
},
]);

// logs: { className: "BisCore:Element", id: "0x1" }, { className: "BisCore:Element", id: "0x2" }
for await (const key of Selectables.load(selectables)) {
console.log(key);
}
```
5 changes: 5 additions & 0 deletions .changeset/olive-trees-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@itwin/unified-selection-react": major
---

Add a package that provides React APIs for conveniently using the `@itwin/unified-selection` package in React applications and components.
9 changes: 9 additions & 0 deletions .changeset/ten-badgers-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@itwin/presentation-components": minor
---

Added `activeScopeProvider` prop to `FavoritePropertiesDataProvider` constructor.

The new prop is a function that returns the active scope. When not provided, the provider uses the old way of getting the active scope - `SelectionScopesManager`, accessed through `Presentation.selection.scopes` global from `@itwin/presentation-frontend` package. The selection APIs in that package are about to be deprecated and this change makes the provider ready for that. The `activeScopeProvider` prop will be made required in the next major release of this package.

In addition, the `FavoritePropertiesDataProvider` now uses `@itwin/unified-selection` package for adjusting selection based on selection scope. This change does not affect the results.
1 change: 1 addition & 0 deletions apps/full-stack-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@itwin/presentation-shared": "workspace:^",
"@itwin/presentation-testing": "workspace:^",
"@itwin/unified-selection": "workspace:^",
"@itwin/unified-selection-react": "workspace:^",
"@itwin/webgl-compatibility": "catalog:itwinjs-core",
"@opentelemetry/api": "^1.9.0",
"@testing-library/dom": "catalog:test-tools",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { insertPhysicalElement, insertPhysicalModelWithPartition, insertSpatialC
import { useCallback, useState } from "react";
import { UiComponents, VirtualizedPropertyGridWithDataProvider } from "@itwin/components-react";
import { IModelApp, IModelConnection } from "@itwin/core-frontend";
import { InstanceKey, KeySet } from "@itwin/presentation-common";
import { InstanceKey } from "@itwin/presentation-common";
import { PresentationPropertyDataProvider, usePropertyDataProviderWithUnifiedSelection } from "@itwin/presentation-components";
import { Presentation } from "@itwin/presentation-frontend";
import { buildTestIModel } from "@itwin/presentation-testing";
import { createStorage } from "@itwin/unified-selection";
import { initialize, terminate } from "../../IntegrationTests.js";
import { act, getByText, render, waitFor } from "../../RenderUtils.js";
import { useOptionalDisposable } from "../../UseOptionalDisposable.js";
Expand All @@ -30,6 +30,9 @@ describe("Learning snippets", async () => {

it("renders unified selection property grid", async function () {
// __PUBLISH_EXTRACT_START__ Presentation.Components.UnifiedSelection.PropertyGrid
// Create a single unified selection storage to be shared between all application's components
const selectionStorage = createStorage();

function MyPropertyGrid(props: { imodel: IModelConnection }) {
// create a presentation rules driven data provider; the provider implements `IDisposable`, so we
// create it through `useOptionalDisposable` hook to make sure it's properly cleaned up
Expand All @@ -48,8 +51,9 @@ describe("Learning snippets", async () => {
}

function MyPropertyGridWithProvider({ dataProvider }: { dataProvider: PresentationPropertyDataProvider }) {
// set up the data provider to be notified about changes in unified selection
const { isOverLimit, numSelectedElements } = usePropertyDataProviderWithUnifiedSelection({ dataProvider });
// set up the data provider to be notified about changes in unified selection, the provided
// selection storage is used to synchronize selection between different components
const { isOverLimit, numSelectedElements } = usePropertyDataProviderWithUnifiedSelection({ dataProvider, selectionStorage });

// width and height should generally we computed using ResizeObserver API or one of its derivatives
const [width] = useState(400);
Expand Down Expand Up @@ -87,10 +91,10 @@ describe("Learning snippets", async () => {
await waitFor(() => getByText(container, "Select an element to see its properties"));

// test Unified Selection -> Property Grid content synchronization
act(() => Presentation.selection.replaceSelection("", imodel, new KeySet([elementKeys[0]])));
act(() => selectionStorage.replaceSelection({ imodelKey: imodel.key, source: "", selectables: [elementKeys[0]] }));
await ensurePropertyGridHasPropertyRecord(container, "$élêçtèd Ítêm(s)", "User Label", "My Element 1");

act(() => Presentation.selection.replaceSelection("", imodel, new KeySet([elementKeys[1]])));
act(() => selectionStorage.replaceSelection({ imodelKey: imodel.key, source: "", selectables: [elementKeys[1]] }));
await ensurePropertyGridHasPropertyRecord(container, "$élêçtèd Ítêm(s)", "User Label", "My Element 2");
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,10 @@ import { insertPhysicalElement, insertPhysicalModelWithPartition, insertSpatialC
import { PropertyRecord } from "@itwin/appui-abstract";
import { PropertyValueRendererManager, UiComponents } from "@itwin/components-react";
import { IModelApp, IModelConnection } from "@itwin/core-frontend";
import { InstanceKey, KeySet, Ruleset } from "@itwin/presentation-common";
import {
TableColumnDefinition,
TableRowDefinition,
UnifiedSelectionContextProvider,
usePresentationTableWithUnifiedSelection,
} from "@itwin/presentation-components";
import { Presentation } from "@itwin/presentation-frontend";
import { InstanceKey, Ruleset } from "@itwin/presentation-common";
import { TableColumnDefinition, TableRowDefinition, usePresentationTableWithUnifiedSelection } from "@itwin/presentation-components";
import { buildTestIModel } from "@itwin/presentation-testing";
import { createStorage, SelectionStorage } from "@itwin/unified-selection";
import { initialize, terminate } from "../../IntegrationTests.js";
import { act, getByText, render, waitFor } from "../../RenderUtils.js";
import { ensureTableHasRowsWithCellValues } from "../TableUtils.js";
Expand All @@ -34,7 +29,7 @@ describe("Learning snippets", async () => {

it("renders unified selection table", async function () {
// __PUBLISH_EXTRACT_START__ Presentation.Components.UnifiedSelection.Table
function MyTable(props: { imodel: IModelConnection }) {
function MyTable(props: { imodel: IModelConnection; selectionStorage: SelectionStorage }) {
// the library provides a variation of `usePresentationTable` that updates table content based
// on unified selection
const { columns, rows, isLoading } = usePresentationTableWithUnifiedSelection({
Expand All @@ -43,6 +38,7 @@ describe("Learning snippets", async () => {
pageSize: 10,
columnMapper: mapTableColumns,
rowMapper: mapTableRow,
selectionStorage: props.selectionStorage,
});

// don't render anything if the table is loading
Expand Down Expand Up @@ -115,35 +111,35 @@ describe("Learning snippets", async () => {
);
});

function TableWithUnifiedSelection() {
return (
// __PUBLISH_EXTRACT_START__ Presentation.Components.UnifiedSelection.TableWithinUnifiedSelectionContext
<UnifiedSelectionContextProvider imodel={imodel}>
<MyTable imodel={imodel} />
</UnifiedSelectionContextProvider>
// __PUBLISH_EXTRACT_END__
);
// __PUBLISH_EXTRACT_START__ Presentation.Components.UnifiedSelection.TableWithinUnifiedSelectionContext
// Create a single unified selection storage to be shared between all application's components
const selectionStorage = createStorage();

function App() {
// pass selection storage to the component to hook into it
return <MyTable imodel={imodel} selectionStorage={selectionStorage} />;
}
// __PUBLISH_EXTRACT_END__

// render the component
const { container } = render(<TableWithUnifiedSelection />);
const { container } = render(<App />);

await waitFor(() => getByText(container, "Select something to see properties"));

// test Unified Selection -> Table content synchronization
act(() => Presentation.selection.replaceSelection("", imodel, new KeySet([elementKeys[0]])));
act(() => selectionStorage.replaceSelection({ imodelKey: imodel.key, source: "", selectables: [elementKeys[0]] }));
await ensureTableHasRowsWithCellValues(container, "User Label", ["My Element 1"]);

act(() => Presentation.selection.replaceSelection("", imodel, new KeySet([elementKeys[1]])));
act(() => selectionStorage.replaceSelection({ imodelKey: imodel.key, source: "", selectables: [elementKeys[1]] }));
await ensureTableHasRowsWithCellValues(container, "User Label", ["My Element 2"]);

act(() => Presentation.selection.replaceSelection("", imodel, new KeySet([elementKeys[0], elementKeys[1]])));
act(() => selectionStorage.replaceSelection({ imodelKey: imodel.key, source: "", selectables: [elementKeys[0], elementKeys[1]] }));
await ensureTableHasRowsWithCellValues(container, "User Label", ["My Element 1", "My Element 2"]);

act(() => Presentation.selection.clearSelection("", imodel));
act(() => selectionStorage.clearSelection({ imodelKey: imodel.key, source: "" }));
await waitFor(() => getByText(container, "Select something to see properties"));

act(() => Presentation.selection.replaceSelection("", imodel, new KeySet([modelKey])));
act(() => selectionStorage.replaceSelection({ imodelKey: imodel.key, source: "", selectables: [modelKey] }));
await ensureTableHasRowsWithCellValues(container, "User Label", ["My Element 1", "My Element 2"]);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ describe("Learning snippets", async () => {
// set up imodel for the test
let modelKey: InstanceKey;
let elementKey: InstanceKey;
// eslint-disable-next-line @typescript-eslint/no-deprecated
const imodel = await buildTestIModel(this, async (builder) => {
const categoryKey = insertSpatialCategory({ builder, fullClassNameSeparator: ":", codeValue: "My Category" });
modelKey = insertPhysicalModelWithPartition({ builder, fullClassNameSeparator: ":", codeValue: "My Model" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { createECSchemaProvider, createECSqlQueryExecutor, createIModelKey } fro
import { createLimitingECSqlQueryExecutor, createNodesQueryClauseFactory, HierarchyDefinition } from "@itwin/presentation-hierarchies";
// __PUBLISH_EXTRACT_END__
// __PUBLISH_EXTRACT_START__ Presentation.HierarchiesReact.SelectionStorage.Imports
import { TreeRenderer, UnifiedSelectionProvider, useIModelUnifiedSelectionTree } from "@itwin/presentation-hierarchies-react";
import { createStorage } from "@itwin/unified-selection";
import { TreeRenderer, useIModelUnifiedSelectionTree } from "@itwin/presentation-hierarchies-react";
import { createStorage, SelectionStorage } from "@itwin/unified-selection";
import { useEffect, useState } from "react";
// __PUBLISH_EXTRACT_END__
// __PUBLISH_EXTRACT_START__ Presentation.HierarchiesReact.CustomTreeExample.Imports
Expand Down Expand Up @@ -83,7 +83,7 @@ describe("Hierarchies React", () => {
it("Tree", async function () {
// __PUBLISH_EXTRACT_START__ Presentation.HierarchiesReact.SelectionStorage
// Not part of the package - this should be created once and reused across different components of the application.
const selectionStorage = createStorage();
const unifiedSelectionStorage = createStorage();

/** Component providing the selection storage and access to iModel. Usually this is done in a top-level component. */
function MyTreeComponent({ imodel }: { imodel: IModelConnection }) {
Expand All @@ -96,11 +96,7 @@ describe("Hierarchies React", () => {
return null;
}

return (
<UnifiedSelectionProvider storage={selectionStorage}>
<MyTreeComponentInternal imodelAccess={imodelAccess} />
</UnifiedSelectionProvider>
);
return <MyTreeComponentInternal imodelAccess={imodelAccess} selectionStorage={unifiedSelectionStorage} />;
}
// __PUBLISH_EXTRACT_END__
// __PUBLISH_EXTRACT_START__ Presentation.HierarchiesReact.CustomTreeExample
Expand Down Expand Up @@ -136,8 +132,10 @@ describe("Hierarchies React", () => {
}

/** Internal component that creates and renders tree state. */
function MyTreeComponentInternal({ imodelAccess }: { imodelAccess: IModelAccess }) {
function MyTreeComponentInternal({ imodelAccess, selectionStorage }: { imodelAccess: IModelAccess; selectionStorage: SelectionStorage }) {
const { rootNodes, setFormatter, isLoading, ...state } = useIModelUnifiedSelectionTree({
// the unified selection storage used by all app components let them share selection state
selectionStorage,
// the source name is used to distinguish selection changes being made by different components
sourceName: "MyTreeComponent",
// iModel access is used to build the hierarchy
Expand All @@ -158,34 +156,6 @@ describe("Hierarchies React", () => {
expect(getByText("My Model A")).to.not.be.null;
expect(getByText("My Model B")).to.not.be.null;
});

it("useIModelUnifiedSelectionTree", async function () {
type IModelAccess = Props<typeof useIModelUnifiedSelectionTree>["imodelAccess"];
const getHierarchyDefinition = () => ({
defineHierarchyLevel: async () => [],
});

// __PUBLISH_EXTRACT_START__ Presentation.HierarchiesReact.UseUnifiedSelectionTree
function MyTreeComponentInternal({ imodelAccess }: { imodelAccess: IModelAccess }) {
const { rootNodes, ...state } = useIModelUnifiedSelectionTree({
// the source name is used to distinguish selection changes being made by different components
sourceName: "MyTreeComponent",
// iModel access is used to build the hierarchy
imodelAccess,
// the hierarchy definition describes the hierarchy using ECSQL queries
getHierarchyDefinition,
});
if (!rootNodes || !rootNodes.length) {
return "No data to display";
}
return <TreeRenderer {...state} rootNodes={rootNodes} />;
}
// __PUBLISH_EXTRACT_END__

const { getByText } = render(<MyTreeComponentInternal imodelAccess={createIModelAccess(iModel)} />);

await waitFor(() => expect(getByText("No data to display")).to.not.be.null);
});
});
});
});
Loading
Loading