mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(projects): preserve selected project identity
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useMemo } from "react";
|
||||
import SettingsScreen from "@/screens/settings-screen";
|
||||
import { normalizeProjectSettingsRouteKey } from "@/utils/host-routes";
|
||||
|
||||
export default function SettingsProjectDetailRoute() {
|
||||
const params = useLocalSearchParams<{ projectKey?: string | string[] }>();
|
||||
const rawProjectKey = Array.isArray(params.projectKey) ? params.projectKey[0] : params.projectKey;
|
||||
const projectKey = typeof rawProjectKey === "string" ? decodeURIComponent(rawProjectKey) : "";
|
||||
const projectKey = normalizeProjectSettingsRouteKey(params.projectKey);
|
||||
const view = useMemo(() => ({ kind: "project" as const, projectKey }), [projectKey]);
|
||||
|
||||
return <SettingsScreen view={view} />;
|
||||
|
||||
@@ -155,8 +155,20 @@ export function resolveInitialWorkspaceProject(input: {
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
const candidatePlacement = candidate.hosts.find(
|
||||
(host) => host.serverId === input.serverId && host.projectId,
|
||||
);
|
||||
const hydratedProject =
|
||||
input.projects.find((project) => project.projectKey === candidate.projectKey) ?? candidate;
|
||||
(candidatePlacement
|
||||
? input.projects.find((project) =>
|
||||
project.hosts.some(
|
||||
(host) =>
|
||||
host.serverId === input.serverId && host.projectId === candidatePlacement.projectId,
|
||||
),
|
||||
)
|
||||
: undefined) ??
|
||||
input.projects.find((project) => project.projectKey === candidate.projectKey) ??
|
||||
candidate;
|
||||
if (
|
||||
canCreateWorkspaceForHostProject({
|
||||
project: hydratedProject,
|
||||
|
||||
@@ -254,6 +254,38 @@ describe("host project list", () => {
|
||||
).toEqual(selectedHostProject);
|
||||
});
|
||||
|
||||
it("hydrates a route project by its host-local project id", () => {
|
||||
const hydratedDirectory = hostProject({
|
||||
projectKey: "host:host-a:project:project-a",
|
||||
projectName: "Notes",
|
||||
projectKind: "directory",
|
||||
hosts: [
|
||||
{
|
||||
serverId: "host-a",
|
||||
projectId: "project-a",
|
||||
iconWorkingDir: "/notes",
|
||||
canCreateWorktree: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
const routeDirectory = hostProjectFromRoute({
|
||||
serverId: "host-a",
|
||||
projectId: "project-a",
|
||||
displayName: "Notes",
|
||||
sourceDirectory: "/notes",
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveInitialWorkspaceProject({
|
||||
routeProject: routeDirectory,
|
||||
lastActiveProject: null,
|
||||
projects: [hydratedDirectory],
|
||||
serverId: "host-a",
|
||||
allowAllProjects: true,
|
||||
}),
|
||||
).toEqual(hydratedDirectory);
|
||||
});
|
||||
|
||||
it("resolves the selected host project source directory", () => {
|
||||
const project = hostProject({
|
||||
hosts: [
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
encodeWorkspaceIdForPathSegment,
|
||||
isSettingsSectionSlug,
|
||||
normalizeHostSectionSlug,
|
||||
normalizeProjectSettingsRouteKey,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseHostWorkspaceOpenIntentFromPathname,
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
@@ -214,6 +215,11 @@ describe("projects settings routes", () => {
|
||||
const segment = route.slice("/settings/projects/".length);
|
||||
expect(decodeURIComponent(segment)).toBe(projectKey);
|
||||
});
|
||||
|
||||
it("keeps an already-decoded opaque project key unchanged", () => {
|
||||
const projectKey = "remote:github.com/acme/app#subdir:packages/my%20app";
|
||||
expect(normalizeProjectSettingsRouteKey(projectKey)).toBe(projectKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("global routes", () => {
|
||||
|
||||
@@ -573,3 +573,8 @@ export function buildProjectSettingsRoute(projectKey: string) {
|
||||
}
|
||||
return `/settings/projects/${encodeSegment(normalized)}` as const;
|
||||
}
|
||||
|
||||
export function normalizeProjectSettingsRouteKey(value: string | string[] | undefined): string {
|
||||
const projectKey = Array.isArray(value) ? value[0] : value;
|
||||
return typeof projectKey === "string" ? projectKey : "";
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ describe("buildProjects", () => {
|
||||
expect(result.projects[0]?.hostCount).toBe(1);
|
||||
});
|
||||
|
||||
it("prefers placement mainRepoRoot for the host repoRoot and falls back to projectRootPath when placement is absent", () => {
|
||||
it("uses the persisted selected project root instead of the checkout root", () => {
|
||||
const result = buildProjects({
|
||||
hosts: [
|
||||
{
|
||||
@@ -425,11 +425,11 @@ describe("buildProjects", () => {
|
||||
workspaces: [
|
||||
workspace({
|
||||
id: "main",
|
||||
repoRoot: "/worktrees/app/main",
|
||||
repoRoot: "/worktrees/app/main/packages/client",
|
||||
project: placement({
|
||||
projectKey: "remote:github.com/acme/app",
|
||||
projectName: "acme/app",
|
||||
cwd: "/worktrees/app/main",
|
||||
projectKey: "remote:github.com/acme/app#subdir:packages/client",
|
||||
projectName: "client",
|
||||
cwd: "/worktrees/app/main/packages/client",
|
||||
remoteUrl: "https://github.com/acme/app.git",
|
||||
mainRepoRoot: "/repo/app",
|
||||
}),
|
||||
@@ -453,11 +453,11 @@ describe("buildProjects", () => {
|
||||
});
|
||||
|
||||
const acme = result.projects.find(
|
||||
(project) => project.projectKey === "remote:github.com/acme/app",
|
||||
(project) => project.projectKey === "remote:github.com/acme/app#subdir:packages/client",
|
||||
);
|
||||
const legacy = result.projects.find((project) => project.projectKey === "legacy-project");
|
||||
|
||||
expect(acme?.hosts[0]?.repoRoot).toBe("/repo/app");
|
||||
expect(acme?.hosts[0]?.repoRoot).toBe("/worktrees/app/main/packages/client");
|
||||
expect(legacy?.hosts[0]?.repoRoot).toBe("/repo/legacy");
|
||||
});
|
||||
|
||||
|
||||
@@ -119,12 +119,6 @@ function deriveGithubUrl(projectKey: string): string | undefined {
|
||||
}
|
||||
|
||||
function resolveHostRepoRoot(group: HostGroup): string {
|
||||
for (const workspace of group.workspaces) {
|
||||
const mainRepoRoot = workspace.project?.checkout.mainRepoRoot;
|
||||
if (mainRepoRoot) {
|
||||
return mainRepoRoot;
|
||||
}
|
||||
}
|
||||
return group.workspaces[0]?.projectRootPath ?? group.fallbackRepoRoot;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ test("creates a worktree and registers it in the source workspace project withou
|
||||
expect(deps.workspaceGitService.getSnapshot).not.toHaveBeenCalled();
|
||||
expect(deps.projects.get(sourceProject.projectId)).toEqual({
|
||||
...sourceProject,
|
||||
projectGroupKey: path.resolve(sourceProject.rootPath),
|
||||
projectGroupKey: result.repoRoot,
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
expect(events).toEqual([`workspace:${result.workspace.workspaceId}`]);
|
||||
|
||||
@@ -68,4 +68,18 @@ describe("deriveProjectGroupKey", () => {
|
||||
}),
|
||||
).toBe("remote:github.com/getpaseo/paseo");
|
||||
});
|
||||
|
||||
test("preserves the selected path for a checkout without a remote", () => {
|
||||
const worktreeRoot = path.resolve("worktree");
|
||||
const mainRepoRoot = path.resolve("main");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
rootPath: path.join(worktreeRoot, "packages", "app"),
|
||||
remoteUrl: null,
|
||||
worktreeRoot,
|
||||
mainRepoRoot,
|
||||
}),
|
||||
).toBe(path.join(mainRepoRoot, "packages", "app"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,10 +11,14 @@ export function deriveProjectGroupKey(input: {
|
||||
mainRepoRoot: string | null;
|
||||
}): string {
|
||||
const remoteKey = deriveRemoteProjectGroupKey(input.remoteUrl);
|
||||
if (!remoteKey) return resolve(input.mainRepoRoot ?? input.rootPath);
|
||||
|
||||
const selectedPath = deriveSelectedPath(input.rootPath, input.worktreeRoot);
|
||||
return selectedPath ? `${remoteKey}#subdir:${selectedPath}` : remoteKey;
|
||||
if (!remoteKey) {
|
||||
return selectedPath && input.mainRepoRoot
|
||||
? resolve(input.mainRepoRoot, selectedPath)
|
||||
: resolve(input.mainRepoRoot ?? input.rootPath);
|
||||
}
|
||||
|
||||
return selectedPath ? `${remoteKey}#subdir:${encodeSelectedPath(selectedPath)}` : remoteKey;
|
||||
}
|
||||
|
||||
function deriveSelectedPath(rootPath: string, worktreeRoot: string | null): string | null {
|
||||
@@ -29,6 +33,10 @@ function deriveSelectedPath(rootPath: string, worktreeRoot: string | null): stri
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return selectedPath;
|
||||
}
|
||||
|
||||
function encodeSelectedPath(selectedPath: string): string {
|
||||
return selectedPath.split(sep).map(encodeURIComponent).join("/");
|
||||
}
|
||||
|
||||
|
||||
@@ -1409,7 +1409,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
kind: "project_updated",
|
||||
projectId: "p1",
|
||||
directory: rootPath,
|
||||
fields: { projectGroupKey: "/tmp/main-repo" },
|
||||
fields: { projectGroupKey: path.resolve("/tmp/main-repo") },
|
||||
},
|
||||
{
|
||||
kind: "workspace_updated",
|
||||
|
||||
Reference in New Issue
Block a user