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

Hierarchies: Add a learning page for hierarchy filtering #675

Merged
merged 6 commits into from
Jul 15, 2024
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
20 changes: 20 additions & 0 deletions .changeset/giant-comics-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@itwin/presentation-shared": minor
---

Added an utility `ECSql.createInstanceKeySelector` function to simplify selecting `InstanceKey` objects.

Example usage:

```ts
const reader = queryExecutor.createQueryReader({
ecsql: `
SELECT ${ECSql.createInstanceKeySelector("el")} key
FROM bis.Element el
`,
});
for await (const row of reader) {
const instanceKey: InstanceKey = JSON.parse(row.key);
// do something with instanceKey
}
```
5 changes: 5 additions & 0 deletions .changeset/plenty-days-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@itwin/presentation-hierarchies": patch
---

Fix nodes being erroneously set as filter targets when they had filter target siblings.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-unused-vars */

import { expect } from "chai";
import { insertPhysicalModelWithPartition } from "presentation-test-utilities";
Expand All @@ -13,7 +14,7 @@ import { IModelConnection } from "@itwin/core-frontend";
import { SchemaContext } from "@itwin/ecschema-metadata";
import { ECSchemaRpcLocater } from "@itwin/ecschema-rpcinterface-common";
import { createECSchemaProvider, createECSqlQueryExecutor } from "@itwin/presentation-core-interop";
import { createLimitingECSqlQueryExecutor, createNodesQueryClauseFactory } from "@itwin/presentation-hierarchies";
import { createLimitingECSqlQueryExecutor, createNodesQueryClauseFactory, HierarchyDefinition } from "@itwin/presentation-hierarchies";
// __PUBLISH_EXTRACT_END__
// __PUBLISH_EXTRACT_START__ Presentation.HierarchiesReact.SelectionStorage.Imports
import { TreeRenderer, UnifiedSelectionProvider, useUnifiedSelectionTree } from "@itwin/presentation-hierarchies-react";
Expand Down Expand Up @@ -99,42 +100,46 @@ describe("Hierarchies React", () => {
// __PUBLISH_EXTRACT_START__ Presentation.HierarchiesReact.CustomTreeExample
type IModelAccess = Parameters<typeof useUnifiedSelectionTree>[0]["imodelAccess"];

/** Internal component that defines the hierarchy and creates tree state. */
function MyTreeComponentInternal({ imodelAccess, imodelKey }: { imodelAccess: IModelAccess; imodelKey: string }) {
// The hierarchy definition describes the hierarchy using ECSQL queries; here it just returns all `BisCore.PhysicalModel` instances
function getHierarchyDefinition({ imodelAccess }: { imodelAccess: IModelAccess }): HierarchyDefinition {
// Create a factory for building nodes SELECT query clauses in a format understood by the provider
const [nodesQueryFactory] = useState(createNodesQueryClauseFactory({ imodelAccess }));
const nodesQueryFactory = createNodesQueryClauseFactory({ imodelAccess });
// Create a factory for building labels SELECT query clauses according to BIS conventions
const [labelsQueryFactory] = useState(createBisInstanceLabelSelectClauseFactory({ classHierarchyInspector: imodelAccess }));
const labelsQueryFactory = createBisInstanceLabelSelectClauseFactory({ classHierarchyInspector: imodelAccess });
return {
defineHierarchyLevel: async () => [
{
fullClassName: "BisCore.PhysicalModel",
query: {
ecsql: `
SELECT
${await nodesQueryFactory.createSelectClause({
ecClassId: { selector: "this.ECClassId" },
ecInstanceId: { selector: "this.ECInstanceId" },
nodeLabel: {
selector: await labelsQueryFactory.createSelectClause({ classAlias: "this", className: "BisCore.PhysicalModel" }),
},
hasChildren: false,
})}
FROM BisCore.PhysicalModel this
`,
},
},
],
};
}

const { rootNodes, ...state } = useUnifiedSelectionTree({
/** Internal component that creates and renders tree state. */
function MyTreeComponentInternal({ imodelAccess, imodelKey }: { imodelAccess: IModelAccess; imodelKey: string }) {
const { rootNodes, setFormatter, isLoading, ...state } = useUnifiedSelectionTree({
// the source name is used to distinguish selection changes being made by different components
sourceName: "MyTreeComponent",
// the iModel key is required for unified selection system to distinguish selection changes between different iModels
imodelKey,
// iModel access is used to build the hierarchy
imodelAccess,
// the hierarchy definition describes the hierarchy using ECSQL queries; here it just returns all bis.PhysicalModel instances
getHierarchyDefinition: () => ({
defineHierarchyLevel: async () => [
{
fullClassName: "BisCore.PhysicalModel",
query: {
ecsql: `
SELECT
${await nodesQueryFactory.createSelectClause({
ecClassId: { selector: "this.ECClassId" },
ecInstanceId: { selector: "this.ECInstanceId" },
nodeLabel: {
selector: await labelsQueryFactory.createSelectClause({ classAlias: "this", className: "BisCore.PhysicalModel" }),
},
hasChildren: false,
})}
FROM BisCore.PhysicalModel this
`,
},
},
],
}),
// supply the hierarchy definition
getHierarchyDefinition,
});
if (!rootNodes) {
return "Loading...";
Expand All @@ -144,7 +149,7 @@ describe("Hierarchies React", () => {
// __PUBLISH_EXTRACT_END__

const { getByRole, getByText } = render(<MyTreeComponent imodel={iModel} />);
await waitFor(() => getByRole("tree"), { timeout: 2000 });
await waitFor(() => getByRole("tree"));

expect(getByText("My Model A")).to.not.be.null;
expect(getByText("My Model B")).to.not.be.null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,16 @@ describe("Hierarchies", () => {
NodeValidators.createForInstanceNode({
instanceKeys: [keys.rootSubject],
autoExpand: true,
isFilterTarget: false,
children: [
NodeValidators.createForCustomNode({
key: "custom",
autoExpand: true,
isFilterTarget: false,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [keys.childSubject2],
isFilterTarget: true,
children: false,
}),
],
Expand All @@ -121,11 +124,9 @@ describe("Hierarchies", () => {
});

it("filters custom nodes", async function () {
const { imodel, ...keys } = await buildIModel(this, async (builder) => {
const { imodel, ...keys } = await buildIModel(this, async () => {
const rootSubject = { className: subjectClassName, id: IModel.rootSubjectId };
const childSubject1 = insertSubject({ builder, codeValue: "test subject 1", parentId: rootSubject.id });
const childSubject2 = insertSubject({ builder, codeValue: "test subject 2", parentId: rootSubject.id });
return { rootSubject, childSubject1, childSubject2 };
return { rootSubject };
});

const selectQueryFactory = createNodesQueryClauseFactory({ imodelAccess: createIModelAccess(imodel) });
Expand Down Expand Up @@ -176,10 +177,12 @@ describe("Hierarchies", () => {
NodeValidators.createForInstanceNode({
instanceKeys: [keys.rootSubject],
autoExpand: true,
isFilterTarget: false,
children: [
NodeValidators.createForCustomNode({
key: "custom2",
autoExpand: false,
isFilterTarget: true,
}),
],
}),
Expand Down Expand Up @@ -267,9 +270,11 @@ describe("Hierarchies", () => {
NodeValidators.createForInstanceNode({
instanceKeys: [keys.rootSubject],
autoExpand: true,
isFilterTarget: false,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [keys.childSubject2],
isFilterTarget: true,
children: false,
}),
],
Expand Down Expand Up @@ -353,6 +358,7 @@ describe("Hierarchies", () => {
expect: [
NodeValidators.createForInstanceNode({
instanceKeys: [keys.rootSubject],
isFilterTarget: false,
children: false,
}),
],
Expand Down Expand Up @@ -436,6 +442,7 @@ describe("Hierarchies", () => {
expect: [
NodeValidators.createForInstanceNode({
instanceKeys: [keys.rootSubject],
isFilterTarget: false,
children: false,
}),
],
Expand Down Expand Up @@ -529,10 +536,12 @@ describe("Hierarchies", () => {
NodeValidators.createForInstanceNode({
instanceKeys: [x],
autoExpand: true,
isFilterTarget: true,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [z],
autoExpand: false,
isFilterTarget: false,
children: false,
}),
],
Expand Down Expand Up @@ -618,10 +627,12 @@ describe("Hierarchies", () => {
NodeValidators.createForInstanceNode({
instanceKeys: [x],
autoExpand: true,
isFilterTarget: true,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [y2],
autoExpand: false,
isFilterTarget: false,
children: false,
}),
],
Expand Down
17 changes: 16 additions & 1 deletion apps/full-stack-tests/src/hierarchies/HierarchyValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export namespace NodeValidators {
label?: string | RegExp;
autoExpand?: boolean;
supportsFiltering?: boolean;
isFilterTarget?: boolean;
children?: ExpectedHierarchyDef[] | boolean;
},
) {
Expand Down Expand Up @@ -61,13 +62,24 @@ export namespace NodeValidators {
)}, got ${optionalBooleanToString(node.supportsFiltering)}`,
);
}
if (expectations.isFilterTarget !== undefined && !!node.filtering?.isFilterTarget !== !!expectations.isFilterTarget) {
throw new Error(
`[${node.label}] Expected node's \`filtering.isFilterTarget\` flag to be ${optionalBooleanToString(
expectations.isFilterTarget,
)}, got ${optionalBooleanToString(node.filtering?.isFilterTarget)}`,
);
}
if (expectations.children !== undefined && hasChildren(expectations) !== hasChildren(node)) {
throw new Error(`[${node.label}] Expected node to ${hasChildren(expectations) ? "" : "not "}have children but it does ${hasChildren(node) ? "" : "not"}`);
}
}

export function createForCustomNode<TChildren extends ExpectedHierarchyDef[] | boolean>(
expectedNode: Partial<Omit<NonGroupingHierarchyNode, "label" | "children">> & { label?: string; children?: TChildren },
expectedNode: Partial<Omit<NonGroupingHierarchyNode, "label" | "children" | "filtering">> & {
label?: string;
isFilterTarget?: boolean;
children?: TChildren;
},
) {
return {
node: (node: HierarchyNode) => {
Expand All @@ -81,6 +93,7 @@ export namespace NodeValidators {
label: expectedNode.label,
autoExpand: expectedNode.autoExpand,
supportsFiltering: expectedNode.supportsFiltering,
isFilterTarget: expectedNode.isFilterTarget,
children: expectedNode.children,
});
},
Expand All @@ -93,6 +106,7 @@ export namespace NodeValidators {
label?: string | RegExp;
autoExpand?: boolean;
supportsFiltering?: boolean;
isFilterTarget?: boolean;
children?: TChildren;
}) {
return {
Expand All @@ -116,6 +130,7 @@ export namespace NodeValidators {
label: props.label,
autoExpand: props.autoExpand,
supportsFiltering: props.supportsFiltering,
isFilterTarget: props.isFilterTarget,
children: props.children,
});
},
Expand Down
Loading
Loading