From ef20ee1882648fd36c8b775106652dd16c5fb55e Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Wed, 21 Dec 2022 18:01:46 +0100 Subject: [PATCH] [EC-63] Implement breadcrumb component (#3762) * [EC-63] feat: scaffold breadcrumb module * [EC-63] feat: add first very basic structure * [EC-63] feat: dynamically rendered crumbs with styling * [EC-63] feat: implement overflow logic * [EC-63] feat: hide overflow and show ellipsis * [EC-63] feat: fully working with links * [EC-63] feat: add support for only showing last crumb * [EC-63] chore: fix missing template * [EC-63] chore: refactor and add test case * [EC-63] refactor: change parent type to treenode * [EC-63] feat: add breadcrumbs to org vault * [EC-63] feat: add links to breadcrumbs (dont work yet) * [EC-63] feat: add support for click handler in breadcrumbs * [EC-63] feat: working breadcrumb links * [EC-63] feat: add collections group head * [EC-63] feat: add breadcrumbs to personal vault * [EC-63] feat: use icon button * [EC-63] feat: use small icon button * [EC-63] fix: add margin to breadcrumb links The reason for this fix is that the bitIconButton used to open the overflow menu is much taller than the rest of the elements in the list. This causes the whole component to grow and shrink depending on if it contains too many breadcrumbs or not. In the web vault this causes the cipher list to jump up and down while navigating. This increases the height of the entire component so that the icon button no longer affects it. * [EC-63] fix: tests using wrong parent * [EC-63] feat: use ngIf instead of else * [EC-63] refactor: attempt to improve tree node factory readability --- .../organizations/vault/vault.component.html | 11 +++ .../organizations/vault/vault.component.ts | 25 +++++ .../breadcrumbs/breadcrumb.component.html | 4 + .../breadcrumbs/breadcrumb.component.ts | 25 +++++ .../breadcrumbs/breadcrumbs.component.html | 81 ++++++++++++++++ .../breadcrumbs/breadcrumbs.component.ts | 39 ++++++++ .../breadcrumbs/breadcrumbs.module.ts | 15 +++ .../breadcrumbs/breadcrumbs.stories.ts | 92 +++++++++++++++++++ apps/web/src/app/shared/shared.module.ts | 4 + .../services/vault-filter.service.spec.ts | 64 +++++++++++-- .../services/vault-filter.service.ts | 8 +- apps/web/src/app/vault/vault.component.html | 11 +++ apps/web/src/app/vault/vault.component.ts | 29 +++++- libs/common/src/misc/serviceUtils.spec.ts | 74 +++++++-------- libs/common/src/misc/serviceUtils.ts | 4 +- libs/common/src/models/domain/tree-node.ts | 4 +- 16 files changed, 431 insertions(+), 59 deletions(-) create mode 100644 apps/web/src/app/shared/components/breadcrumbs/breadcrumb.component.html create mode 100644 apps/web/src/app/shared/components/breadcrumbs/breadcrumb.component.ts create mode 100644 apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.component.html create mode 100644 apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.component.ts create mode 100644 apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.module.ts create mode 100644 apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.stories.ts diff --git a/apps/web/src/app/organizations/vault/vault.component.html b/apps/web/src/app/organizations/vault/vault.component.html index 9445a254f2c..8c9d1d3c84d 100644 --- a/apps/web/src/app/organizations/vault/vault.component.html +++ b/apps/web/src/app/organizations/vault/vault.component.html @@ -16,6 +16,17 @@
+ + + + {{ collection.node.name | i18n }} + {{ collection.node.name }} + +

{{ "vaultItems" | i18n }} diff --git a/apps/web/src/app/organizations/vault/vault.component.ts b/apps/web/src/app/organizations/vault/vault.component.ts index baed8fdcf90..7d52bba53ac 100644 --- a/apps/web/src/app/organizations/vault/vault.component.ts +++ b/apps/web/src/app/organizations/vault/vault.component.ts @@ -21,11 +21,13 @@ import { PasswordRepromptService } from "@bitwarden/common/abstractions/password import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { Organization } from "@bitwarden/common/models/domain/organization"; +import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; import { CipherView } from "@bitwarden/common/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; import { VaultFilterService } from "../../vault/vault-filter/services/abstractions/vault-filter.service"; import { VaultFilter } from "../../vault/vault-filter/shared/models/vault-filter.model"; +import { CollectionFilter } from "../../vault/vault-filter/shared/models/vault-filter.type"; import { EntityEventsComponent } from "../manage/entity-events.component"; import { CollectionDialogResult, @@ -306,6 +308,29 @@ export class VaultComponent implements OnInit, OnDestroy { }); } + get breadcrumbs(): TreeNode[] { + if (!this.activeFilter.selectedCollectionNode) { + return []; + } + + const collections = [this.activeFilter.selectedCollectionNode]; + while (collections[collections.length - 1].parent != undefined) { + collections.push(collections[collections.length - 1].parent); + } + + return collections + .map((c) => c) + .slice(1) // 1 for self + .reverse(); + } + + protected applyCollectionFilter(collection: TreeNode) { + const filter = this.activeFilter; + filter.resetFilter(); + filter.selectedCollectionNode = collection; + this.applyVaultFilter(filter); + } + private go(queryParams: any = null) { if (queryParams == null) { queryParams = { diff --git a/apps/web/src/app/shared/components/breadcrumbs/breadcrumb.component.html b/apps/web/src/app/shared/components/breadcrumbs/breadcrumb.component.html new file mode 100644 index 00000000000..5291f0cab4c --- /dev/null +++ b/apps/web/src/app/shared/components/breadcrumbs/breadcrumb.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/app/shared/components/breadcrumbs/breadcrumb.component.ts b/apps/web/src/app/shared/components/breadcrumbs/breadcrumb.component.ts new file mode 100644 index 00000000000..060154b4f69 --- /dev/null +++ b/apps/web/src/app/shared/components/breadcrumbs/breadcrumb.component.ts @@ -0,0 +1,25 @@ +import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core"; + +@Component({ + selector: "bit-breadcrumb", + templateUrl: "./breadcrumb.component.html", +}) +export class BreadcrumbComponent { + @Input() + icon?: string; + + @Input() + route?: string | any[] = undefined; + + @Input() + queryParams?: Record = {}; + + @Output() + click = new EventEmitter(); + + @ViewChild(TemplateRef, { static: true }) content: TemplateRef; + + onClick(args: unknown) { + this.click.next(args); + } +} diff --git a/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.component.html b/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.component.html new file mode 100644 index 00000000000..e291c4a9b97 --- /dev/null +++ b/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.component.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.component.ts b/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.component.ts new file mode 100644 index 00000000000..64ca8146c80 --- /dev/null +++ b/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.component.ts @@ -0,0 +1,39 @@ +import { Component, ContentChildren, Input, QueryList } from "@angular/core"; + +import { BreadcrumbComponent } from "./breadcrumb.component"; + +@Component({ + selector: "bit-breadcrumbs", + templateUrl: "./breadcrumbs.component.html", +}) +export class BreadcrumbsComponent { + @Input() + show = 3; + + private breadcrumbs: BreadcrumbComponent[] = []; + + @ContentChildren(BreadcrumbComponent) + protected set breadcrumbList(value: QueryList) { + this.breadcrumbs = value.toArray(); + } + + protected get beforeOverflow() { + if (this.hasOverflow) { + return this.breadcrumbs.slice(0, this.show - 1); + } + + return this.breadcrumbs; + } + + protected get overflow() { + return this.breadcrumbs.slice(this.show - 1, -1); + } + + protected get afterOverflow() { + return this.breadcrumbs.slice(-1); + } + + protected get hasOverflow() { + return this.breadcrumbs.length > this.show; + } +} diff --git a/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.module.ts b/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.module.ts new file mode 100644 index 00000000000..d609baf6262 --- /dev/null +++ b/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.module.ts @@ -0,0 +1,15 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { IconButtonModule, LinkModule, MenuModule } from "@bitwarden/components"; + +import { BreadcrumbComponent } from "./breadcrumb.component"; +import { BreadcrumbsComponent } from "./breadcrumbs.component"; + +@NgModule({ + imports: [CommonModule, LinkModule, IconButtonModule, MenuModule, RouterModule], + declarations: [BreadcrumbsComponent, BreadcrumbComponent], + exports: [BreadcrumbsComponent, BreadcrumbComponent], +}) +export class BreadcrumbsModule {} diff --git a/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.stories.ts b/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.stories.ts new file mode 100644 index 00000000000..2d306a79ffa --- /dev/null +++ b/apps/web/src/app/shared/components/breadcrumbs/breadcrumbs.stories.ts @@ -0,0 +1,92 @@ +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { Meta, Story, moduleMetadata } from "@storybook/angular"; + +import { IconButtonModule, LinkModule, MenuModule } from "@bitwarden/components"; + +import { PreloadedEnglishI18nModule } from "../../../tests/preloaded-english-i18n.module"; + +import { BreadcrumbComponent } from "./breadcrumb.component"; +import { BreadcrumbsComponent } from "./breadcrumbs.component"; + +interface Breadcrumb { + icon?: string; + name: string; + route: string; +} + +@Component({ + template: "", +}) +class EmptyComponent {} + +export default { + title: "Web/Breadcrumbs", + component: BreadcrumbsComponent, + decorators: [ + moduleMetadata({ + declarations: [BreadcrumbComponent], + imports: [ + LinkModule, + MenuModule, + IconButtonModule, + PreloadedEnglishI18nModule, + RouterModule.forRoot([{ path: "**", component: EmptyComponent }], { useHash: true }), + ], + }), + ], + args: { + items: [], + }, + argTypes: { + breadcrumbs: { + table: { disable: true }, + }, + click: { action: "clicked" }, + }, +} as Meta; + +const Template: Story = (args: BreadcrumbsComponent) => ({ + props: args, + template: ` +

Router links

+

+ + {{item.name}} + +

+ +

Click emit

+

+ + {{item.name}} + +

+ `, +}); + +export const TopLevel = Template.bind({}); +TopLevel.args = { + items: [{ icon: "bwi-star", name: "Top Level" }] as Breadcrumb[], +}; + +export const SecondLevel = Template.bind({}); +SecondLevel.args = { + items: [ + { name: "Acme Vault", route: "/" }, + { icon: "bwi-collection", name: "Collection", route: "collection" }, + ] as Breadcrumb[], +}; + +export const Overflow = Template.bind({}); +Overflow.args = { + items: [ + { name: "Acme Vault", route: "" }, + { icon: "bwi-collection", name: "Collection", route: "collection" }, + { icon: "bwi-collection", name: "Middle-Collection 1", route: "middle-collection-1" }, + { icon: "bwi-collection", name: "Middle-Collection 2", route: "middle-collection-2" }, + { icon: "bwi-collection", name: "Middle-Collection 3", route: "middle-collection-3" }, + { icon: "bwi-collection", name: "Middle-Collection 4", route: "middle-collection-4" }, + { icon: "bwi-collection", name: "End Collection", route: "end-collection" }, + ] as Breadcrumb[], +}; diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index 0f3e87fd31d..491fbf9160e 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -28,6 +28,8 @@ import { ColorPasswordModule, } from "@bitwarden/components"; +import { BreadcrumbsModule } from "./components/breadcrumbs/breadcrumbs.module"; + // Register the locales for the application import "./locales"; @@ -71,6 +73,7 @@ import "./locales"; ColorPasswordModule, // Web specific + BreadcrumbsModule, ], exports: [ CommonModule, @@ -104,6 +107,7 @@ import "./locales"; ColorPasswordModule, // Web specific + BreadcrumbsModule, ], providers: [DatePipe], bootstrap: [], diff --git a/apps/web/src/app/vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/vault-filter/services/vault-filter.service.spec.ts index a0fa600f835..9136e9d1b20 100644 --- a/apps/web/src/app/vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/vault-filter/services/vault-filter.service.spec.ts @@ -186,20 +186,70 @@ describe("vault filter service", () => { }); describe("collection tree", () => { - it("returns a nested tree", async () => { + it("returns tree with children", async () => { const storedCollections = [ - createCollectionView("Collection 1 Id", "Collection 1", "org test id"), - createCollectionView("Collection 2 Id", "Collection 1/Collection 2", "org test id"), - createCollectionView("Collection 3 Id", "Collection 1/Collection 3", "org test id"), + createCollectionView("id-1", "Collection 1", "org test id"), + createCollectionView("id-2", "Collection 1/Collection 2", "org test id"), + createCollectionView("id-3", "Collection 1/Collection 3", "org test id"), ]; collectionService.getAllDecrypted.mockResolvedValue(storedCollections); vaultFilterService.reloadCollections(); const result = await firstValueFrom(vaultFilterService.collectionTree$); - expect(result.children[0].node.id === "Collection 1 Id"); - expect(result.children[0].children.find((c) => c.node.id === "Collection 2 Id")); - expect(result.children[0].children.find((c) => c.node.id === "Collection 3 Id")); + expect(result.children.map((c) => c.node.id)).toEqual(["id-1"]); + expect(result.children[0].children.map((c) => c.node.id)).toEqual(["id-2", "id-3"]); + }); + + it("returns tree where non-existing collections are excluded from children", async () => { + const storedCollections = [ + createCollectionView("id-1", "Collection 1", "org test id"), + createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"), + ]; + collectionService.getAllDecrypted.mockResolvedValue(storedCollections); + vaultFilterService.reloadCollections(); + + const result = await firstValueFrom(vaultFilterService.collectionTree$); + + expect(result.children.map((c) => c.node.id)).toEqual(["id-1"]); + expect(result.children[0].children.map((c) => c.node.id)).toEqual(["id-3"]); + expect(result.children[0].children[0].node.name).toBe("Collection 2/Collection 3"); + }); + + it("returns tree with parents", async () => { + const storedCollections = [ + createCollectionView("id-1", "Collection 1", "org test id"), + createCollectionView("id-2", "Collection 1/Collection 2", "org test id"), + createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"), + createCollectionView("id-4", "Collection 1/Collection 4", "org test id"), + ]; + collectionService.getAllDecrypted.mockResolvedValue(storedCollections); + vaultFilterService.reloadCollections(); + + const result = await firstValueFrom(vaultFilterService.collectionTree$); + + const c1 = result.children[0]; + const c2 = c1.children[0]; + const c3 = c2.children[0]; + const c4 = c1.children[1]; + expect(c2.parent.node.id).toEqual("id-1"); + expect(c3.parent.node.id).toEqual("id-2"); + expect(c4.parent.node.id).toEqual("id-1"); + }); + + it("returns tree where non-existing collections are excluded from parents", async () => { + const storedCollections = [ + createCollectionView("id-1", "Collection 1", "org test id"), + createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"), + ]; + collectionService.getAllDecrypted.mockResolvedValue(storedCollections); + vaultFilterService.reloadCollections(); + + const result = await firstValueFrom(vaultFilterService.collectionTree$); + + const c1 = result.children[0]; + const c3 = c1.children[0]; + expect(c3.parent.node.id).toEqual("id-1"); }); }); }); diff --git a/apps/web/src/app/vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/vault-filter/services/vault-filter.service.ts index e0e433d6ede..7b2ade9da73 100644 --- a/apps/web/src/app/vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/vault-filter/services/vault-filter.service.ts @@ -135,7 +135,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { orgs.forEach((org) => { const orgCopy = org as OrganizationFilter; orgCopy.icon = "bwi-business"; - const node = new TreeNode(orgCopy, headNode.node, orgCopy.name); + const node = new TreeNode(orgCopy, headNode, orgCopy.name); headNode.children.push(node); }); } @@ -163,7 +163,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { ): Observable> { const headNode = new TreeNode(head, null); array?.forEach((filter) => { - const node = new TreeNode(filter, head, filter.name); + const node = new TreeNode(filter, headNode, filter.name); headNode.children.push(node); }); return of(headNode); @@ -196,7 +196,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter); }); nodes.forEach((n) => { - n.parent = headNode.node; + n.parent = headNode; headNode.children.push(n); }); return headNode; @@ -239,7 +239,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { }); nodes.forEach((n) => { - n.parent = headNode.node; + n.parent = headNode; headNode.children.push(n); }); return headNode; diff --git a/apps/web/src/app/vault/vault.component.html b/apps/web/src/app/vault/vault.component.html index 2f3b9cbfc00..0a9833e56e6 100644 --- a/apps/web/src/app/vault/vault.component.html +++ b/apps/web/src/app/vault/vault.component.html @@ -17,6 +17,17 @@

+ + + + {{ collection.node.name | i18n }} + {{ collection.node.name }} + +

{{ "vaultItems" | i18n }} diff --git a/apps/web/src/app/vault/vault.component.ts b/apps/web/src/app/vault/vault.component.ts index 2f407ecf9c6..ee6753d3487 100644 --- a/apps/web/src/app/vault/vault.component.ts +++ b/apps/web/src/app/vault/vault.component.ts @@ -37,7 +37,11 @@ import { ShareComponent } from "./share.component"; import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component"; import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service"; import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model"; -import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type"; +import { + CollectionFilter, + FolderFilter, + OrganizationFilter, +} from "./vault-filter/shared/models/vault-filter.type"; import { VaultItemsComponent } from "./vault-items.component"; const BroadcasterSubscriptionId = "VaultComponent"; @@ -380,6 +384,29 @@ export class VaultComponent implements OnInit, OnDestroy { await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef); } + get breadcrumbs(): TreeNode[] { + if (!this.activeFilter.selectedCollectionNode) { + return []; + } + + const collections = [this.activeFilter.selectedCollectionNode]; + while (collections[collections.length - 1].parent != undefined) { + collections.push(collections[collections.length - 1].parent); + } + + return collections + .map((c) => c) + .slice(1) // 1 for self + .reverse(); + } + + protected applyCollectionFilter(collection: TreeNode) { + const filter = this.activeFilter; + filter.resetFilter(); + filter.selectedCollectionNode = collection; + this.applyVaultFilter(filter); + } + private go(queryParams: any = null) { if (queryParams == null) { queryParams = { diff --git a/libs/common/src/misc/serviceUtils.spec.ts b/libs/common/src/misc/serviceUtils.spec.ts index 592171507b7..7cfa318f8c3 100644 --- a/libs/common/src/misc/serviceUtils.spec.ts +++ b/libs/common/src/misc/serviceUtils.spec.ts @@ -1,56 +1,27 @@ -import { TreeNode } from "../models/domain/tree-node"; +import { ITreeNodeObject, TreeNode } from "../models/domain/tree-node"; import { ServiceUtils } from "./serviceUtils"; +type FakeObject = { id: string; name: string }; + describe("serviceUtils", () => { - type fakeObject = { id: string; name: string }; - let nodeTree: TreeNode[]; + let nodeTree: TreeNode[]; beforeEach(() => { nodeTree = [ - { - parent: null, - node: { id: "1", name: "1" }, - children: [ - { - parent: { id: "1", name: "1" }, - node: { id: "1.1", name: "1.1" }, - children: [ - { - parent: { id: "1.1", name: "1.1" }, - node: { id: "1.1.1", name: "1.1.1" }, - children: [], - }, - ], - }, - { - parent: { id: "1", name: "1" }, - node: { id: "1.2", name: "1.2" }, - children: [], - }, - ], - }, - { - parent: null, - node: { id: "2", name: "2" }, - children: [ - { - parent: { id: "2", name: "2" }, - node: { id: "2.1", name: "2.1" }, - children: [], - }, - ], - }, - { - parent: null, - node: { id: "3", name: "3" }, - children: [], - }, + createTreeNode({ id: "1", name: "1" }, [ + createTreeNode({ id: "1.1", name: "1.1" }, [ + createTreeNode({ id: "1.1.1", name: "1.1.1" }), + ]), + createTreeNode({ id: "1.2", name: "1.2" }), + ])(null), + createTreeNode({ id: "2", name: "2" }, [createTreeNode({ id: "2.1", name: "2.1" })])(null), + createTreeNode({ id: "3", name: "3" }, [])(null), ]; }); describe("nestedTraverse", () => { it("should traverse a tree and add a node at the correct position given a valid path", () => { - const nodeToBeAdded: fakeObject = { id: "1.2.1", name: "1.2.1" }; + const nodeToBeAdded: FakeObject = { id: "1.2.1", name: "1.2.1" }; const path = ["1", "1.2", "1.2.1"]; ServiceUtils.nestedTraverse(nodeTree, 0, path, nodeToBeAdded, null, "/"); @@ -58,7 +29,7 @@ describe("serviceUtils", () => { }); it("should combine the path for missing nodes and use as the added node name given an invalid path", () => { - const nodeToBeAdded: fakeObject = { id: "blank", name: "blank" }; + const nodeToBeAdded: FakeObject = { id: "blank", name: "blank" }; const path = ["3", "3.1", "3.1.1"]; ServiceUtils.nestedTraverse(nodeTree, 0, path, nodeToBeAdded, null, "/"); @@ -82,3 +53,20 @@ describe("serviceUtils", () => { }); }); }); + +type TreeNodeFactory = ( + obj: T, + children?: TreeNodeFactoryWithoutParent[] +) => TreeNodeFactoryWithoutParent; + +type TreeNodeFactoryWithoutParent = ( + parent?: TreeNode +) => TreeNode; + +const createTreeNode: TreeNodeFactory = + (obj, children = []) => + (parent) => { + const node = new TreeNode(obj, parent, obj.name, obj.id); + node.children = children.map((childFunc) => childFunc(node)); + return node; + }; diff --git a/libs/common/src/misc/serviceUtils.ts b/libs/common/src/misc/serviceUtils.ts index 8934e842195..ac85cf0853a 100644 --- a/libs/common/src/misc/serviceUtils.ts +++ b/libs/common/src/misc/serviceUtils.ts @@ -15,7 +15,7 @@ export class ServiceUtils { partIndex: number, parts: string[], obj: ITreeNodeObject, - parent: ITreeNodeObject, + parent: TreeNode | undefined, delimiter: string ) { if (parts.length <= partIndex) { @@ -40,7 +40,7 @@ export class ServiceUtils { partIndex + 1, parts, obj, - nodeTree[i].node, + nodeTree[i], delimiter ); return; diff --git a/libs/common/src/models/domain/tree-node.ts b/libs/common/src/models/domain/tree-node.ts index ea36eeafacf..7af1d9e6ab4 100644 --- a/libs/common/src/models/domain/tree-node.ts +++ b/libs/common/src/models/domain/tree-node.ts @@ -1,9 +1,9 @@ export class TreeNode { - parent: T; node: T; + parent: TreeNode; children: TreeNode[] = []; - constructor(node: T, parent: T, name?: string, id?: string) { + constructor(node: T, parent: TreeNode, name?: string, id?: string) { this.parent = parent; this.node = node; if (name) {