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 <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
João Salavisa
2026-07-19 00:50:38 +01:00
committed by GitHub
parent ad3e4d4c4b
commit d2d19aa1b5
3 changed files with 55 additions and 5 deletions
@@ -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 <Suspense>, React logged two React errors:
// "<AutomationsPage> 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 <AutomationsListPage>.
const { listPageSpy } = vi.hoisted(() => ({ listPageSpy: vi.fn() }));
vi.mock("@/components/automations/automations-list-page", () => ({
AutomationsListPage: (props: { workspaceId: string }) => {
listPageSpy(props);
return <div data-testid="automations-list-page">{props.workspaceId}</div>;
},
}));
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(<AutomationsPage workspaceId="ws-42" />);
// 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();
});
});
@@ -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 <AutomationsListPage workspaceId={id} />;
export default function AutomationsPage({ workspaceId }: Props) {
return <AutomationsListPage workspaceId={workspaceId} />;
}
+1 -1
View File
@@ -355,7 +355,7 @@ function renderWorkspaceSettingsRoute(pathname: string) {
if (section === "workflows") {
return <WorkspaceWorkflowsRoute workspaceId={id} />;
}
return <AutomationsPage params={Promise.resolve({ id })} />;
return <AutomationsPage workspaceId={id} />;
}
const workspaceId = matchSingle(pathname, /^\/settings\/workspace\/([^/]+)$/);