Skip to content
Draft
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
25 changes: 18 additions & 7 deletions src/bases/TaskListView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/ui/TaskListView.groupCollapse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading