Keep selected projects when switching hosts (#2700)
* fix(app): preserve project selection across hosts Rendered project identity and persisted equivalence were conflated after duplicate-project handling. Keep them separate so host changes resolve selectable equivalents deterministically without resetting legacy sidebar identity. * fix(app): restore selected clone after host round trip Keep manual origin and current destination placements separately so switching back restores the chosen checkout while archive gaps preserve the active host selection. * fix(app): key status icons by project view * fix(app): preserve origin during archive gaps
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createProjectViewKey } from "@/projects/workspace-structure";
|
||||
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import {
|
||||
@@ -25,6 +26,10 @@ import {
|
||||
|
||||
const SERVER_ID = "test-server";
|
||||
|
||||
function equivalenceViewKey(projectKey: string): string {
|
||||
return createProjectViewKey({ kind: "equivalence", projectKey });
|
||||
}
|
||||
|
||||
function createWorkspace(
|
||||
input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">,
|
||||
): WorkspaceDescriptor {
|
||||
@@ -114,10 +119,10 @@ function emptySidebarOrder(): SidebarOrderSnapshot {
|
||||
};
|
||||
}
|
||||
|
||||
function selectWorkspaceStructureProjectKeys(
|
||||
function selectWorkspaceStructureProjectViewKeys(
|
||||
state: Parameters<typeof selectWorkspaceStructureProjects>[0],
|
||||
): string[] {
|
||||
return selectWorkspaceStructureProjects(state, [SERVER_ID]).map((project) => project.projectKey);
|
||||
return selectWorkspaceStructureProjects(state, [SERVER_ID]).map((project) => project.viewKey);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
@@ -348,9 +353,11 @@ describe("workspace structure composition", () => {
|
||||
};
|
||||
initializeWorkspaces([workspace]);
|
||||
|
||||
const emittedProjectKeys = [selectWorkspaceStructureProjectKeys(useSessionStore.getState())];
|
||||
const emittedProjectKeys = [
|
||||
selectWorkspaceStructureProjectViewKeys(useSessionStore.getState()),
|
||||
];
|
||||
const stop = useSessionStore.subscribe((state) => {
|
||||
emittedProjectKeys.push(selectWorkspaceStructureProjectKeys(state));
|
||||
emittedProjectKeys.push(selectWorkspaceStructureProjectViewKeys(state));
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -360,7 +367,10 @@ describe("workspace structure composition", () => {
|
||||
stop();
|
||||
}
|
||||
|
||||
expect(emittedProjectKeys).toEqual([["project-a"], ["project-a"]]);
|
||||
expect(emittedProjectKeys).toEqual([
|
||||
[equivalenceViewKey("project-a")],
|
||||
[equivalenceViewKey("project-a")],
|
||||
]);
|
||||
});
|
||||
|
||||
it("changes for membership updates but not status-only updates", () => {
|
||||
@@ -451,10 +461,13 @@ describe("workspace structure composition", () => {
|
||||
const before = snapshotStructure(SERVER_ID, emptySidebarOrder());
|
||||
const after = snapshotStructure(SERVER_ID, {
|
||||
...emptySidebarOrder(),
|
||||
projectOrder: ["project-b", "project-a"],
|
||||
projectOrder: [equivalenceViewKey("project-b"), equivalenceViewKey("project-a")],
|
||||
});
|
||||
|
||||
expect(after.projects.map((project) => project.projectKey)).toEqual(["project-b", "project-a"]);
|
||||
expect(after.projects.map((project) => project.viewKey)).toEqual([
|
||||
equivalenceViewKey("project-b"),
|
||||
equivalenceViewKey("project-a"),
|
||||
]);
|
||||
expect(after).not.toEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,8 +214,7 @@ export function composeWorkspaceStructure(input: {
|
||||
|
||||
const orderedProjects = applyStoredOrdering({
|
||||
items: input.projects.map((project) => {
|
||||
const workspaceOrder =
|
||||
input.workspaceOrderByScope[project.projectKey] ?? EMPTY_WORKSPACE_KEYS;
|
||||
const workspaceOrder = input.workspaceOrderByScope[project.viewKey] ?? EMPTY_WORKSPACE_KEYS;
|
||||
return {
|
||||
...project,
|
||||
workspaceKeys: applyStoredOrdering({
|
||||
@@ -226,7 +225,7 @@ export function composeWorkspaceStructure(input: {
|
||||
};
|
||||
}),
|
||||
storedOrder: input.projectOrder,
|
||||
getKey: (project) => project.projectKey,
|
||||
getKey: (project) => project.viewKey,
|
||||
});
|
||||
|
||||
return { projects: orderedProjects };
|
||||
|
||||
@@ -7,8 +7,8 @@ interface SidebarOrderStoreState {
|
||||
workspaceOrderByProject: Record<string, string[]>;
|
||||
getProjectOrder: () => string[];
|
||||
setProjectOrder: (keys: string[]) => void;
|
||||
getWorkspaceOrder: (projectKey: string) => string[];
|
||||
setWorkspaceOrder: (projectKey: string, keys: string[]) => void;
|
||||
getWorkspaceOrder: (projectViewKey: string) => string[];
|
||||
setWorkspaceOrder: (projectViewKey: string, keys: string[]) => void;
|
||||
}
|
||||
|
||||
interface SidebarOrderPersistedState {
|
||||
@@ -20,7 +20,7 @@ interface SidebarOrderPersistedState {
|
||||
|
||||
interface SidebarWorkspaceOrderScope {
|
||||
serverId: string;
|
||||
projectKey: string;
|
||||
projectViewKey: string;
|
||||
}
|
||||
|
||||
function normalizeKeys(keys: string[]): string[] {
|
||||
@@ -43,8 +43,8 @@ function normalizeWorkspaceOrderByProject(
|
||||
workspaceOrderByProject: Record<string, string[]> | undefined,
|
||||
): Record<string, string[]> {
|
||||
const normalized: Record<string, string[]> = {};
|
||||
for (const [projectKey, order] of Object.entries(workspaceOrderByProject ?? {})) {
|
||||
const scope = projectKey.trim();
|
||||
for (const [projectViewKey, order] of Object.entries(workspaceOrderByProject ?? {})) {
|
||||
const scope = projectViewKey.trim();
|
||||
if (!scope) continue;
|
||||
normalized[scope] = normalizeKeys(order);
|
||||
}
|
||||
@@ -55,9 +55,9 @@ function extractWorkspaceOrderScope(scopeKey: string): SidebarWorkspaceOrderScop
|
||||
const separatorIndex = scopeKey.indexOf("::");
|
||||
if (separatorIndex < 0) return null;
|
||||
const serverId = scopeKey.slice(0, separatorIndex).trim();
|
||||
const projectKey = scopeKey.slice(separatorIndex + 2).trim();
|
||||
if (!serverId || !projectKey) return null;
|
||||
return { serverId, projectKey };
|
||||
const projectViewKey = scopeKey.slice(separatorIndex + 2).trim();
|
||||
if (!serverId || !projectViewKey) return null;
|
||||
return { serverId, projectViewKey };
|
||||
}
|
||||
|
||||
function normalizeLegacyWorkspaceKey(serverId: string, rawWorkspaceKey: string): string | null {
|
||||
@@ -91,7 +91,7 @@ export function migrateSidebarOrderState(persistedState: unknown): {
|
||||
for (const [scopeKey, order] of Object.entries(state.workspaceOrderByServerAndProject ?? {})) {
|
||||
const scope = extractWorkspaceOrderScope(scopeKey);
|
||||
if (!scope) continue;
|
||||
const existing = workspaceOrderByProject[scope.projectKey] ?? [];
|
||||
const existing = workspaceOrderByProject[scope.projectViewKey] ?? [];
|
||||
const merged = [...existing];
|
||||
const seen = new Set(merged);
|
||||
for (const key of order) {
|
||||
@@ -100,7 +100,7 @@ export function migrateSidebarOrderState(persistedState: unknown): {
|
||||
seen.add(workspaceKey);
|
||||
merged.push(workspaceKey);
|
||||
}
|
||||
workspaceOrderByProject[scope.projectKey] = merged;
|
||||
workspaceOrderByProject[scope.projectViewKey] = merged;
|
||||
}
|
||||
|
||||
return { projectOrder, workspaceOrderByProject };
|
||||
@@ -116,13 +116,13 @@ export const useSidebarOrderStore = create<SidebarOrderStoreState>()(
|
||||
const normalized = normalizeKeys(keys);
|
||||
set({ projectOrder: normalized });
|
||||
},
|
||||
getWorkspaceOrder: (projectKey) => {
|
||||
const scope = projectKey.trim();
|
||||
getWorkspaceOrder: (projectViewKey) => {
|
||||
const scope = projectViewKey.trim();
|
||||
if (!scope) return [];
|
||||
return get().workspaceOrderByProject[scope] ?? [];
|
||||
},
|
||||
setWorkspaceOrder: (projectKey, keys) => {
|
||||
const scope = projectKey.trim();
|
||||
setWorkspaceOrder: (projectViewKey, keys) => {
|
||||
const scope = projectViewKey.trim();
|
||||
if (!scope) return;
|
||||
const normalized = normalizeKeys(keys);
|
||||
set((state) => ({
|
||||
|
||||
Reference in New Issue
Block a user