mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(projects): stabilize settings and path identity
This commit is contained in:
@@ -58,6 +58,7 @@ import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sec
|
||||
import { useHostFeatureMap } from "@/runtime/host-features";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
||||
import { resolveProjectSettingsRouteKey } from "@/projects/project-settings-target";
|
||||
import {
|
||||
buildNewWorkspaceRoute,
|
||||
buildProjectSettingsRoute,
|
||||
@@ -523,6 +524,7 @@ function ProjectRowTrailingActions({
|
||||
>
|
||||
<ProjectKebabMenu
|
||||
projectKey={project.projectKey}
|
||||
projectSettingsKey={resolveProjectSettingsRouteKey(project)}
|
||||
projectPath={project.iconWorkingDir}
|
||||
onRemoveProject={onRemoveProject}
|
||||
removeProjectStatus={removeProjectStatus}
|
||||
@@ -550,11 +552,13 @@ function renderKebabTriggerIcon({ hovered }: { hovered?: boolean }) {
|
||||
|
||||
function ProjectKebabMenu({
|
||||
projectKey,
|
||||
projectSettingsKey,
|
||||
projectPath,
|
||||
onRemoveProject,
|
||||
removeProjectStatus,
|
||||
}: {
|
||||
projectKey: string;
|
||||
projectSettingsKey: string;
|
||||
projectPath: string;
|
||||
onRemoveProject: () => void;
|
||||
removeProjectStatus: "idle" | "pending" | "success";
|
||||
@@ -562,10 +566,10 @@ function ProjectKebabMenu({
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const handleOpenProjectSettings = useCallback(() => {
|
||||
if (projectKey.trim().length === 0) return;
|
||||
router.navigate(buildProjectSettingsRoute(projectKey));
|
||||
}, [projectKey]);
|
||||
const canOpenProjectSettings = projectKey.trim().length > 0;
|
||||
if (projectSettingsKey.trim().length === 0) return;
|
||||
router.navigate(buildProjectSettingsRoute(projectSettingsKey));
|
||||
}, [projectSettingsKey]);
|
||||
const canOpenProjectSettings = projectSettingsKey.trim().length > 0;
|
||||
// Desktop-only: open a second window that lands on this project via the same
|
||||
// open-project flow as a CLI launch. The project stays visible here too — no
|
||||
// ownership, no move.
|
||||
|
||||
@@ -118,4 +118,15 @@ describe("buildWorktreeSetupCalloutPolicy", () => {
|
||||
testID: "worktree-setup-callout-project-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the action route stable when the structural group key changes", () => {
|
||||
expect(
|
||||
buildWorktreeSetupCalloutPolicy({
|
||||
serverId: "server-1",
|
||||
projectId: "prj_local",
|
||||
projectKey: "remote:github.com/acme/project",
|
||||
repoRoot: "/repo/project",
|
||||
}).projectSettingsRoute,
|
||||
).toBe("/settings/projects/host%3Aserver-1%3Aproject%3Aprj_local");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,6 +68,10 @@ export function buildWorktreeSetupCalloutPolicy(
|
||||
project: ActiveGitWorkspaceProject,
|
||||
): WorktreeSetupCalloutPolicy {
|
||||
const calloutKey = `worktree-setup-missing:${project.projectKey}`;
|
||||
const projectSettingsKey = resolveProjectGroupKey({
|
||||
serverId: project.serverId,
|
||||
projectId: project.projectId,
|
||||
});
|
||||
|
||||
return {
|
||||
id: calloutKey,
|
||||
@@ -76,7 +80,7 @@ export function buildWorktreeSetupCalloutPolicy(
|
||||
title: i18n.t("sidebar.worktreeSetup.title"),
|
||||
description: i18n.t("sidebar.worktreeSetup.description"),
|
||||
actionLabel: i18n.t("sidebar.worktreeSetup.openProjectSettings"),
|
||||
projectSettingsRoute: buildProjectSettingsRoute(project.projectKey),
|
||||
projectSettingsRoute: buildProjectSettingsRoute(projectSettingsKey),
|
||||
testID: `worktree-setup-callout-${project.projectKey}`,
|
||||
};
|
||||
}
|
||||
|
||||
20
packages/app/src/projects/project-settings-target.test.ts
Normal file
20
packages/app/src/projects/project-settings-target.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
findProjectSettingsTarget,
|
||||
resolveProjectSettingsRouteKey,
|
||||
} from "./project-settings-target";
|
||||
|
||||
describe("project settings target", () => {
|
||||
const project = {
|
||||
projectKey: "remote:github.com/acme/app",
|
||||
hosts: [{ serverId: "host-a", projectId: "prj_1234" }],
|
||||
};
|
||||
|
||||
it("builds settings routes from stable host-local identity", () => {
|
||||
expect(resolveProjectSettingsRouteKey(project)).toBe("host:host-a:project:prj_1234");
|
||||
});
|
||||
|
||||
it("keeps a host-local route valid after the structural key changes", () => {
|
||||
expect(findProjectSettingsTarget([project], "host:host-a:project:prj_1234")).toBe(project);
|
||||
});
|
||||
});
|
||||
32
packages/app/src/projects/project-settings-target.ts
Normal file
32
packages/app/src/projects/project-settings-target.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { resolveProjectGroupKey } from "./project-group-key";
|
||||
|
||||
interface ProjectSettingsTarget {
|
||||
projectKey: string;
|
||||
hosts: ReadonlyArray<{ serverId: string; projectId?: string }>;
|
||||
}
|
||||
|
||||
function resolveHostLocalProjectKey(host: { serverId: string; projectId?: string }): string | null {
|
||||
const projectId = host.projectId?.trim();
|
||||
if (!projectId) return null;
|
||||
return resolveProjectGroupKey({ serverId: host.serverId, projectId });
|
||||
}
|
||||
|
||||
export function resolveProjectSettingsRouteKey(project: ProjectSettingsTarget): string {
|
||||
for (const host of project.hosts) {
|
||||
const hostLocalKey = resolveHostLocalProjectKey(host);
|
||||
if (hostLocalKey) return hostLocalKey;
|
||||
}
|
||||
return project.projectKey;
|
||||
}
|
||||
|
||||
export function findProjectSettingsTarget<T extends ProjectSettingsTarget>(
|
||||
projects: readonly T[],
|
||||
routeKey: string,
|
||||
): T | undefined {
|
||||
return (
|
||||
projects.find((project) => project.projectKey === routeKey) ??
|
||||
projects.find((project) =>
|
||||
project.hosts.some((host) => resolveHostLocalProjectKey(host) === routeKey),
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import { SettingsGroup } from "@/screens/settings/settings-group";
|
||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import { useProjects } from "@/hooks/use-projects";
|
||||
import { findProjectSettingsTarget } from "@/projects/project-settings-target";
|
||||
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
||||
import { useHostRuntimeClient, useHostRuntimeSnapshot } from "@/runtime/host-runtime";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
@@ -90,7 +91,7 @@ export interface ProjectSettingsScreenProps {
|
||||
export default function ProjectSettingsScreen({ projectKey }: ProjectSettingsScreenProps) {
|
||||
const { projects } = useProjects();
|
||||
const project = useMemo(
|
||||
() => projects.find((entry) => entry.projectKey === projectKey),
|
||||
() => findProjectSettingsTarget(projects, projectKey),
|
||||
[projects, projectKey],
|
||||
);
|
||||
const editableHosts = useMemo(() => filterEditableHosts(project), [project]);
|
||||
|
||||
@@ -8,6 +8,10 @@ import { ProjectIconView } from "@/components/project-icon-view";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { useProjects, type ProjectHostError } from "@/hooks/use-projects";
|
||||
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
||||
import {
|
||||
findProjectSettingsTarget,
|
||||
resolveProjectSettingsRouteKey,
|
||||
} from "@/projects/project-settings-target";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import { buildProjectSettingsRoute } from "@/utils/host-routes";
|
||||
import type { ProjectSummary } from "@/utils/projects";
|
||||
@@ -19,7 +23,10 @@ interface ProjectsScreenProps {
|
||||
export default function ProjectsScreen({ view }: ProjectsScreenProps) {
|
||||
const { t } = useTranslation();
|
||||
const { projects, hostErrors, isLoading } = useProjects();
|
||||
const selectedProjectKey = view.kind === "project" ? view.projectKey : null;
|
||||
const selectedProjectKey =
|
||||
view.kind === "project"
|
||||
? (findProjectSettingsTarget(projects, view.projectKey)?.projectKey ?? null)
|
||||
: null;
|
||||
const iconTargets = useMemo(
|
||||
() =>
|
||||
projects.flatMap((project) => {
|
||||
@@ -100,10 +107,11 @@ function ProjectRow({ project, isFirst, isSelected, iconDataUri }: ProjectRowPro
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useUnistyles();
|
||||
const { projectKey, projectName } = project;
|
||||
const settingsRouteKey = resolveProjectSettingsRouteKey(project);
|
||||
|
||||
const handleNavigate = useCallback(() => {
|
||||
router.navigate(buildProjectSettingsRoute(projectKey));
|
||||
}, [projectKey]);
|
||||
router.navigate(buildProjectSettingsRoute(settingsRouteKey));
|
||||
}, [settingsRouteKey]);
|
||||
|
||||
const rowStyle = useCallback(
|
||||
({ pressed, hovered }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
|
||||
Reference in New Issue
Block a user