Keep collapsed projects across sidebar refreshes

Sidebar project lists can be partial while hosts or workspace data are changing. Preserve collapsed preferences instead of treating a temporarily missing project as a request to expand it.
This commit is contained in:
Mohamed Boudra
2026-06-20 22:26:17 +07:00
parent 25252d1b86
commit 7f74853174
3 changed files with 106 additions and 20 deletions
+1 -1
View File
@@ -213,7 +213,7 @@ export const LeftSidebar = memo(function LeftSidebar({
enabled: isCompactLayout || isOpen,
});
const { collapsedProjectKeys, shortcutIndexByWorkspaceKey, toggleProjectCollapsed } =
useSidebarShortcutModel({ projects, isInitialLoad });
useSidebarShortcutModel({ projects });
const groupMode = useSidebarViewStore((state) =>
activeServerId ? state.getGroupMode(activeServerId) : "project",
@@ -0,0 +1,102 @@
/**
* @vitest-environment jsdom
*/
import React from "react";
import { act } from "@testing-library/react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type {
SidebarProjectEntry,
SidebarWorkspaceEntry,
} from "@/hooks/use-sidebar-workspaces-list";
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
import { useSidebarShortcutModel } from "./use-sidebar-shortcut-model";
function workspace(projectKey: string, workspaceId: string): SidebarWorkspaceEntry {
return {
workspaceKey: `srv:${workspaceId}`,
serverId: "srv",
workspaceId,
projectKey,
projectRootPath: `/repo/${projectKey}`,
workspaceDirectory: `/repo/${projectKey}/${workspaceId}`,
projectKind: "git",
workspaceKind: "worktree",
name: workspaceId,
title: null,
currentBranch: null,
statusBucket: "done",
statusEnteredAt: null,
archivingAt: null,
diffStat: null,
prHint: null,
archiveHasUncommittedChanges: null,
archiveUnpushedCommitCount: null,
scripts: [],
hasRunningScripts: false,
};
}
function project(projectKey: string): SidebarProjectEntry {
return {
projectKey,
projectName: projectKey,
projectKind: "git",
iconWorkingDir: `/repo/${projectKey}`,
canCreateWorktree: true,
workspaces: [workspace(projectKey, `${projectKey}-main`)],
};
}
const PROJECTS_BOTH = [project("p1"), project("p2")];
const PROJECTS_ONLY_SECOND = [project("p2")];
function Probe({ projectSet }: { projectSet: "both" | "onlySecond" }) {
const projects = projectSet === "both" ? PROJECTS_BOTH : PROJECTS_ONLY_SECOND;
useSidebarShortcutModel({ projects });
return null;
}
describe("useSidebarShortcutModel", () => {
let root: Root | null = null;
let container: HTMLElement | null = null;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
useSidebarCollapsedSectionsStore.setState({
collapsedProjectKeys: new Set(),
collapsedStatusGroupKeys: new Set(),
});
});
afterEach(() => {
if (root) {
act(() => {
root?.unmount();
});
}
root = null;
container?.remove();
container = null;
});
it("keeps a collapsed project collapsed when the project list temporarily omits it", async () => {
useSidebarCollapsedSectionsStore.setState({
collapsedProjectKeys: new Set(["p1"]),
});
await act(async () => {
root?.render(<Probe projectSet="both" />);
});
await act(async () => {
root?.render(<Probe projectSet="onlySecond" />);
});
await act(async () => {
root?.render(<Probe projectSet="both" />);
});
expect(useSidebarCollapsedSectionsStore.getState().collapsedProjectKeys.has("p1")).toBe(true);
});
});
@@ -1,13 +1,10 @@
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import type { SidebarProjectEntry } from "@/hooks/use-sidebar-workspaces-list";
import { buildSidebarShortcutModel } from "@/utils/sidebar-shortcuts";
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
export function useSidebarShortcutModel(input: {
projects: SidebarProjectEntry[];
isInitialLoad: boolean;
}) {
const { projects, isInitialLoad } = input;
export function useSidebarShortcutModel(input: { projects: SidebarProjectEntry[] }) {
const { projects } = input;
const collapsedProjectKeys = useSidebarCollapsedSectionsStore(
(state) => state.collapsedProjectKeys,
);
@@ -27,19 +24,6 @@ export function useSidebarShortcutModel(input: {
[collapsedProjectKeys, projects],
);
useEffect(() => {
if (isInitialLoad || projects.length === 0) {
return;
}
const projectKeys = new Set(projects.map((project) => project.projectKey));
for (const key of collapsedProjectKeys) {
if (!projectKeys.has(key)) {
setProjectCollapsed(key, false);
}
}
}, [collapsedProjectKeys, isInitialLoad, projects, setProjectCollapsed]);
return {
collapsedProjectKeys,
shortcutIndexByWorkspaceKey: shortcutModel.shortcutIndexByWorkspaceKey,