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 { describe, expect, test } from "vitest";
|
||||
import { createProjectViewKey } from "@/projects/workspace-structure";
|
||||
import type { ProjectDescriptor, WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { buildProjects, getProjectSummaryForHostProject } from "./projects";
|
||||
|
||||
@@ -56,7 +57,7 @@ describe("buildProjects", () => {
|
||||
|
||||
expect(result.projects).toEqual([
|
||||
expect.objectContaining({
|
||||
projectKey: key,
|
||||
viewKey: createProjectViewKey({ kind: "equivalence", projectKey: key }),
|
||||
hostCount: 2,
|
||||
onlineHostCount: 1,
|
||||
totalWorkspaceCount: 2,
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface ProjectHostEntry {
|
||||
}
|
||||
|
||||
export interface ProjectSummary {
|
||||
projectKey: string;
|
||||
viewKey: string;
|
||||
projectName: string;
|
||||
projectCustomName?: string | null;
|
||||
hosts: ProjectHostEntry[];
|
||||
@@ -91,7 +91,7 @@ interface HostGroup {
|
||||
}
|
||||
|
||||
interface ProjectGroup {
|
||||
projectKey: string;
|
||||
viewKey: string;
|
||||
projectName: string;
|
||||
projectCustomName: string | null;
|
||||
hostsByServerId: Map<string, HostGroup>;
|
||||
@@ -99,10 +99,10 @@ interface ProjectGroup {
|
||||
|
||||
function findProjectMetadata(
|
||||
host: ProjectHost,
|
||||
projectKey: string,
|
||||
projectId: string,
|
||||
): { customName: string | null; displayName: string } | null {
|
||||
for (const project of host.projects) {
|
||||
if (project.projectId === projectKey) {
|
||||
if (project.projectId === projectId) {
|
||||
return {
|
||||
customName: project.projectCustomName ?? null,
|
||||
displayName: project.projectDisplayName,
|
||||
@@ -172,7 +172,7 @@ function toProjectSummary(draft: ProjectGroup): ProjectSummary {
|
||||
const totalWorkspaceCount = hosts.reduce((sum, host) => sum + host.workspaceCount, 0);
|
||||
const onlineHostCount = hosts.filter((host) => host.isOnline).length;
|
||||
return {
|
||||
projectKey: draft.projectKey,
|
||||
viewKey: draft.viewKey,
|
||||
projectName: draft.projectName,
|
||||
projectCustomName: draft.projectCustomName,
|
||||
hosts,
|
||||
@@ -196,15 +196,15 @@ function addHostProjects(
|
||||
if (!placement) continue;
|
||||
const projectId = placement.projectId;
|
||||
const customName = findProjectMetadata(host, projectId);
|
||||
let group = groups.get(hostProject.projectKey);
|
||||
let group = groups.get(hostProject.viewKey);
|
||||
if (!group) {
|
||||
group = {
|
||||
projectKey: hostProject.projectKey,
|
||||
viewKey: hostProject.viewKey,
|
||||
projectName: customName?.displayName ?? hostProject.projectName,
|
||||
projectCustomName: customName?.customName ?? null,
|
||||
hostsByServerId: new Map(),
|
||||
};
|
||||
groups.set(hostProject.projectKey, group);
|
||||
groups.set(hostProject.viewKey, group);
|
||||
} else if (customName?.customName && !group.projectCustomName) {
|
||||
group.projectCustomName = customName.customName;
|
||||
group.projectName = customName.displayName;
|
||||
@@ -230,16 +230,16 @@ function attachHostWorkspaces(
|
||||
host: ProjectHost,
|
||||
hostProjects: HostProjectListItem[],
|
||||
): void {
|
||||
const projectKeyByProjectId = new Map(
|
||||
const projectViewKeyByProjectId = new Map(
|
||||
hostProjects.flatMap((project) =>
|
||||
project.hosts
|
||||
.filter((placement) => placement.serverId === host.serverId && placement.projectId)
|
||||
.map((placement) => [placement.projectId!, project.projectKey] as const),
|
||||
.map((placement) => [placement.projectId!, project.viewKey] as const),
|
||||
),
|
||||
);
|
||||
|
||||
for (const workspace of host.workspaces) {
|
||||
const key = projectKeyByProjectId.get(workspace.projectId);
|
||||
const key = projectViewKeyByProjectId.get(workspace.projectId);
|
||||
if (key) groups.get(key)?.hostsByServerId.get(host.serverId)?.workspaces.push(workspace);
|
||||
}
|
||||
}
|
||||
@@ -262,7 +262,7 @@ export function buildProjects(input: BuildProjectsInput): BuildProjectsResult {
|
||||
if (name !== 0) {
|
||||
return name;
|
||||
}
|
||||
return left.projectKey.localeCompare(right.projectKey);
|
||||
return left.viewKey.localeCompare(right.viewKey);
|
||||
});
|
||||
|
||||
return { projects };
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
import {
|
||||
buildSidebarProjectRowModel,
|
||||
resolveSidebarProjectIconTarget,
|
||||
resolveSidebarProjectIconTargets,
|
||||
resolveSidebarProjectLocalPath,
|
||||
} from "./sidebar-project-row-model";
|
||||
|
||||
@@ -14,7 +15,7 @@ function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorks
|
||||
workspaceKey: "srv:ws-root",
|
||||
serverId: "srv",
|
||||
workspaceId: "ws-root",
|
||||
projectKey: "project-1",
|
||||
projectViewKey: "project-1",
|
||||
projectName: "paseo",
|
||||
workspaceDirectory: "/repo",
|
||||
projectKind: "git",
|
||||
@@ -48,7 +49,7 @@ function project(overrides: ProjectOverrides = {}): SidebarProjectEntry {
|
||||
(host) => Object.assign({}, host, { projectId: host.projectId ?? `project-${host.serverId}` }),
|
||||
);
|
||||
return {
|
||||
projectKey: "project-1",
|
||||
viewKey: "project-1",
|
||||
projectName: "paseo",
|
||||
projectKind,
|
||||
iconWorkingDir: "/repo",
|
||||
@@ -209,6 +210,22 @@ describe("buildSidebarProjectRowModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keys project icon results by the rendered project view", () => {
|
||||
const [iconTarget] = resolveSidebarProjectIconTargets([
|
||||
project({
|
||||
viewKey: '["placement","host-b","project-b"]',
|
||||
hosts: [{ serverId: "host-b", iconWorkingDir: "/repo/b", canCreateWorktree: true }],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(iconTarget).toEqual({
|
||||
projectViewKey: '["placement","host-b","project-b"]',
|
||||
serverId: "host-b",
|
||||
projectId: "project-host-b",
|
||||
iconWorkingDir: "/repo/b",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves desktop file actions from the local project placement", () => {
|
||||
const groupedProject = project({
|
||||
iconWorkingDir: "/remote/repo",
|
||||
|
||||
@@ -44,7 +44,7 @@ export function resolveSidebarProjectIconTarget(
|
||||
}
|
||||
|
||||
export interface SidebarProjectIconTarget extends SidebarProjectHostTarget {
|
||||
projectKey: string;
|
||||
projectViewKey: string;
|
||||
}
|
||||
|
||||
export function resolveSidebarProjectIconTargets(
|
||||
@@ -52,7 +52,7 @@ export function resolveSidebarProjectIconTargets(
|
||||
): SidebarProjectIconTarget[] {
|
||||
return projects.flatMap((project) => {
|
||||
const target = resolveSidebarProjectIconTarget(project);
|
||||
return target ? [{ projectKey: project.projectKey, ...target }] : [];
|
||||
return target ? [{ projectViewKey: project.viewKey, ...target }] : [];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ function workspace(input: {
|
||||
workspaceKey: `${input.serverId}:${input.workspaceId}`,
|
||||
serverId: input.serverId,
|
||||
workspaceId: input.workspaceId,
|
||||
projectKey: input.projectKey ?? "project-default",
|
||||
projectViewKey: input.projectKey ?? "project-default",
|
||||
projectName: input.projectKey ?? "Project",
|
||||
workspaceDirectory: input.workspaceDirectory,
|
||||
projectKind: "git",
|
||||
@@ -46,7 +46,7 @@ function workspace(input: {
|
||||
|
||||
function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): SidebarProjectEntry {
|
||||
return {
|
||||
projectKey,
|
||||
viewKey: projectKey,
|
||||
projectName: projectKey,
|
||||
projectKind: "git",
|
||||
iconWorkingDir: workspaces[0]?.workspaceDirectory ?? "",
|
||||
|
||||
@@ -36,7 +36,7 @@ export function buildSidebarShortcutModel(input: {
|
||||
return buildSidebarShortcutSections({
|
||||
sections: input.projects.map((project) => ({
|
||||
workspaces: project.workspaces,
|
||||
collapsed: input.collapsedProjectKeys.has(project.projectKey),
|
||||
collapsed: input.collapsedProjectKeys.has(project.viewKey),
|
||||
})),
|
||||
shortcutLimit: input.shortcutLimit,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user