Improve workspace navigation and render isolation
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { toggleDesktopSidebarsWithCheckoutIntent } from "./desktop-sidebar-toggle";
|
||||
|
||||
describe("toggleDesktopSidebarsWithCheckoutIntent", () => {
|
||||
it("closes both sidebars when either desktop sidebar is open", () => {
|
||||
const openAgentList = vi.fn();
|
||||
const closeAgentList = vi.fn();
|
||||
const closeFileExplorer = vi.fn();
|
||||
const toggleFocusedFileExplorer = vi.fn(() => true);
|
||||
|
||||
const handled = toggleDesktopSidebarsWithCheckoutIntent({
|
||||
isAgentListOpen: true,
|
||||
isFileExplorerOpen: false,
|
||||
openAgentList,
|
||||
closeAgentList,
|
||||
closeFileExplorer,
|
||||
toggleFocusedFileExplorer,
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(closeAgentList).toHaveBeenCalledTimes(1);
|
||||
expect(closeFileExplorer).toHaveBeenCalledTimes(1);
|
||||
expect(openAgentList).not.toHaveBeenCalled();
|
||||
expect(toggleFocusedFileExplorer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens the right sidebar only through the focused checkout-aware handler", () => {
|
||||
const openAgentList = vi.fn();
|
||||
const closeAgentList = vi.fn();
|
||||
const closeFileExplorer = vi.fn();
|
||||
const toggleFocusedFileExplorer = vi.fn(() => false);
|
||||
|
||||
const handled = toggleDesktopSidebarsWithCheckoutIntent({
|
||||
isAgentListOpen: false,
|
||||
isFileExplorerOpen: false,
|
||||
openAgentList,
|
||||
closeAgentList,
|
||||
closeFileExplorer,
|
||||
toggleFocusedFileExplorer,
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(openAgentList).toHaveBeenCalledTimes(1);
|
||||
expect(toggleFocusedFileExplorer).toHaveBeenCalledTimes(1);
|
||||
expect(closeAgentList).not.toHaveBeenCalled();
|
||||
expect(closeFileExplorer).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
interface DesktopSidebarToggleInput {
|
||||
isAgentListOpen: boolean;
|
||||
isFileExplorerOpen: boolean;
|
||||
openAgentList: () => void;
|
||||
closeAgentList: () => void;
|
||||
closeFileExplorer: () => void;
|
||||
toggleFocusedFileExplorer: () => boolean;
|
||||
}
|
||||
|
||||
export function toggleDesktopSidebarsWithCheckoutIntent(input: DesktopSidebarToggleInput): boolean {
|
||||
if (input.isAgentListOpen || input.isFileExplorerOpen) {
|
||||
input.closeAgentList();
|
||||
input.closeFileExplorer();
|
||||
return true;
|
||||
}
|
||||
|
||||
input.openAgentList();
|
||||
input.toggleFocusedFileExplorer();
|
||||
return true;
|
||||
}
|
||||
@@ -55,13 +55,12 @@ describe("buildSidebarProjectRowModel", () => {
|
||||
expect(result).toEqual({
|
||||
kind: "workspace_link",
|
||||
workspace: flattenedWorkspace,
|
||||
selected: false,
|
||||
chevron: null,
|
||||
trailingAction: "none",
|
||||
});
|
||||
});
|
||||
|
||||
it("marks flattened non-git project rows as selected when their workspace is active", () => {
|
||||
it("builds flattened non-git rows without route selection input", () => {
|
||||
const flattenedWorkspace = workspace({
|
||||
serverId: "srv-2",
|
||||
workspaceId: "ws-non-git",
|
||||
@@ -73,17 +72,15 @@ describe("buildSidebarProjectRowModel", () => {
|
||||
workspaces: [flattenedWorkspace],
|
||||
}),
|
||||
collapsed: false,
|
||||
serverId: "srv-2",
|
||||
activeWorkspaceSelection: {
|
||||
serverId: "srv-2",
|
||||
workspaceId: "ws-non-git",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
kind: "workspace_link",
|
||||
selected: true,
|
||||
workspace: flattenedWorkspace,
|
||||
chevron: null,
|
||||
trailingAction: "none",
|
||||
});
|
||||
expect(result).not.toHaveProperty("selected");
|
||||
});
|
||||
|
||||
it("keeps single-workspace git projects as sections with the new worktree action", () => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
export interface SidebarProjectWorkspaceLinkRowModel {
|
||||
kind: "workspace_link";
|
||||
workspace: SidebarWorkspaceEntry;
|
||||
selected: boolean;
|
||||
chevron: null;
|
||||
trailingAction: "new_worktree" | "none";
|
||||
}
|
||||
@@ -28,23 +27,15 @@ export function isSidebarProjectFlattened(project: SidebarProjectEntry): boolean
|
||||
export function buildSidebarProjectRowModel(input: {
|
||||
project: SidebarProjectEntry;
|
||||
collapsed: boolean;
|
||||
serverId?: string | null;
|
||||
activeWorkspaceSelection?: { serverId: string; workspaceId: string } | null;
|
||||
}): SidebarProjectRowModel {
|
||||
const flattenedWorkspace = isSidebarProjectFlattened(input.project)
|
||||
? (input.project.workspaces[0] ?? null)
|
||||
: null;
|
||||
const selected =
|
||||
flattenedWorkspace !== null &&
|
||||
Boolean(input.serverId) &&
|
||||
input.activeWorkspaceSelection?.serverId === input.serverId &&
|
||||
input.activeWorkspaceSelection?.workspaceId === flattenedWorkspace.workspaceId;
|
||||
|
||||
if (flattenedWorkspace) {
|
||||
return {
|
||||
kind: "workspace_link",
|
||||
workspace: flattenedWorkspace,
|
||||
selected,
|
||||
chevron: null,
|
||||
trailingAction: input.project.projectKind === "git" ? "new_worktree" : "none",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { router } from "expo-router";
|
||||
import { getNavigationActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { buildWorkspaceArchiveRedirectRoute } from "@/utils/workspace-archive-navigation";
|
||||
|
||||
export function redirectIfArchivingActiveWorkspace(input: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
}): boolean {
|
||||
const activeWorkspaceSelection = getNavigationActiveWorkspaceSelection();
|
||||
if (
|
||||
activeWorkspaceSelection?.serverId !== input.serverId ||
|
||||
activeWorkspaceSelection.workspaceId !== input.workspaceId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
router.replace(
|
||||
buildWorkspaceArchiveRedirectRoute({
|
||||
serverId: input.serverId,
|
||||
archivedWorkspaceId: input.workspaceId,
|
||||
workspaces: useSessionStore.getState().sessions[input.serverId]?.workspaces.values() ?? [],
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -1,9 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { DaemonClient } from "@server/client/daemon-client";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
buildWorkspaceArchiveRedirectRoute,
|
||||
resolveWorkspaceArchiveRedirectWorkspaceId,
|
||||
} from "@/utils/workspace-archive-navigation";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
activateNavigationWorkspaceSelection,
|
||||
syncNavigationActiveWorkspace,
|
||||
} from "@/stores/navigation-active-workspace-store";
|
||||
import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect";
|
||||
|
||||
const { replaceMock } = vi.hoisted(() => ({
|
||||
replaceMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("expo-router", () => ({
|
||||
router: {
|
||||
replace: replaceMock,
|
||||
},
|
||||
}));
|
||||
|
||||
function workspace(
|
||||
input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">,
|
||||
@@ -76,3 +93,53 @@ describe("resolveWorkspaceArchiveRedirectWorkspaceId", () => {
|
||||
).toBe("/h/server-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("redirectIfArchivingActiveWorkspace", () => {
|
||||
afterEach(() => {
|
||||
replaceMock.mockClear();
|
||||
syncNavigationActiveWorkspace({ current: null });
|
||||
useSessionStore.getState().clearSession("server-1");
|
||||
});
|
||||
|
||||
it("does not replace the route when archiving an inactive workspace", () => {
|
||||
useSessionStore.getState().initializeSession("server-1", null as unknown as DaemonClient);
|
||||
useSessionStore.getState().setWorkspaces(
|
||||
"server-1",
|
||||
new Map([
|
||||
["main", workspace({ id: "main", workspaceKind: "local_checkout" })],
|
||||
["feature", workspace({ id: "feature", name: "feature" })],
|
||||
]),
|
||||
);
|
||||
activateNavigationWorkspaceSelection({ serverId: "server-1", workspaceId: "main" });
|
||||
|
||||
expect(
|
||||
redirectIfArchivingActiveWorkspace({
|
||||
serverId: "server-1",
|
||||
workspaceId: "feature",
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(replaceMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("replaces the route at action time when archiving the active workspace", () => {
|
||||
useSessionStore.getState().initializeSession("server-1", null as unknown as DaemonClient);
|
||||
useSessionStore.getState().setWorkspaces(
|
||||
"server-1",
|
||||
new Map([
|
||||
["main", workspace({ id: "main", workspaceKind: "local_checkout" })],
|
||||
["feature", workspace({ id: "feature", name: "feature" })],
|
||||
]),
|
||||
);
|
||||
activateNavigationWorkspaceSelection({ serverId: "server-1", workspaceId: "feature" });
|
||||
|
||||
expect(
|
||||
redirectIfArchivingActiveWorkspace({
|
||||
serverId: "server-1",
|
||||
workspaceId: "feature",
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(replaceMock).toHaveBeenCalledWith("/h/server-1/workspace/main");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user