mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Fix removed host routes getting stuck connecting
This commit is contained in:
@@ -180,6 +180,18 @@ export async function goBackInSettings(page: Page): Promise<void> {
|
||||
await page.getByRole("button", { name: "Back", exact: true }).click();
|
||||
}
|
||||
|
||||
export async function closeCompactSettings(page: Page): Promise<void> {
|
||||
await goBackInSettings(page);
|
||||
await expect(page).not.toHaveURL(/\/settings(\/|$)/);
|
||||
}
|
||||
|
||||
export async function removeCurrentHostFromSettings(page: Page): Promise<void> {
|
||||
await page.getByTestId("host-page-remove-host-button").click();
|
||||
await expect(page.getByTestId("remove-host-confirm-modal")).toBeVisible();
|
||||
await page.getByTestId("remove-host-confirm").click();
|
||||
await expect(page).toHaveURL(/\/settings$/);
|
||||
}
|
||||
|
||||
export async function expectSettingsBackButton(page: Page): Promise<void> {
|
||||
await expect(page.getByRole("button", { name: "Back", exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||
import { gotoAppShell, openSettings } from "./helpers/app";
|
||||
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
||||
import {
|
||||
closeCompactSettings,
|
||||
openSettingsSection,
|
||||
expectSettingsHeader,
|
||||
openAddHostFlow,
|
||||
@@ -31,9 +33,18 @@ import {
|
||||
selectSettingsHost,
|
||||
expectSettingsHostPickerLabel,
|
||||
openSettingsHostSection,
|
||||
removeCurrentHostFromSettings,
|
||||
} from "./helpers/settings";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
|
||||
async function openWorkspace(
|
||||
page: import("@playwright/test").Page,
|
||||
workspace: { workspaceId: string },
|
||||
) {
|
||||
await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.workspaceId));
|
||||
await expect(page.getByTestId("menu-button")).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe("Settings sidebar navigation", () => {
|
||||
test("clicking a sidebar section updates the URL and renders the section", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
@@ -187,4 +198,20 @@ test.describe("Settings — compact master-detail", () => {
|
||||
|
||||
await openSettingsHostSection(page, secondaryServerId, "connections");
|
||||
});
|
||||
|
||||
test("removing the last active host returns to welcome after settings closes", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}) => {
|
||||
const workspace = await withWorkspace({ prefix: "remove-host-compact-" });
|
||||
|
||||
await openWorkspace(page, workspace);
|
||||
await openCompactSettings(page);
|
||||
await openSettingsHostSection(page, getServerId(), "host");
|
||||
await removeCurrentHostFromSettings(page);
|
||||
await closeCompactSettings(page);
|
||||
|
||||
await expect(page).toHaveURL(/\/welcome$/);
|
||||
await expect(page.getByTestId("welcome-direct-connection")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,7 +84,6 @@ import { resolveActiveHost } from "@/utils/active-host";
|
||||
import { toggleDesktopSidebarsWithCheckoutIntent } from "@/utils/desktop-sidebar-toggle";
|
||||
import {
|
||||
buildHostRootRoute,
|
||||
mapPathnameToServer,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseServerIdFromPathname,
|
||||
parseWorkspaceOpenIntent,
|
||||
@@ -811,7 +810,6 @@ function OpenProjectListener() {
|
||||
}
|
||||
|
||||
function AppWithSidebar({ children }: { children: ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const params = useGlobalSearchParams<{ open?: string | string[] }>();
|
||||
const hosts = useHosts();
|
||||
@@ -820,16 +818,6 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
|
||||
const shouldShowAppChrome =
|
||||
storeReady && activeServerId !== null && hosts.some((host) => host.serverId === activeServerId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeServerId || hosts.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (hosts.some((host) => host.serverId === activeServerId)) {
|
||||
return;
|
||||
}
|
||||
router.replace(mapPathnameToServer(pathname, hosts[0].serverId));
|
||||
}, [activeServerId, hosts, pathname, router]);
|
||||
|
||||
// Parse selectedAgentKey directly from pathname
|
||||
// useLocalSearchParams doesn't update when navigating between same-pattern routes
|
||||
const selectedAgentKey = useMemo(() => {
|
||||
|
||||
25
packages/app/src/app/h/[serverId]/_layout.tsx
Normal file
25
packages/app/src/app/h/[serverId]/_layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Redirect, Slot, useLocalSearchParams } from "expo-router";
|
||||
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
|
||||
import { useHosts } from "@/runtime/host-runtime";
|
||||
import { resolveKnownHostRoute } from "@/utils/host-routes";
|
||||
|
||||
export default function HostRouteLayout() {
|
||||
return (
|
||||
<HostRouteBootstrapBoundary>
|
||||
<KnownHostRoute />
|
||||
</HostRouteBootstrapBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function KnownHostRoute() {
|
||||
const params = useLocalSearchParams<{ serverId?: string | string[] }>();
|
||||
const hosts = useHosts();
|
||||
const routeServerId = typeof params.serverId === "string" ? params.serverId : null;
|
||||
const resolution = resolveKnownHostRoute({ routeServerId, hosts });
|
||||
|
||||
if (resolution.kind === "redirect") {
|
||||
return <Redirect href={resolution.href} />;
|
||||
}
|
||||
|
||||
return <Slot />;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
parseHostWorkspaceOpenIntentFromPathname,
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
parseWorkspaceOpenIntent,
|
||||
resolveKnownHostRoute,
|
||||
} from "./host-routes";
|
||||
|
||||
describe("parseHostAgentRouteFromPathname", () => {
|
||||
@@ -190,3 +191,32 @@ describe("host settings section slugs", () => {
|
||||
expect(normalizeHostSectionSlug("daemon")).toBe("host");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveKnownHostRoute", () => {
|
||||
it("renders when the route host is still saved", () => {
|
||||
expect(
|
||||
resolveKnownHostRoute({
|
||||
routeServerId: "srv-current",
|
||||
hosts: [{ serverId: "srv-current" }, { serverId: "srv-next" }],
|
||||
}),
|
||||
).toEqual({ kind: "render" });
|
||||
});
|
||||
|
||||
it("sends removed host routes to the next saved host home", () => {
|
||||
expect(
|
||||
resolveKnownHostRoute({
|
||||
routeServerId: "srv-removed",
|
||||
hosts: [{ serverId: "srv-next" }],
|
||||
}),
|
||||
).toEqual({ kind: "redirect", href: "/h/srv-next/open-project" });
|
||||
});
|
||||
|
||||
it("sends host routes to welcome when no hosts are saved", () => {
|
||||
expect(
|
||||
resolveKnownHostRoute({
|
||||
routeServerId: "srv-removed",
|
||||
hosts: [],
|
||||
}),
|
||||
).toEqual({ kind: "redirect", href: "/welcome" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -355,6 +355,27 @@ export function buildHostOpenProjectRoute(serverId: string) {
|
||||
return `${base}/open-project` as const;
|
||||
}
|
||||
|
||||
export type KnownHostRouteResolution =
|
||||
| { kind: "render" }
|
||||
| { kind: "redirect"; href: ReturnType<typeof buildHostOpenProjectRoute> | "/welcome" };
|
||||
|
||||
export function resolveKnownHostRoute(input: {
|
||||
routeServerId: string | null | undefined;
|
||||
hosts: readonly { serverId: string }[];
|
||||
}): KnownHostRouteResolution {
|
||||
const routeServerId = trimNonEmpty(input.routeServerId);
|
||||
if (routeServerId && input.hosts.some((host) => host.serverId === routeServerId)) {
|
||||
return { kind: "render" };
|
||||
}
|
||||
|
||||
const fallbackServerId = input.hosts[0]?.serverId;
|
||||
if (fallbackServerId) {
|
||||
return { kind: "redirect", href: buildHostOpenProjectRoute(fallbackServerId) };
|
||||
}
|
||||
|
||||
return { kind: "redirect", href: "/welcome" };
|
||||
}
|
||||
|
||||
export function buildHostNewWorkspaceRoute(
|
||||
serverId: string,
|
||||
sourceDirectory?: string,
|
||||
|
||||
Reference in New Issue
Block a user