diff --git a/src/bases/TaskListView.ts b/src/bases/TaskListView.ts index 5919804fc..54483270c 100644 --- a/src/bases/TaskListView.ts +++ b/src/bases/TaskListView.ts @@ -2153,14 +2153,11 @@ export class TaskListView extends BasesViewBase { }; } - /** - * Restore ephemeral state after view reload. - * Restores scroll position, collapsed groups, and collapsed sub-groups. - */ - setEphemeralState(state: unknown): void { - if (!isTaskListEphemeralState(state)) return; - super.setEphemeralState(state); + private hasInitializedCollapseState(): boolean { + return this.initializedPrimaryGroupKeys.size > 0 || this.initializedSubGroupKeys.size > 0; + } + private restoreCollapsedStateFromEphemeral(state: TaskListEphemeralState): void { let restoredCollapsedState = false; // Restore collapsed groups immediately @@ -2181,6 +2178,20 @@ export class TaskListView extends BasesViewBase { restoredCollapsedState = restoredCollapsedState || filtered.length > 0; } this.deferCollapseDefaultForNextSnapshot = restoredCollapsedState; + } + + /** + * Restore ephemeral state after view reload. Collapse state is applied only before the + * view builds its first grouping snapshot, so a stale snapshot captured before a render + * cannot undo the collapse default that render seeded. + */ + setEphemeralState(state: unknown): void { + if (!isTaskListEphemeralState(state)) return; + super.setEphemeralState(state); + + if (!this.hasInitializedCollapseState()) { + this.restoreCollapsedStateFromEphemeral(state); + } // Restore scroll position after render completes if (typeof state.scrollTop === "number" && this.rootElement) { diff --git a/tests/unit/ui/TaskListView.groupCollapse.test.ts b/tests/unit/ui/TaskListView.groupCollapse.test.ts index 731e2056d..f6fa9de63 100644 --- a/tests/unit/ui/TaskListView.groupCollapse.test.ts +++ b/tests/unit/ui/TaskListView.groupCollapse.test.ts @@ -102,6 +102,45 @@ describe("TaskListView group collapse controls", () => { expect((view as any).collapsedSubGroups.has("Open:Urgent")).toBe(true); }); + it("keeps the collapsed default when a pre-render ephemeral snapshot is restored after seeding", () => { + const view = createView(); + (view as any).defaultCollapsedState = "Collapsed"; + + const savedState = view.getEphemeralState(); + (view as any).initializeCollapseStateForSnapshot( + ["Open", "Done"], + new Map([ + ["Open", ["Open:Urgent"]], + ["Done", ["Done:Later"]], + ]) + ); + view.setEphemeralState(savedState); + + expect((view as any).collapsedGroups.has("Open")).toBe(true); + expect((view as any).collapsedGroups.has("Done")).toBe(true); + expect((view as any).collapsedSubGroups.has("Open:Urgent")).toBe(true); + }); + + it("expands only the toggled group after a pre-render ephemeral snapshot is restored", async () => { + const view = createView(); + (view as any).defaultCollapsedState = "Collapsed"; + + const savedState = view.getEphemeralState(); + (view as any).initializeCollapseStateForSnapshot( + ["Open", "Done"], + new Map([ + ["Open", ["Open:Urgent"]], + ["Done", ["Done:Later"]], + ]) + ); + view.setEphemeralState(savedState); + + await (view as any).handleGroupToggle("Open"); + + expect((view as any).collapsedGroups.has("Open")).toBe(false); + expect((view as any).collapsedGroups.has("Done")).toBe(true); + }); + it("reads defaultCollapsedState correctly from config.get", () => { const view = createView(); (view as any).config = {