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();
|
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> {
|
export async function expectSettingsBackButton(page: Page): Promise<void> {
|
||||||
await expect(page.getByRole("button", { name: "Back", exact: true })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Back", exact: true })).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { test, expect } from "./fixtures";
|
import { test, expect } from "./fixtures";
|
||||||
|
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||||
import { gotoAppShell, openSettings } from "./helpers/app";
|
import { gotoAppShell, openSettings } from "./helpers/app";
|
||||||
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
||||||
import {
|
import {
|
||||||
|
closeCompactSettings,
|
||||||
openSettingsSection,
|
openSettingsSection,
|
||||||
expectSettingsHeader,
|
expectSettingsHeader,
|
||||||
openAddHostFlow,
|
openAddHostFlow,
|
||||||
@@ -31,9 +33,18 @@ import {
|
|||||||
selectSettingsHost,
|
selectSettingsHost,
|
||||||
expectSettingsHostPickerLabel,
|
expectSettingsHostPickerLabel,
|
||||||
openSettingsHostSection,
|
openSettingsHostSection,
|
||||||
|
removeCurrentHostFromSettings,
|
||||||
} from "./helpers/settings";
|
} from "./helpers/settings";
|
||||||
import { getServerId } from "./helpers/server-id";
|
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.describe("Settings sidebar navigation", () => {
|
||||||
test("clicking a sidebar section updates the URL and renders the section", async ({ page }) => {
|
test("clicking a sidebar section updates the URL and renders the section", async ({ page }) => {
|
||||||
await gotoAppShell(page);
|
await gotoAppShell(page);
|
||||||
@@ -187,4 +198,20 @@ test.describe("Settings — compact master-detail", () => {
|
|||||||
|
|
||||||
await openSettingsHostSection(page, secondaryServerId, "connections");
|
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 { toggleDesktopSidebarsWithCheckoutIntent } from "@/utils/desktop-sidebar-toggle";
|
||||||
import {
|
import {
|
||||||
buildHostRootRoute,
|
buildHostRootRoute,
|
||||||
mapPathnameToServer,
|
|
||||||
parseHostAgentRouteFromPathname,
|
parseHostAgentRouteFromPathname,
|
||||||
parseServerIdFromPathname,
|
parseServerIdFromPathname,
|
||||||
parseWorkspaceOpenIntent,
|
parseWorkspaceOpenIntent,
|
||||||
@@ -811,7 +810,6 @@ function OpenProjectListener() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AppWithSidebar({ children }: { children: ReactNode }) {
|
function AppWithSidebar({ children }: { children: ReactNode }) {
|
||||||
const router = useRouter();
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const params = useGlobalSearchParams<{ open?: string | string[] }>();
|
const params = useGlobalSearchParams<{ open?: string | string[] }>();
|
||||||
const hosts = useHosts();
|
const hosts = useHosts();
|
||||||
@@ -820,16 +818,6 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
|
|||||||
const shouldShowAppChrome =
|
const shouldShowAppChrome =
|
||||||
storeReady && activeServerId !== null && hosts.some((host) => host.serverId === activeServerId);
|
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
|
// Parse selectedAgentKey directly from pathname
|
||||||
// useLocalSearchParams doesn't update when navigating between same-pattern routes
|
// useLocalSearchParams doesn't update when navigating between same-pattern routes
|
||||||
const selectedAgentKey = useMemo(() => {
|
const selectedAgentKey = useMemo(() => {
|
||||||
|
|||||||
@@ -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,
|
parseHostWorkspaceOpenIntentFromPathname,
|
||||||
parseHostWorkspaceRouteFromPathname,
|
parseHostWorkspaceRouteFromPathname,
|
||||||
parseWorkspaceOpenIntent,
|
parseWorkspaceOpenIntent,
|
||||||
|
resolveKnownHostRoute,
|
||||||
} from "./host-routes";
|
} from "./host-routes";
|
||||||
|
|
||||||
describe("parseHostAgentRouteFromPathname", () => {
|
describe("parseHostAgentRouteFromPathname", () => {
|
||||||
@@ -190,3 +191,32 @@ describe("host settings section slugs", () => {
|
|||||||
expect(normalizeHostSectionSlug("daemon")).toBe("host");
|
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;
|
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(
|
export function buildHostNewWorkspaceRoute(
|
||||||
serverId: string,
|
serverId: string,
|
||||||
sourceDirectory?: string,
|
sourceDirectory?: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user