fix(projects): close grouped routing gaps

This commit is contained in:
Mohamed Boudra
2026-07-28 22:30:45 +00:00
parent d3d59749b0
commit 07bb8eeaa6
7 changed files with 76 additions and 11 deletions

View File

@@ -22,6 +22,7 @@ describe("selectActiveGitWorkspaceProject", () => {
it("selects the active git workspace project from checkout metadata", () => {
expect(selectActiveGitWorkspaceProject("server-1", gitWorkspace())).toEqual({
serverId: "server-1",
projectId: "project-1",
projectKey: "project-1",
repoRoot: "/repo/main-project-1",
});
@@ -35,11 +36,27 @@ describe("selectActiveGitWorkspaceProject", () => {
),
).toEqual({
serverId: "server-1",
projectId: "project-1",
projectKey: "project-1",
repoRoot: "/repo/project-1",
});
});
it("uses the persisted project group key for the settings route", () => {
expect(
selectActiveGitWorkspaceProject(
"server-1",
gitWorkspace({
projectId: "prj_local",
projectGroupKey: "remote:github.com/acme/project",
}),
),
).toMatchObject({
projectId: "prj_local",
projectKey: "remote:github.com/acme/project",
});
});
it("ignores non-git workspaces and blank project coordinates", () => {
expect(
selectActiveGitWorkspaceProject("server-1", gitWorkspace({ projectKind: "local" })),
@@ -85,6 +102,7 @@ describe("buildWorktreeSetupCalloutPolicy", () => {
expect(
buildWorktreeSetupCalloutPolicy({
serverId: "server-1",
projectId: "project-1",
projectKey: "project-1",
repoRoot: "/repo/project-1",
}),

View File

@@ -1,9 +1,11 @@
import type { PaseoConfigRaw } from "@getpaseo/protocol/messages";
import { i18n } from "@/i18n/i18next";
import { resolveProjectGroupKey } from "@/projects/project-group-key";
import { buildProjectSettingsRoute } from "@/utils/host-routes";
export interface WorktreeSetupWorkspaceInput {
projectId: string;
projectGroupKey?: string | null;
projectKind: string;
projectRootPath: string;
project?: {
@@ -15,6 +17,7 @@ export interface WorktreeSetupWorkspaceInput {
export interface ActiveGitWorkspaceProject {
serverId: string;
projectId: string;
projectKey: string;
repoRoot: string;
}
@@ -43,13 +46,18 @@ export function selectActiveGitWorkspaceProject(
return null;
}
const projectKey = workspace.projectId.trim();
const projectId = workspace.projectId.trim();
const projectKey = resolveProjectGroupKey({
serverId,
projectId,
projectGroupKey: workspace.projectGroupKey,
});
const repoRoot = (workspace.project?.checkout?.mainRepoRoot ?? workspace.projectRootPath).trim();
if (!projectKey || !repoRoot) {
if (!projectId || !repoRoot) {
return null;
}
return { serverId, projectKey, repoRoot };
return { serverId, projectId, projectKey, repoRoot };
}
export function shouldShowWorktreeSetupCallout(readResult: ReadProjectConfigResult | undefined) {

View File

@@ -3,6 +3,7 @@ import { useRouter } from "expo-router";
import { useEffect, useMemo } from "react";
import { useSidebarCallouts } from "@/contexts/sidebar-callout-context";
import { useStableEvent } from "@/hooks/use-stable-event";
import { useProjects } from "@/hooks/use-projects";
import { useHostRuntimeClient } from "@/runtime/host-runtime";
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
import { useWorkspaceFields } from "@/stores/session-store-hooks";
@@ -14,11 +15,25 @@ import {
export function WorktreeSetupCalloutSource() {
const selection = useActiveWorkspaceSelection();
const activeProject = useWorkspaceFields(
const selectedWorkspaceProject = useWorkspaceFields(
selection?.serverId ?? null,
selection?.workspaceId ?? null,
(workspace) => selectActiveGitWorkspaceProject(selection?.serverId ?? "", workspace),
);
const { projects } = useProjects();
const activeProject = useMemo(() => {
if (!selectedWorkspaceProject) return null;
const structuralProject = projects.find((project) =>
project.hosts.some(
(host) =>
host.serverId === selectedWorkspaceProject.serverId &&
host.projectId === selectedWorkspaceProject.projectId,
),
);
return structuralProject
? { ...selectedWorkspaceProject, projectKey: structuralProject.projectKey }
: selectedWorkspaceProject;
}, [projects, selectedWorkspaceProject]);
const client = useHostRuntimeClient(activeProject?.serverId ?? "");
const callouts = useSidebarCallouts();
const router = useRouter();

View File

@@ -16,6 +16,19 @@ describe("deriveProjectGroupKey", () => {
).toBe("remote:git.example.com:8443/acme/app");
});
test("accepts a root-level remote repository path", () => {
const rootPath = path.resolve("repo");
expect(
deriveProjectGroupKey({
rootPath,
remoteUrl: "https://git.example.com/repo.git",
worktreeRoot: rootPath,
mainRepoRoot: null,
}),
).toBe("remote:git.example.com/repo");
});
test("includes the selected path within a repository", () => {
const worktreeRoot = path.resolve("repo");
const rootPath = path.join(worktreeRoot, "packages", "app");

View File

@@ -55,6 +55,6 @@ function deriveRemoteProjectGroupKey(remoteUrl: string | null): string | null {
if (!host || !remotePath) return null;
let cleanedPath = remotePath.trim().replace(/^\/+/, "").replace(/\/+$/, "");
if (cleanedPath.endsWith(".git")) cleanedPath = cleanedPath.slice(0, -4);
if (!cleanedPath.includes("/")) return null;
if (!cleanedPath) return null;
return `remote:${host.toLowerCase()}/${cleanedPath}`;
}

View File

@@ -1,4 +1,4 @@
import { resolve } from "node:path";
import { basename, resolve } from "node:path";
import type { ProjectCheckoutLitePayload } from "@getpaseo/protocol/messages";
@@ -46,7 +46,7 @@ export function classifyDirectoryForProjectMembership(input: {
workspaceKind: deriveWorkspaceKind(checkout),
workspaceDisplayName: deriveWorkspaceDisplayName({ cwd, checkout }),
projectKey,
projectName: deriveProjectGroupingName(projectKey),
projectName: deriveProjectGroupingName(projectKey, cwd),
projectRootPath: deriveProjectRootPath({ cwd, checkout }),
projectKind: deriveProjectKind(checkout),
};
@@ -58,7 +58,8 @@ function deriveWorkspaceDirectoryKey(cwd: string, checkout: ProjectCheckoutLiteP
return worktreeRoot && resolve(worktreeRoot) === selectedRoot ? worktreeRoot : selectedRoot;
}
function deriveProjectGroupingName(projectKey: string): string {
function deriveProjectGroupingName(projectKey: string, selectedRoot: string): string {
if (projectKey.includes("#subdir:")) return basename(selectedRoot);
if (projectKey.startsWith("remote:")) {
const pathSegments = projectKey.slice("remote:".length).split("/").filter(Boolean).slice(1);
if (pathSegments.length >= 2) return pathSegments.slice(-2).join("/");

View File

@@ -426,9 +426,19 @@ describe("bootstrapWorkspaceRegistries", () => {
logger,
});
expect((await projectRegistry.list()).map((project) => project.projectId).sort()).toEqual([
"remote:github.com/acme/legacy-project#subdir:packages/app",
"remote:github.com/acme/legacy-project#subdir:packages/server",
expect(
(await projectRegistry.list())
.map((project) => ({ projectId: project.projectId, displayName: project.displayName }))
.sort((left, right) => left.projectId.localeCompare(right.projectId)),
).toEqual([
{
projectId: "remote:github.com/acme/legacy-project#subdir:packages/app",
displayName: "app",
},
{
projectId: "remote:github.com/acme/legacy-project#subdir:packages/server",
displayName: "server",
},
]);
expect(
new Set((await workspaceRegistry.list()).map((workspace) => workspace.projectId)),