From d2d19aa1b51abb05699fba1d47fecc6350bcdaa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Salavisa?= Date: Sun, 19 Jul 2026 00:50:38 +0100 Subject: [PATCH] fix: stop async client component error on automations settings (#1788) The /settings/workspace/[id]/automations page was a leftover Next.js server component (async, awaiting a params promise in render). Mounted as a client component in the Vite SPA, it logged React errors: "is an async Client Component" and "suspended by an uncached promise". Convert it to a plain synchronous client component taking a workspaceId prop and delegating data loading to AutomationsListPage, matching the sibling repositories/workflows routes. Update the call site to pass workspaceId directly instead of a resolved params promise. Co-authored-by: Kandev Agent Co-authored-by: Claude Fable 5 --- .../workspace/[id]/automations/page.test.tsx | 49 +++++++++++++++++++ .../workspace/[id]/automations/page.tsx | 9 ++-- apps/web/src/settings-routes.tsx | 2 +- 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 apps/web/app/settings/workspace/[id]/automations/page.test.tsx diff --git a/apps/web/app/settings/workspace/[id]/automations/page.test.tsx b/apps/web/app/settings/workspace/[id]/automations/page.test.tsx new file mode 100644 index 0000000000..532630ce36 --- /dev/null +++ b/apps/web/app/settings/workspace/[id]/automations/page.test.tsx @@ -0,0 +1,49 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +// Regression coverage for the workspace-scoped +// /settings/workspace/[id]/automations page. +// +// The original implementation was an async server-style component that awaited +// a `params` promise in its render body (`async function AutomationsPage`). +// Rendered on the client under , React logged two React errors: +// " is an async Client Component. Only Server Components can be +// async" and "A component was suspended by an uncached promise". The page must +// be a plain synchronous client component that takes `workspaceId` directly and +// delegates data loading to . + +const { listPageSpy } = vi.hoisted(() => ({ listPageSpy: vi.fn() })); + +vi.mock("@/components/automations/automations-list-page", () => ({ + AutomationsListPage: (props: { workspaceId: string }) => { + listPageSpy(props); + return
{props.workspaceId}
; + }, +})); + +import AutomationsPage from "./page"; + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe("AutomationsPage (workspace-scoped)", () => { + it("is a synchronous component, not an async one", () => { + // An async function's constructor name is "AsyncFunction"; a plain one is + // "Function". This is the core regression: an async client component throws. + expect(AutomationsPage.constructor.name).toBe("Function"); + }); + + it("renders the automations list for the given workspace without suspending", () => { + const element = render(); + + // Renders synchronously (no thrown promise / suspense fallback) and forwards + // the workspace id straight through to the list page. + expect(screen.getByTestId("automations-list-page").textContent).toBe("ws-42"); + expect(listPageSpy).toHaveBeenCalledTimes(1); + expect(listPageSpy).toHaveBeenCalledWith({ workspaceId: "ws-42" }); + + element.unmount(); + }); +}); diff --git a/apps/web/app/settings/workspace/[id]/automations/page.tsx b/apps/web/app/settings/workspace/[id]/automations/page.tsx index c3d929c175..fc80607658 100644 --- a/apps/web/app/settings/workspace/[id]/automations/page.tsx +++ b/apps/web/app/settings/workspace/[id]/automations/page.tsx @@ -1,10 +1,11 @@ +"use client"; + import { AutomationsListPage } from "@/components/automations/automations-list-page"; type Props = { - params: Promise<{ id: string }>; + workspaceId: string; }; -export default async function AutomationsPage({ params }: Props) { - const { id } = await params; - return ; +export default function AutomationsPage({ workspaceId }: Props) { + return ; } diff --git a/apps/web/src/settings-routes.tsx b/apps/web/src/settings-routes.tsx index bf6a512f78..d075263c27 100644 --- a/apps/web/src/settings-routes.tsx +++ b/apps/web/src/settings-routes.tsx @@ -355,7 +355,7 @@ function renderWorkspaceSettingsRoute(pathname: string) { if (section === "workflows") { return ; } - return ; + return ; } const workspaceId = matchSingle(pathname, /^\/settings\/workspace\/([^/]+)$/);