From f8eda3cbf2d40dd2f3f5715507de63172a8f60e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Potoc=CC=8Cek?= Date: Mon, 11 May 2026 16:45:38 +0200 Subject: [PATCH 1/2] feat(visualization): cap floor label depth slider at the loaded project's depth Replaces the hardcoded slider max of 8 with a selector that walks the unified map node and returns the deepest folder depth, so sliding all the way right labels every folder level and the cap matches the data. Falls back to 15 when no project is loaded. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../maxFolderDepth.selector.spec.ts | 91 +++++++++++++++++++ .../maxFolderDepth.selector.ts | 25 +++++ .../areaSettingsPanel.component.html | 2 +- .../areaSettingsPanel.component.ts | 2 + 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.spec.ts create mode 100644 visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.ts diff --git a/visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.spec.ts b/visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.spec.ts new file mode 100644 index 0000000000..deb65f60ba --- /dev/null +++ b/visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.spec.ts @@ -0,0 +1,91 @@ +import { TestBed } from "@angular/core/testing" +import { provideMockStore, MockStore } from "@ngrx/store/testing" +import { CodeMapNode, NodeType } from "../../../codeCharta.model" +import { accumulatedDataSelector } from "./accumulatedData.selector" +import { maxFolderDepthSelector } from "./maxFolderDepth.selector" + +function getValue() { + let value: number | undefined + maxFolderDepthSelector.release() + const store = TestBed.inject(MockStore) + store.select(maxFolderDepthSelector).subscribe(v => (value = v)) + return value +} + +describe("maxFolderDepthSelector", () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideMockStore()] + }) + }) + + it("should return undefined when no project is loaded", () => { + const store = TestBed.inject(MockStore) + store.overrideSelector(accumulatedDataSelector, { unifiedMapNode: undefined, unifiedFileMeta: undefined }) + + expect(getValue()).toBeUndefined() + }) + + it("should return 1 for a root with only leaf children", () => { + const root: CodeMapNode = { + name: "root", + type: NodeType.FOLDER, + attributes: {}, + path: "/root", + children: [ + { name: "a.ts", type: NodeType.FILE, attributes: {}, path: "/root/a.ts" }, + { name: "b.ts", type: NodeType.FILE, attributes: {}, path: "/root/b.ts" } + ] + } + const store = TestBed.inject(MockStore) + store.overrideSelector(accumulatedDataSelector, { unifiedMapNode: root, unifiedFileMeta: undefined }) + + expect(getValue()).toBe(1) + }) + + it("should return deepest folder depth + 1 for a nested tree", () => { + const root: CodeMapNode = { + name: "root", + type: NodeType.FOLDER, + attributes: {}, + path: "/root", + children: [ + { + name: "src", + type: NodeType.FOLDER, + attributes: {}, + path: "/root/src", + children: [ + { + name: "deep", + type: NodeType.FOLDER, + attributes: {}, + path: "/root/src/deep", + children: [{ name: "x.ts", type: NodeType.FILE, attributes: {}, path: "/root/src/deep/x.ts" }] + } + ] + }, + { name: "readme.md", type: NodeType.FILE, attributes: {}, path: "/root/readme.md" } + ] + } + const store = TestBed.inject(MockStore) + store.overrideSelector(accumulatedDataSelector, { unifiedMapNode: root, unifiedFileMeta: undefined }) + + // deepest folder ("deep") is at depth 2 → max = 3 + expect(getValue()).toBe(3) + }) + + it("should clamp to at least 1 for a root with no children", () => { + const root: CodeMapNode = { + name: "root", + type: NodeType.FOLDER, + attributes: {}, + path: "/root", + children: [] + } + const store = TestBed.inject(MockStore) + store.overrideSelector(accumulatedDataSelector, { unifiedMapNode: root, unifiedFileMeta: undefined }) + + expect(getValue()).toBe(1) + }) +}) diff --git a/visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.ts b/visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.ts new file mode 100644 index 0000000000..0db1321867 --- /dev/null +++ b/visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.ts @@ -0,0 +1,25 @@ +import { createSelector } from "@ngrx/store" +import { CodeMapNode } from "../../../codeCharta.model" +import { accumulatedDataSelector } from "./accumulatedData.selector" + +function computeMaxFolderDepth(node: CodeMapNode, depth: number): number { + if (!node.children || node.children.length === 0) { + return depth - 1 + } + let max = depth + for (const child of node.children) { + const childDepth = computeMaxFolderDepth(child, depth + 1) + if (childDepth > max) { + max = childDepth + } + } + return max +} + +export const maxFolderDepthSelector = createSelector(accumulatedDataSelector, accumulatedData => { + const root = accumulatedData.unifiedMapNode + if (!root) { + return undefined + } + return Math.max(1, computeMaxFolderDepth(root, 0) + 1) +}) diff --git a/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.html b/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.html index 220c3e67e2..f725b49af9 100644 --- a/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.html +++ b/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.html @@ -14,7 +14,7 @@ [value]="floorLabelDepth$ | async" [onChange]="applyDebouncedFloorLabelDepth" [min]="1" - [max]="8" + [max]="(maxFloorLabelDepth$ | async) ?? 15" > Invert Area diff --git a/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.ts b/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.ts index 9f36ad5e78..f0ecec80ac 100644 --- a/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.ts +++ b/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.ts @@ -6,6 +6,7 @@ import { setEnableFloorLabels } from "../../../state/store/appSettings/enableFlo import { enableFloorLabelsSelector } from "../../../state/store/appSettings/enableFloorLabels/enableFloorLabels.selector" import { setFloorLabelDepth } from "../../../state/store/appSettings/floorLabelDepth/floorLabelDepth.actions" import { floorLabelDepthSelector } from "../../../state/store/appSettings/floorLabelDepth/floorLabelDepth.selector" +import { maxFolderDepthSelector } from "../../../state/selectors/accumulatedData/maxFolderDepth.selector" import { invertAreaSelector } from "../../../state/store/appSettings/invertArea/invertArea.selector" import { debounce } from "../../../util/debounce" import { setInvertArea } from "../../../state/store/appSettings/invertArea/invertArea.actions" @@ -27,6 +28,7 @@ export class AreaSettingsPanelComponent { margin$ = this.store.select(marginSelector) enableFloorLabels$ = this.store.select(enableFloorLabelsSelector) floorLabelDepth$ = this.store.select(floorLabelDepthSelector) + maxFloorLabelDepth$ = this.store.select(maxFolderDepthSelector) isInvertedArea$ = this.store.select(invertAreaSelector) constructor(private store: Store) {} From bac93ac556e70d298137527fb1b6a8b00cafd3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Potoc=CC=8Cek?= Date: Mon, 11 May 2026 19:26:51 +0200 Subject: [PATCH 2/2] test(visualization): await single selector emission in maxFolderDepth spec Replace synchronous subscribe-and-return helper with firstValueFrom + take(1) so each test awaits one emission and the subscription closes. Aligns with the repo's other MockStore-based selector specs and removes a subscription leak. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../maxFolderDepth.selector.spec.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.spec.ts b/visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.spec.ts index deb65f60ba..fa312846fc 100644 --- a/visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.spec.ts +++ b/visualization/app/codeCharta/state/selectors/accumulatedData/maxFolderDepth.selector.spec.ts @@ -1,15 +1,14 @@ import { TestBed } from "@angular/core/testing" import { provideMockStore, MockStore } from "@ngrx/store/testing" +import { firstValueFrom, take } from "rxjs" import { CodeMapNode, NodeType } from "../../../codeCharta.model" import { accumulatedDataSelector } from "./accumulatedData.selector" import { maxFolderDepthSelector } from "./maxFolderDepth.selector" function getValue() { - let value: number | undefined maxFolderDepthSelector.release() const store = TestBed.inject(MockStore) - store.select(maxFolderDepthSelector).subscribe(v => (value = v)) - return value + return firstValueFrom(store.select(maxFolderDepthSelector).pipe(take(1))) } describe("maxFolderDepthSelector", () => { @@ -19,14 +18,14 @@ describe("maxFolderDepthSelector", () => { }) }) - it("should return undefined when no project is loaded", () => { + it("should return undefined when no project is loaded", async () => { const store = TestBed.inject(MockStore) store.overrideSelector(accumulatedDataSelector, { unifiedMapNode: undefined, unifiedFileMeta: undefined }) - expect(getValue()).toBeUndefined() + expect(await getValue()).toBeUndefined() }) - it("should return 1 for a root with only leaf children", () => { + it("should return 1 for a root with only leaf children", async () => { const root: CodeMapNode = { name: "root", type: NodeType.FOLDER, @@ -40,10 +39,10 @@ describe("maxFolderDepthSelector", () => { const store = TestBed.inject(MockStore) store.overrideSelector(accumulatedDataSelector, { unifiedMapNode: root, unifiedFileMeta: undefined }) - expect(getValue()).toBe(1) + expect(await getValue()).toBe(1) }) - it("should return deepest folder depth + 1 for a nested tree", () => { + it("should return deepest folder depth + 1 for a nested tree", async () => { const root: CodeMapNode = { name: "root", type: NodeType.FOLDER, @@ -72,10 +71,10 @@ describe("maxFolderDepthSelector", () => { store.overrideSelector(accumulatedDataSelector, { unifiedMapNode: root, unifiedFileMeta: undefined }) // deepest folder ("deep") is at depth 2 → max = 3 - expect(getValue()).toBe(3) + expect(await getValue()).toBe(3) }) - it("should clamp to at least 1 for a root with no children", () => { + it("should clamp to at least 1 for a root with no children", async () => { const root: CodeMapNode = { name: "root", type: NodeType.FOLDER, @@ -86,6 +85,6 @@ describe("maxFolderDepthSelector", () => { const store = TestBed.inject(MockStore) store.overrideSelector(accumulatedDataSelector, { unifiedMapNode: root, unifiedFileMeta: undefined }) - expect(getValue()).toBe(1) + expect(await getValue()).toBe(1) }) })