Add a global New workspace picker (#1324)
* Add global New workspace project picker * Clean up New workspace picker state
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildHostAgentDetailRoute,
|
||||
buildHostNewWorkspaceRoute,
|
||||
buildHostRootRoute,
|
||||
buildHostWorkspaceOpenRoute,
|
||||
buildHostWorkspaceRoute,
|
||||
@@ -120,6 +121,19 @@ describe("workspace route parsing", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("builds a global new workspace route without a source directory", () => {
|
||||
expect(buildHostNewWorkspaceRoute("local")).toBe("/h/local/new");
|
||||
});
|
||||
|
||||
it("builds a project shortcut new workspace route with initial project context", () => {
|
||||
expect(
|
||||
buildHostNewWorkspaceRoute("local", "/repo/project", {
|
||||
displayName: "Project",
|
||||
projectId: "project-1",
|
||||
}),
|
||||
).toBe("/h/local/new?dir=%2Frepo%2Fproject&name=Project&projectId=project-1");
|
||||
});
|
||||
|
||||
it("round-trips URL-safe IDs through encode/decode", () => {
|
||||
const ids = ["1", "40", "164", "9999", "workspace-1", "opaque_id.v2~test"];
|
||||
for (const id of ids) {
|
||||
|
||||
@@ -357,7 +357,7 @@ export function buildHostOpenProjectRoute(serverId: string) {
|
||||
|
||||
export function buildHostNewWorkspaceRoute(
|
||||
serverId: string,
|
||||
sourceDirectory: string,
|
||||
sourceDirectory?: string,
|
||||
options?: { displayName?: string; projectId?: string },
|
||||
) {
|
||||
const base = buildHostRootRoute(serverId);
|
||||
@@ -365,14 +365,20 @@ export function buildHostNewWorkspaceRoute(
|
||||
return "/" as const;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set("dir", sourceDirectory);
|
||||
if (sourceDirectory) {
|
||||
params.set("dir", sourceDirectory);
|
||||
}
|
||||
if (options?.displayName) {
|
||||
params.set("name", options.displayName);
|
||||
}
|
||||
if (options?.projectId) {
|
||||
params.set("projectId", options.projectId);
|
||||
}
|
||||
return `${base}/new?${params.toString()}` as const;
|
||||
const query = params.toString();
|
||||
if (!query) {
|
||||
return `${base}/new` as const;
|
||||
}
|
||||
return `${base}/new?${query}` as const;
|
||||
}
|
||||
|
||||
export const SETTINGS_SECTION_SLUGS = [
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { buildHostProjectList, type HostProjectListItem } from "@/projects/host-project-model";
|
||||
import { buildWorkspaceStructureProjects } from "@/projects/workspace-structure";
|
||||
|
||||
export interface WorkspaceSummary {
|
||||
id: string;
|
||||
@@ -61,6 +63,31 @@ interface ProjectGroup {
|
||||
hostsByServerId: Map<string, HostGroup>;
|
||||
}
|
||||
|
||||
function findProjectCustomName(
|
||||
workspaces: WorkspaceDescriptor[],
|
||||
projectKey: string,
|
||||
): { customName: string; displayName: string } | null {
|
||||
for (const workspace of workspaces) {
|
||||
if (workspace.projectId === projectKey && workspace.projectCustomName) {
|
||||
return {
|
||||
customName: workspace.projectCustomName,
|
||||
displayName: workspace.projectDisplayName,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildHostProjectEntries(host: ProjectHost): HostProjectListItem[] {
|
||||
return buildHostProjectList({
|
||||
serverId: host.serverId,
|
||||
projects: buildWorkspaceStructureProjects({
|
||||
serverId: host.serverId,
|
||||
workspaces: host.workspaces,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function deriveGithubUrl(projectKey: string): string | undefined {
|
||||
const match = projectKey.match(GITHUB_PROJECT_KEY_PATTERN);
|
||||
if (!match) {
|
||||
@@ -134,33 +161,37 @@ export function buildProjects(input: BuildProjectsInput): BuildProjectsResult {
|
||||
const groups = new Map<string, ProjectGroup>();
|
||||
|
||||
for (const host of input.hosts) {
|
||||
for (const workspace of host.workspaces) {
|
||||
const projectKey = workspace.projectId;
|
||||
|
||||
let group = groups.get(projectKey);
|
||||
const hostProjects = buildHostProjectEntries(host);
|
||||
for (const hostProject of hostProjects) {
|
||||
const customName = findProjectCustomName(host.workspaces, hostProject.projectKey);
|
||||
let group = groups.get(hostProject.projectKey);
|
||||
if (!group) {
|
||||
group = {
|
||||
projectKey,
|
||||
projectName: workspace.projectDisplayName,
|
||||
projectCustomName: workspace.projectCustomName ?? null,
|
||||
projectKey: hostProject.projectKey,
|
||||
projectName: customName?.displayName ?? hostProject.projectName,
|
||||
projectCustomName: customName?.customName ?? null,
|
||||
hostsByServerId: new Map(),
|
||||
};
|
||||
groups.set(projectKey, group);
|
||||
} else if (workspace.projectCustomName && !group.projectCustomName) {
|
||||
group.projectCustomName = workspace.projectCustomName;
|
||||
group.projectName = workspace.projectDisplayName;
|
||||
groups.set(hostProject.projectKey, group);
|
||||
} else if (customName && !group.projectCustomName) {
|
||||
group.projectCustomName = customName.customName;
|
||||
group.projectName = customName.displayName;
|
||||
}
|
||||
|
||||
let hostGroup = group.hostsByServerId.get(host.serverId);
|
||||
if (!hostGroup) {
|
||||
hostGroup = {
|
||||
if (!group.hostsByServerId.has(host.serverId)) {
|
||||
group.hostsByServerId.set(host.serverId, {
|
||||
serverId: host.serverId,
|
||||
serverName: host.serverName,
|
||||
isOnline: host.isOnline,
|
||||
workspaces: [],
|
||||
};
|
||||
group.hostsByServerId.set(host.serverId, hostGroup);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const workspace of host.workspaces) {
|
||||
const group = groups.get(workspace.projectId);
|
||||
const hostGroup = group?.hostsByServerId.get(host.serverId);
|
||||
if (!hostGroup) continue;
|
||||
hostGroup.workspaces.push(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,13 @@ function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorks
|
||||
}
|
||||
|
||||
function project(overrides: Partial<SidebarProjectEntry> = {}): SidebarProjectEntry {
|
||||
const projectKind = overrides.projectKind ?? "git";
|
||||
return {
|
||||
projectKey: "project-1",
|
||||
projectName: "paseo",
|
||||
projectKind: "git",
|
||||
projectKind,
|
||||
iconWorkingDir: "/repo",
|
||||
canCreateWorktree: overrides.canCreateWorktree ?? projectKind === "git",
|
||||
workspaces: [workspace()],
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export function buildSidebarProjectRowModel(input: {
|
||||
kind: "workspace_link",
|
||||
workspace: flattenedWorkspace,
|
||||
chevron: null,
|
||||
trailingAction: input.project.projectKind === "git" ? "new_worktree" : "none",
|
||||
trailingAction: input.project.canCreateWorktree ? "new_worktree" : "none",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,6 +51,6 @@ export function buildSidebarProjectRowModel(input: {
|
||||
return {
|
||||
kind: "project_section",
|
||||
chevron,
|
||||
trailingAction: input.project.projectKind === "git" ? "new_worktree" : "none",
|
||||
trailingAction: input.project.canCreateWorktree ? "new_worktree" : "none",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb
|
||||
projectName: projectKey,
|
||||
projectKind: "git",
|
||||
iconWorkingDir: workspaces[0]?.workspaceDirectory ?? "",
|
||||
canCreateWorktree: true,
|
||||
workspaces,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user