Skip to content

Commit

Permalink
Add autoExpand option (#326)
Browse files Browse the repository at this point in the history
* Add autoExpand option

* Add changeset

* Update packages/full-stack-tests/src/hierarchy-builder/grouping/AutoExpand.test.ts

Co-authored-by: Grigas <[email protected]>

* Update packages/hierarchy-builder/src/hierarchy-builder/internal/operators/grouping/AutoExpand.ts

Co-authored-by: Grigas <[email protected]>

* Update packages/hierarchy-builder/src/hierarchy-builder/internal/operators/grouping/AutoExpand.ts

Co-authored-by: Grigas <[email protected]>

* Comment fixes

* Change tests to suggestion

* Group repetitive code

* Fix tests

* Add 100% test coverage

* Fix getAutoExpandOptionsFromNodeProcessingParams

* Update api

* Resolve comments

* Update packages/hierarchy-builder/src/hierarchy-builder/HierarchyNode.ts

Co-authored-by: Grigas <[email protected]>

* Apply suggestion

---------

Co-authored-by: Grigas <[email protected]>
  • Loading branch information
JonasDov and grigasp authored Nov 8, 2023
1 parent 2fadf33 commit 2a413ac
Show file tree
Hide file tree
Showing 12 changed files with 613 additions and 8 deletions.
2 changes: 2 additions & 0 deletions .changeset/pretty-birds-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { Subject } from "@itwin/core-backend";
import { IModel } from "@itwin/core-common";
import { IModelConnection } from "@itwin/core-frontend";
import { ECSqlSelectClauseGroupingParams, IHierarchyLevelDefinitionsFactory, NodeSelectClauseFactory } from "@itwin/presentation-hierarchy-builder";
import { buildIModel, insertSubject } from "../../IModelUtils";
import { initialize, terminate } from "../../IntegrationTests";
import { NodeValidators, validateHierarchy } from "../HierarchyValidation";
import { createProvider } from "../Utils";

describe("Stateless hierarchy builder", () => {
describe("Grouping nodes' autoExpand setting", () => {
let selectClauseFactory: NodeSelectClauseFactory;
let subjectClassName: string;
let emptyIModel: IModelConnection;

before(async function () {
await initialize();
emptyIModel = (await buildIModel(this)).imodel;
subjectClassName = Subject.classFullName.replace(":", ".");
selectClauseFactory = new NodeSelectClauseFactory();
});

after(async () => {
await terminate();
});

function createHierarchyWithSpecifiedGrouping(specifiedGrouping: ECSqlSelectClauseGroupingParams): IHierarchyLevelDefinitionsFactory {
return {
async defineHierarchyLevel(parentNode) {
if (!parentNode) {
return [
{
fullClassName: `BisCore.InformationContentElement`,
query: {
ecsql: `
SELECT ${await selectClauseFactory.createSelectClause({
ecClassId: { selector: `this.ECClassId` },
ecInstanceId: { selector: `this.ECInstanceId` },
nodeLabel: { selector: `this.UserLabel` },
grouping: specifiedGrouping,
})}
FROM ${subjectClassName} this
`,
},
},
];
}
return [];
},
};
}

describe("Base class grouping", () => {
const baseClassAutoExpandAlways: ECSqlSelectClauseGroupingParams = {
byBaseClasses: {
fullClassNames: ["BisCore.InformationReferenceElement"],
autoExpand: "always",
},
};

const baseClassAutoExpandSingleChild: ECSqlSelectClauseGroupingParams = {
byBaseClasses: {
fullClassNames: ["BisCore.InformationReferenceElement"],
autoExpand: "single-child",
},
};

it("grouping nodes' autoExpand option is true when some child has autoExpand set to 'always'", async function () {
await validateHierarchy({
provider: createProvider({ imodel: emptyIModel, hierarchy: createHierarchyWithSpecifiedGrouping(baseClassAutoExpandAlways) }),
expect: [
NodeValidators.createForClassGroupingNode({
label: "Information Reference",
className: "BisCore.InformationReferenceElement",
autoExpand: true,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [{ className: "BisCore.Subject", id: IModel.rootSubjectId }],
children: false,
}),
],
}),
],
});
});

it("grouping nodes' autoExpand option is true when it has one child with autoExpand set to 'single-child'", async function () {
await validateHierarchy({
provider: createProvider({ imodel: emptyIModel, hierarchy: createHierarchyWithSpecifiedGrouping(baseClassAutoExpandSingleChild) }),
expect: [
NodeValidators.createForClassGroupingNode({
label: "Information Reference",
className: "BisCore.InformationReferenceElement",
autoExpand: true,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [{ className: "BisCore.Subject", id: IModel.rootSubjectId }],
children: false,
}),
],
}),
],
});
});

it("grouping nodes' autoExpand option is undefined when none of the child nodes have autoExpand set to 'always'", async function () {
const { imodel, ...keys } = await buildIModel(this, async (builder) => {
const childSubject1 = insertSubject({ builder, codeValue: "A1", parentId: IModel.rootSubjectId });
const childSubject2 = insertSubject({ builder, codeValue: "A2", parentId: IModel.rootSubjectId });
return { childSubject1, childSubject2 };
});

await validateHierarchy({
provider: createProvider({ imodel, hierarchy: createHierarchyWithSpecifiedGrouping(baseClassAutoExpandSingleChild) }),
expect: [
NodeValidators.createForClassGroupingNode({
label: "Information Reference",
className: "BisCore.InformationReferenceElement",
autoExpand: undefined,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [{ className: "BisCore.Subject", id: IModel.rootSubjectId }],
children: false,
}),
NodeValidators.createForInstanceNode({
instanceKeys: [keys.childSubject1],
children: false,
}),
NodeValidators.createForInstanceNode({
instanceKeys: [keys.childSubject2],
children: false,
}),
],
}),
],
});
});
});

describe("Class grouping", () => {
const classAutoExpandAlways: ECSqlSelectClauseGroupingParams = {
byClass: {
autoExpand: "always",
},
};

const classAutoExpandSingleChild: ECSqlSelectClauseGroupingParams = {
byClass: {
autoExpand: "single-child",
},
};

it("grouping nodes' autoExpand option is true when some child has autoExpand set to 'always'", async function () {
await validateHierarchy({
provider: createProvider({ imodel: emptyIModel, hierarchy: createHierarchyWithSpecifiedGrouping(classAutoExpandAlways) }),
expect: [
NodeValidators.createForClassGroupingNode({
className: "BisCore.Subject",
autoExpand: true,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [{ className: "BisCore.Subject", id: IModel.rootSubjectId }],
children: false,
}),
],
}),
],
});
});

it("grouping nodes' autoExpand option is true when it has one child with autoExpand set to 'single-child'", async function () {
await validateHierarchy({
provider: createProvider({ imodel: emptyIModel, hierarchy: createHierarchyWithSpecifiedGrouping(classAutoExpandSingleChild) }),
expect: [
NodeValidators.createForClassGroupingNode({
className: "BisCore.Subject",
autoExpand: true,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [{ className: "BisCore.Subject", id: IModel.rootSubjectId }],
children: false,
}),
],
}),
],
});
});

it("grouping nodes' autoExpand option is undefined when none of the child nodes have autoExpand set to 'always'", async function () {
const { imodel, ...keys } = await buildIModel(this, async (builder) => {
const childSubject1 = insertSubject({ builder, codeValue: "A1", parentId: IModel.rootSubjectId });
const childSubject2 = insertSubject({ builder, codeValue: "A2", parentId: IModel.rootSubjectId });
return { childSubject1, childSubject2 };
});

await validateHierarchy({
provider: createProvider({ imodel, hierarchy: createHierarchyWithSpecifiedGrouping(classAutoExpandSingleChild) }),
expect: [
NodeValidators.createForClassGroupingNode({
className: "BisCore.Subject",
autoExpand: undefined,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [{ className: "BisCore.Subject", id: IModel.rootSubjectId }],
children: false,
}),
NodeValidators.createForInstanceNode({
instanceKeys: [keys.childSubject1],
children: false,
}),
NodeValidators.createForInstanceNode({
instanceKeys: [keys.childSubject2],
children: false,
}),
],
}),
],
});
});
});

describe("Label grouping", () => {
const labelAutoExpandAlways: ECSqlSelectClauseGroupingParams = {
byLabel: {
autoExpand: "always",
},
};

const labelAutoExpandSingleChild: ECSqlSelectClauseGroupingParams = {
byLabel: {
autoExpand: "single-child",
},
};

it("grouping nodes' autoExpand option is true when some child has autoExpand set to 'always'", async function () {
await validateHierarchy({
provider: createProvider({ imodel: emptyIModel, hierarchy: createHierarchyWithSpecifiedGrouping(labelAutoExpandAlways) }),
expect: [
NodeValidators.createForLabelGroupingNode({
autoExpand: true,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [{ className: "BisCore.Subject", id: IModel.rootSubjectId }],
children: false,
}),
],
}),
],
});
});

it("grouping nodes' autoExpand option is true when it has one child with autoExpand set to 'single-child'", async function () {
await validateHierarchy({
provider: createProvider({ imodel: emptyIModel, hierarchy: createHierarchyWithSpecifiedGrouping(labelAutoExpandSingleChild) }),
expect: [
NodeValidators.createForLabelGroupingNode({
autoExpand: true,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [{ className: "BisCore.Subject", id: IModel.rootSubjectId }],
children: false,
}),
],
}),
],
});
});

it("grouping nodes' autoExpand option is undefined when none of the child nodes have autoExpand set to 'always'", async function () {
const groupName = "test1";
const { imodel, ...keys } = await buildIModel(this, async (builder) => {
const childSubject1 = insertSubject({ builder, codeValue: "A1", parentId: IModel.rootSubjectId, userLabel: groupName });
const childSubject2 = insertSubject({ builder, codeValue: "A2", parentId: IModel.rootSubjectId, userLabel: groupName });
return { childSubject1, childSubject2 };
});

await validateHierarchy({
provider: createProvider({ imodel, hierarchy: createHierarchyWithSpecifiedGrouping(labelAutoExpandSingleChild) }),
expect: [
NodeValidators.createForLabelGroupingNode({
autoExpand: true,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [{ className: "BisCore.Subject", id: IModel.rootSubjectId }],
children: false,
}),
],
}),
NodeValidators.createForLabelGroupingNode({
label: groupName,
autoExpand: undefined,
children: [
NodeValidators.createForInstanceNode({
instanceKeys: [keys.childSubject1],
children: false,
}),
NodeValidators.createForInstanceNode({
instanceKeys: [keys.childSubject2],
children: false,
}),
],
}),
],
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ export interface ArrayPropertyAttributes {
minOccurs: number;
}

// @beta
export type AutoExpand = "single-child" | "always";

// @beta
export interface BaseClassGroupingParams extends BaseGroupingParams {
fullClassNames: string[];
}

// @beta
export interface BaseGroupingParams {
autoExpand?: AutoExpand;
hideIfNoSiblings?: boolean;
hideIfOneGroupedNode?: boolean;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/hierarchy-builder/src/hierarchy-builder/HierarchyNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,17 @@ export interface BaseGroupingParams {
hideIfNoSiblings?: boolean;
/** Hiding option that determines whether to hide group nodes which have only one node as its children. */
hideIfOneGroupedNode?: boolean;
/** Option which auto expands grouping nodes' children when it has single child or always. */
autoExpand?: AutoExpand;
}
/**
* Defines possible values for [[BaseGroupingParams.autoExpand]] attribute:
* - `single-child` - set the grouping node to auto-expand if it groups a single node.
* - `always` - always set the grouping node to auto-expand.
* @beta
*/
export type AutoExpand = "single-child" | "always";

/**
* A data structure that represents base class grouping.
* @beta
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ function mergeBaseGroupingParams(
return {
...(lhsBaseGroupingParams?.hideIfOneGroupedNode || rhsBaseGroupingParams?.hideIfOneGroupedNode ? { hideIfOneGroupedNode: true } : undefined),
...(lhsBaseGroupingParams?.hideIfNoSiblings || rhsBaseGroupingParams?.hideIfNoSiblings ? { hideIfNoSiblings: true } : undefined),
...(lhsBaseGroupingParams?.autoExpand || rhsBaseGroupingParams?.autoExpand
? {
autoExpand: lhsBaseGroupingParams?.autoExpand === "always" || rhsBaseGroupingParams?.autoExpand === "always" ? "always" : "single-child",
}
: undefined),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { HierarchyNode, HierarchyNodeKey, ProcessedGroupingHierarchyNode, Proces
import { getLogger } from "../../Logging";
import { IMetadataProvider } from "../../Metadata";
import { createOperatorLoggingNamespace } from "../Common";
import { assignAutoExpand } from "./grouping/AutoExpand";
import { createBaseClassGroupingHandlers } from "./grouping/BaseClassGrouping";
import { createClassGroups } from "./grouping/ClassGrouping";
import { applyGroupHidingParams } from "./grouping/GroupHiding";
Expand Down Expand Up @@ -84,7 +85,7 @@ async function groupInstanceNodes(
for (let i = 0; i < groupingHandlers.length; ++i) {
const currentHandler = groupingHandlers[i];
const nextHandlers = groupingHandlers.slice(i + 1);
const groupings = applyGroupHidingParams(await currentHandler(curr?.ungrouped ?? nodes), extraSiblings);
const groupings = assignAutoExpand(applyGroupHidingParams(await currentHandler(curr?.ungrouped ?? nodes), extraSiblings));
curr = {
groupingType: groupings.groupingType,
grouped: [
Expand Down
Loading

0 comments on commit 2a413ac

Please sign in to comment.