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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user