fix(projects): make freshly-added projects editable
Make workspace-less projects flow through aggregated project fetching and keep the settings screen editable immediately after adding one.
This commit is contained in:
Generated
+1353
-2
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,8 @@ import { loadDaemonClientConstructor } from "./helpers/daemon-client-loader";
|
|||||||
import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./helpers/node-ws-factory";
|
import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./helpers/node-ws-factory";
|
||||||
import { forkPaseoHomeMetadata, resolvePaseoHomePath } from "./helpers/paseo-home-fork";
|
import { forkPaseoHomeMetadata, resolvePaseoHomePath } from "./helpers/paseo-home-fork";
|
||||||
|
|
||||||
|
const wranglerCliPath = path.resolve(__dirname, "../node_modules/wrangler/bin/wrangler.js");
|
||||||
|
|
||||||
interface WaitForServerOptions {
|
interface WaitForServerOptions {
|
||||||
host?: string;
|
host?: string;
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
@@ -573,8 +575,18 @@ async function startRelay(excludedPorts: Set<number>): Promise<number> {
|
|||||||
const state: RelayStreamState = { failureLine: null, readyForSelectedPort: false };
|
const state: RelayStreamState = { failureLine: null, readyForSelectedPort: false };
|
||||||
|
|
||||||
relayProcess = spawn(
|
relayProcess = spawn(
|
||||||
"npx",
|
process.execPath,
|
||||||
["wrangler", "dev", "--local", "--ip", "127.0.0.1", "--port", String(relayPort)],
|
[
|
||||||
|
wranglerCliPath,
|
||||||
|
"dev",
|
||||||
|
"--local",
|
||||||
|
"--ip",
|
||||||
|
"127.0.0.1",
|
||||||
|
"--port",
|
||||||
|
String(relayPort),
|
||||||
|
"--live-reload=false",
|
||||||
|
"--show-interactive-dev-session=false",
|
||||||
|
],
|
||||||
{
|
{
|
||||||
cwd: relayDir,
|
cwd: relayDir,
|
||||||
env: { ...process.env },
|
env: { ...process.env },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { chmod, readFile } from "node:fs/promises";
|
import { chmod, readFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { expect, test as base } from "./fixtures";
|
import { expect, test as base, type Page } from "./fixtures";
|
||||||
import { seedWorkspace } from "./helpers/seed-client";
|
import { connectSeedClient, seedWorkspace } from "./helpers/seed-client";
|
||||||
import {
|
import {
|
||||||
blockPaseoConfigWrites,
|
blockPaseoConfigWrites,
|
||||||
bumpPaseoConfigOnDisk,
|
bumpPaseoConfigOnDisk,
|
||||||
@@ -30,6 +30,8 @@ import {
|
|||||||
restorePaseoConfig,
|
restorePaseoConfig,
|
||||||
unblockPaseoConfigWrites,
|
unblockPaseoConfigWrites,
|
||||||
} from "./helpers/project-settings";
|
} from "./helpers/project-settings";
|
||||||
|
import { gotoAppShell } from "./helpers/app";
|
||||||
|
import { createTempGitRepo } from "./helpers/workspace";
|
||||||
|
|
||||||
const updatedSetup = ["npm install", "npm run build"];
|
const updatedSetup = ["npm install", "npm run build"];
|
||||||
|
|
||||||
@@ -131,7 +133,64 @@ async function readProjectConfigFile(project: ProjectsSettingsProject): Promise<
|
|||||||
return readFile(path.join(project.path, "paseo.json"), "utf8");
|
return readFile(path.join(project.path, "paseo.json"), "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addProjectFromSidebar(page: Page, projectPath: string): Promise<string> {
|
||||||
|
await page.getByTestId("sidebar-add-project").click();
|
||||||
|
|
||||||
|
const input = page.getByPlaceholder("Type a directory path...");
|
||||||
|
await expect(input).toBeVisible({ timeout: 30_000 });
|
||||||
|
await input.fill(projectPath);
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
|
||||||
|
const projectRow = page
|
||||||
|
.locator('[data-testid^="sidebar-project-row-"]')
|
||||||
|
.filter({ hasText: path.basename(projectPath) })
|
||||||
|
.first();
|
||||||
|
await expect(projectRow).toBeVisible({ timeout: 30_000 });
|
||||||
|
|
||||||
|
const testId = await projectRow.getAttribute("data-testid");
|
||||||
|
expect(testId).not.toBeNull();
|
||||||
|
return testId!.replace("sidebar-project-row-", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openProjectSettingsFromSidebar(page: Page, projectId: string): Promise<void> {
|
||||||
|
const projectRow = page.getByTestId(`sidebar-project-row-${projectId}`);
|
||||||
|
await expect(projectRow).toBeVisible({ timeout: 30_000 });
|
||||||
|
await projectRow.hover();
|
||||||
|
|
||||||
|
const kebab = page.getByTestId(`sidebar-project-kebab-${projectId}`);
|
||||||
|
await expect(kebab).toBeVisible({ timeout: 10_000 });
|
||||||
|
await kebab.click();
|
||||||
|
|
||||||
|
const openSettingsItem = page.getByTestId(`sidebar-project-menu-open-settings-${projectId}`);
|
||||||
|
await expect(openSettingsItem).toBeVisible({ timeout: 10_000 });
|
||||||
|
await openSettingsItem.click();
|
||||||
|
}
|
||||||
|
|
||||||
test.describe("Projects settings", () => {
|
test.describe("Projects settings", () => {
|
||||||
|
test("freshly-added project with no workspace is editable from the sidebar without a reload", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const repo = await createTempGitRepo("projects-settings-empty-");
|
||||||
|
const client = await connectSeedClient();
|
||||||
|
let projectId: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gotoAppShell(page);
|
||||||
|
|
||||||
|
projectId = await addProjectFromSidebar(page, repo.path);
|
||||||
|
await openProjectSettingsFromSidebar(page, projectId);
|
||||||
|
|
||||||
|
await expectProjectSettingsFormVisible(page);
|
||||||
|
await expect(page.getByTestId("project-settings-back-button")).not.toBeVisible();
|
||||||
|
} finally {
|
||||||
|
if (projectId) {
|
||||||
|
await client.removeProject(projectId).catch(() => undefined);
|
||||||
|
}
|
||||||
|
await client.close().catch(() => undefined);
|
||||||
|
await repo.cleanup().catch(() => undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("user edits worktree setup from the projects page", async ({ page, editableProject }) => {
|
test("user edits worktree setup from the projects page", async ({ page, editableProject }) => {
|
||||||
await openProjects(page);
|
await openProjects(page);
|
||||||
await openProjectSettings(page, editableProject.name);
|
await openProjectSettings(page, editableProject.name);
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
"serve-sim": "^0.1.40",
|
"serve-sim": "^0.1.40",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "~5.9.2",
|
||||||
"vitest": "^4.1.6",
|
"vitest": "^4.1.6",
|
||||||
"wrangler": "^4.75.0",
|
"wrangler": "^4.105.0",
|
||||||
"ws": "^8.20.0"
|
"ws": "^8.20.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
|
import { projectsQueryKey } from "@/hooks/use-projects";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { openProjectDirectly, type OpenProjectResult } from "@/hooks/open-project";
|
import { openProjectDirectly, type OpenProjectResult } from "@/hooks/open-project";
|
||||||
|
|
||||||
@@ -9,6 +11,7 @@ export function useOpenProject(
|
|||||||
const normalizedServerId = serverId?.trim() ?? "";
|
const normalizedServerId = serverId?.trim() ?? "";
|
||||||
const client = useHostRuntimeClient(normalizedServerId);
|
const client = useHostRuntimeClient(normalizedServerId);
|
||||||
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
|
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const canAddProject = useSessionStore((state) =>
|
const canAddProject = useSessionStore((state) =>
|
||||||
normalizedServerId
|
normalizedServerId
|
||||||
? state.sessions[normalizedServerId]?.serverInfo?.features?.projectAdd === true
|
? state.sessions[normalizedServerId]?.serverInfo?.features?.projectAdd === true
|
||||||
@@ -19,7 +22,7 @@ export function useOpenProject(
|
|||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async (path: string) => {
|
async (path: string) => {
|
||||||
return openProjectDirectly({
|
const result = await openProjectDirectly({
|
||||||
serverId: normalizedServerId,
|
serverId: normalizedServerId,
|
||||||
projectPath: path,
|
projectPath: path,
|
||||||
isConnected,
|
isConnected,
|
||||||
@@ -28,6 +31,14 @@ export function useOpenProject(
|
|||||||
addEmptyProject,
|
addEmptyProject,
|
||||||
setHasHydratedWorkspaces,
|
setHasHydratedWorkspaces,
|
||||||
});
|
});
|
||||||
|
// The aggregated projects query derives the project list from a fetch
|
||||||
|
// that now includes empty projects; refetch so a freshly-added project
|
||||||
|
// (no workspace yet) is immediately editable instead of only after a
|
||||||
|
// restart.
|
||||||
|
if (result.ok) {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: projectsQueryKey });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
addEmptyProject,
|
addEmptyProject,
|
||||||
@@ -35,6 +46,7 @@ export function useOpenProject(
|
|||||||
client,
|
client,
|
||||||
isConnected,
|
isConnected,
|
||||||
normalizedServerId,
|
normalizedServerId,
|
||||||
|
queryClient,
|
||||||
setHasHydratedWorkspaces,
|
setHasHydratedWorkspaces,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function useSidebarWorkspacesList(options?: {
|
|||||||
void (async () => {
|
void (async () => {
|
||||||
const next = new Map<string, WorkspaceDescriptor>();
|
const next = new Map<string, WorkspaceDescriptor>();
|
||||||
try {
|
try {
|
||||||
const workspaces = await fetchAllWorkspaceDescriptors({
|
const { workspaces, emptyProjects } = await fetchAllWorkspaceDescriptors({
|
||||||
client,
|
client,
|
||||||
sort: [{ key: "activity_at", direction: "desc" }],
|
sort: [{ key: "activity_at", direction: "desc" }],
|
||||||
});
|
});
|
||||||
@@ -181,6 +181,9 @@ export function useSidebarWorkspacesList(options?: {
|
|||||||
}
|
}
|
||||||
const store = useSessionStore.getState();
|
const store = useSessionStore.getState();
|
||||||
store.setWorkspaces(serverId, next);
|
store.setWorkspaces(serverId, next);
|
||||||
|
// Keep parents with no workspaces yet, so a manual refresh doesn't drop
|
||||||
|
// a freshly-added project from the sidebar.
|
||||||
|
store.setEmptyProjects(serverId, emptyProjects);
|
||||||
store.setHasHydratedWorkspaces(serverId, true);
|
store.setHasHydratedWorkspaces(serverId, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[WorkspaceFetch][sidebar-refresh] failed", {
|
console.error("[WorkspaceFetch][sidebar-refresh] failed", {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
|
||||||
import { fetchAllWorkspaceDescriptors } from "@/projects/workspace-fetching";
|
import { fetchAllWorkspaceDescriptors } from "@/projects/workspace-fetching";
|
||||||
import { buildProjects, type ProjectHost, type ProjectSummary } from "@/utils/projects";
|
import { buildProjects, type ProjectHost, type ProjectSummary } from "@/utils/projects";
|
||||||
|
|
||||||
@@ -58,13 +57,14 @@ export async function fetchAggregatedProjects(
|
|||||||
serverName: host.serverName,
|
serverName: host.serverName,
|
||||||
isOnline,
|
isOnline,
|
||||||
workspaces: [],
|
workspaces: [],
|
||||||
|
emptyProjects: [],
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workspaces: WorkspaceDescriptor[] = await fetchAllWorkspaceDescriptors({
|
const { workspaces, emptyProjects } = await fetchAllWorkspaceDescriptors({
|
||||||
client,
|
client,
|
||||||
sort: [{ key: "name", direction: "asc" }],
|
sort: [{ key: "name", direction: "asc" }],
|
||||||
});
|
});
|
||||||
@@ -74,6 +74,7 @@ export async function fetchAggregatedProjects(
|
|||||||
serverName: host.serverName,
|
serverName: host.serverName,
|
||||||
isOnline,
|
isOnline,
|
||||||
workspaces,
|
workspaces,
|
||||||
|
emptyProjects,
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
@@ -84,6 +85,7 @@ export async function fetchAggregatedProjects(
|
|||||||
serverName: host.serverName,
|
serverName: host.serverName,
|
||||||
isOnline,
|
isOnline,
|
||||||
workspaces: [],
|
workspaces: [],
|
||||||
|
emptyProjects: [],
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
serverId: host.serverId,
|
serverId: host.serverId,
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||||
import { normalizeWorkspaceDescriptor, type WorkspaceDescriptor } from "@/stores/session-store";
|
import {
|
||||||
|
type EmptyProjectDescriptor,
|
||||||
|
normalizeEmptyProjectDescriptor,
|
||||||
|
normalizeWorkspaceDescriptor,
|
||||||
|
type WorkspaceDescriptor,
|
||||||
|
} from "@/stores/session-store";
|
||||||
|
|
||||||
export type FetchWorkspacesClient = Pick<DaemonClient, "fetchWorkspaces">;
|
export type FetchWorkspacesClient = Pick<DaemonClient, "fetchWorkspaces">;
|
||||||
export type FetchWorkspacesSort = NonNullable<
|
export type FetchWorkspacesSort = NonNullable<
|
||||||
Parameters<DaemonClient["fetchWorkspaces"]>[0]
|
Parameters<DaemonClient["fetchWorkspaces"]>[0]
|
||||||
>["sort"];
|
>["sort"];
|
||||||
|
|
||||||
|
export interface FetchWorkspaceDescriptorsResult {
|
||||||
|
workspaces: WorkspaceDescriptor[];
|
||||||
|
/**
|
||||||
|
* Project parents with no active workspaces. The daemon only rides these on
|
||||||
|
* the first page of `fetchWorkspaces`, so a freshly-added project that has no
|
||||||
|
* workspace yet shows up here and nowhere else — drop it and the project is
|
||||||
|
* invisible to anything that derives projects from workspaces alone.
|
||||||
|
*/
|
||||||
|
emptyProjects: EmptyProjectDescriptor[];
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchAllWorkspaceDescriptors(input: {
|
export async function fetchAllWorkspaceDescriptors(input: {
|
||||||
client: FetchWorkspacesClient;
|
client: FetchWorkspacesClient;
|
||||||
sort: FetchWorkspacesSort;
|
sort: FetchWorkspacesSort;
|
||||||
}): Promise<WorkspaceDescriptor[]> {
|
}): Promise<FetchWorkspaceDescriptorsResult> {
|
||||||
const entries: WorkspaceDescriptor[] = [];
|
const workspaces: WorkspaceDescriptor[] = [];
|
||||||
|
const emptyProjects = new Map<string, EmptyProjectDescriptor>();
|
||||||
let cursor: string | null = null;
|
let cursor: string | null = null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -18,12 +35,16 @@ export async function fetchAllWorkspaceDescriptors(input: {
|
|||||||
sort: input.sort,
|
sort: input.sort,
|
||||||
page: cursor ? { limit: 200, cursor } : { limit: 200 },
|
page: cursor ? { limit: 200, cursor } : { limit: 200 },
|
||||||
});
|
});
|
||||||
entries.push(...payload.entries.map((entry) => normalizeWorkspaceDescriptor(entry)));
|
workspaces.push(...payload.entries.map((entry) => normalizeWorkspaceDescriptor(entry)));
|
||||||
|
for (const project of payload.emptyProjects ?? []) {
|
||||||
|
const descriptor = normalizeEmptyProjectDescriptor(project);
|
||||||
|
emptyProjects.set(descriptor.projectId, descriptor);
|
||||||
|
}
|
||||||
if (!payload.pageInfo.hasMore || !payload.pageInfo.nextCursor) {
|
if (!payload.pageInfo.hasMore || !payload.pageInfo.nextCursor) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
cursor = payload.pageInfo.nextCursor;
|
cursor = payload.pageInfo.nextCursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries;
|
return { workspaces, emptyProjects: Array.from(emptyProjects.values()) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -514,4 +514,36 @@ describe("buildProjects", () => {
|
|||||||
expect(summary?.hosts[0]?.repoRoot).toBe("/repo/legacy");
|
expect(summary?.hosts[0]?.repoRoot).toBe("/repo/legacy");
|
||||||
expect(summary?.hosts[0]?.workspaceCount).toBe(1);
|
expect(summary?.hosts[0]?.workspaceCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("surfaces a project with no workspaces yet and gives its host an editable repoRoot", () => {
|
||||||
|
const result = buildProjects({
|
||||||
|
hosts: [
|
||||||
|
{
|
||||||
|
serverId: "local",
|
||||||
|
serverName: "Local",
|
||||||
|
isOnline: true,
|
||||||
|
workspaces: [],
|
||||||
|
emptyProjects: [
|
||||||
|
{
|
||||||
|
projectId: "/repo/fresh",
|
||||||
|
projectDisplayName: "fresh",
|
||||||
|
projectCustomName: null,
|
||||||
|
projectRootPath: "/repo/fresh",
|
||||||
|
projectKind: "git",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.projects).toHaveLength(1);
|
||||||
|
const summary = result.projects[0];
|
||||||
|
expect(summary?.projectKey).toBe("/repo/fresh");
|
||||||
|
expect(summary?.totalWorkspaceCount).toBe(0);
|
||||||
|
expect(summary?.hosts).toHaveLength(1);
|
||||||
|
// repoRoot must be non-empty or the project settings screen treats the host
|
||||||
|
// as non-editable and shows its empty state.
|
||||||
|
expect(summary?.hosts[0]?.repoRoot).toBe("/repo/fresh");
|
||||||
|
expect(summary?.hosts[0]?.workspaceCount).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
import type { EmptyProjectDescriptor, WorkspaceDescriptor } from "@/stores/session-store";
|
||||||
import { buildHostProjectList, type HostProjectListItem } from "@/projects/host-project-model";
|
import { buildHostProjectList, type HostProjectListItem } from "@/projects/host-project-model";
|
||||||
import { buildWorkspaceStructureProjects } from "@/projects/workspace-structure";
|
import { buildWorkspaceStructureProjects } from "@/projects/workspace-structure";
|
||||||
|
|
||||||
@@ -37,6 +37,8 @@ export interface ProjectHost {
|
|||||||
serverName: string;
|
serverName: string;
|
||||||
isOnline: boolean;
|
isOnline: boolean;
|
||||||
workspaces: WorkspaceDescriptor[];
|
workspaces: WorkspaceDescriptor[];
|
||||||
|
/** Project parents with no active workspaces, so they still surface as projects. */
|
||||||
|
emptyProjects?: EmptyProjectDescriptor[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BuildProjectsInput {
|
export interface BuildProjectsInput {
|
||||||
@@ -54,6 +56,9 @@ interface HostGroup {
|
|||||||
serverName: string;
|
serverName: string;
|
||||||
isOnline: boolean;
|
isOnline: boolean;
|
||||||
workspaces: WorkspaceDescriptor[];
|
workspaces: WorkspaceDescriptor[];
|
||||||
|
// Repo root for a project parent that has no workspaces yet. Without it the
|
||||||
|
// host's repoRoot resolves to "" and the project reads as non-editable.
|
||||||
|
fallbackRepoRoot: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectGroup {
|
interface ProjectGroup {
|
||||||
@@ -81,7 +86,13 @@ function findProjectCustomName(
|
|||||||
function buildHostProjectEntries(host: ProjectHost): HostProjectListItem[] {
|
function buildHostProjectEntries(host: ProjectHost): HostProjectListItem[] {
|
||||||
return buildHostProjectList({
|
return buildHostProjectList({
|
||||||
projects: buildWorkspaceStructureProjects({
|
projects: buildWorkspaceStructureProjects({
|
||||||
sessions: [{ serverId: host.serverId, workspaces: host.workspaces }],
|
sessions: [
|
||||||
|
{
|
||||||
|
serverId: host.serverId,
|
||||||
|
workspaces: host.workspaces,
|
||||||
|
emptyProjects: host.emptyProjects,
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -94,14 +105,14 @@ function deriveGithubUrl(projectKey: string): string | undefined {
|
|||||||
return `https://github.com/${match[1]}/${match[2]}`;
|
return `https://github.com/${match[1]}/${match[2]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveHostRepoRoot(workspaces: WorkspaceDescriptor[]): string {
|
function resolveHostRepoRoot(group: HostGroup): string {
|
||||||
for (const workspace of workspaces) {
|
for (const workspace of group.workspaces) {
|
||||||
const mainRepoRoot = workspace.project?.checkout.mainRepoRoot;
|
const mainRepoRoot = workspace.project?.checkout.mainRepoRoot;
|
||||||
if (mainRepoRoot) {
|
if (mainRepoRoot) {
|
||||||
return mainRepoRoot;
|
return mainRepoRoot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return workspaces[0]?.projectRootPath ?? "";
|
return group.workspaces[0]?.projectRootPath ?? group.fallbackRepoRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toWorkspaceSummary(workspace: WorkspaceDescriptor): WorkspaceSummary {
|
function toWorkspaceSummary(workspace: WorkspaceDescriptor): WorkspaceSummary {
|
||||||
@@ -115,7 +126,7 @@ function toWorkspaceSummary(workspace: WorkspaceDescriptor): WorkspaceSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toHostEntry(group: HostGroup): ProjectHostEntry {
|
function toHostEntry(group: HostGroup): ProjectHostEntry {
|
||||||
const repoRoot = resolveHostRepoRoot(group.workspaces);
|
const repoRoot = resolveHostRepoRoot(group);
|
||||||
const canonical =
|
const canonical =
|
||||||
group.workspaces.find((workspace) => workspace.projectRootPath === repoRoot) ??
|
group.workspaces.find((workspace) => workspace.projectRootPath === repoRoot) ??
|
||||||
group.workspaces[0];
|
group.workspaces[0];
|
||||||
@@ -159,6 +170,11 @@ export function buildProjects(input: BuildProjectsInput): BuildProjectsResult {
|
|||||||
const groups = new Map<string, ProjectGroup>();
|
const groups = new Map<string, ProjectGroup>();
|
||||||
|
|
||||||
for (const host of input.hosts) {
|
for (const host of input.hosts) {
|
||||||
|
const emptyRepoRootByProjectKey = new Map<string, string>();
|
||||||
|
for (const emptyProject of host.emptyProjects ?? []) {
|
||||||
|
emptyRepoRootByProjectKey.set(emptyProject.projectId, emptyProject.projectRootPath);
|
||||||
|
}
|
||||||
|
|
||||||
const hostProjects = buildHostProjectEntries(host);
|
const hostProjects = buildHostProjectEntries(host);
|
||||||
for (const hostProject of hostProjects) {
|
for (const hostProject of hostProjects) {
|
||||||
const customName = findProjectCustomName(host.workspaces, hostProject.projectKey);
|
const customName = findProjectCustomName(host.workspaces, hostProject.projectKey);
|
||||||
@@ -182,6 +198,7 @@ export function buildProjects(input: BuildProjectsInput): BuildProjectsResult {
|
|||||||
serverName: host.serverName,
|
serverName: host.serverName,
|
||||||
isOnline: host.isOnline,
|
isOnline: host.isOnline,
|
||||||
workspaces: [],
|
workspaces: [],
|
||||||
|
fallbackRepoRoot: emptyRepoRootByProjectKey.get(hostProject.projectKey) ?? "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user