fix(web): break circular links in DegradedProjectState — Back to project and Open dashboard view both point to current page (#1868)

* fix(web): break circular links in DegradedProjectState

Both "Back to project" and "Open dashboard view" linked to
projectDashboardPath(projectId) — the same page the component is
rendered on. Replace with working navigation:

- "Back to project" → "/" (global dashboard)
- "Open dashboard view" → "/projects/{id}/settings" (edit config)

Fixes #1867

* fix(web): break circular links in DegradedProjectState

Both "Back to project" and "Open dashboard view" linked to
projectDashboardPath(projectId) — the same page the component is
rendered on. Replace with working navigation:

- "Back to project" → "/" (global dashboard)
- "Open dashboard view" → "/projects/{id}/settings" (edit config)

Fixes #1867

* fix(web): remove circular 'Edit settings' link from DegradedProjectState

The 'Edit settings' link pointed to /projects/{id}/settings, which also
renders DegradedProjectState — making the button a self-link on the
settings page. Remove it entirely; 'Back to dashboard' (/) is the only
escape hatch needed.

Add DegradedProjectState component tests and assert both pages show no
'Edit settings' link and that 'Back to dashboard' points to /.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Aditi Chauhan <aditi1178@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
i-trytoohard 2026-05-21 19:55:17 +05:30 committed by GitHub
parent a66a087ef6
commit 50dc18ffa1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 93 additions and 9 deletions

View File

@ -80,5 +80,7 @@ describe("ProjectPage", () => {
expect(screen.getByText("This project's config failed to load")).toBeInTheDocument();
expect(screen.getByText("Local config failed validation")).toBeInTheDocument();
expect(screen.queryByTestId("dashboard")).not.toBeInTheDocument();
expect(screen.getByRole("link", { name: "Back to dashboard" })).toHaveAttribute("href", "/");
expect(screen.queryByRole("link", { name: "Edit settings" })).not.toBeInTheDocument();
});
});

View File

@ -77,5 +77,7 @@ describe("ProjectSettingsPage", () => {
).toBeInTheDocument();
expect(screen.getByText("Local config failed validation")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Save changes" })).not.toBeInTheDocument();
expect(screen.getByRole("link", { name: "Back to dashboard" })).toHaveAttribute("href", "/");
expect(screen.queryByRole("link", { name: "Edit settings" })).not.toBeInTheDocument();
});
});

View File

@ -1,5 +1,5 @@
import Link from "next/link";
import { projectDashboardPath } from "@/lib/routes";
import { RepairDegradedProjectButton } from "./RepairDegradedProjectButton";
interface DegradedProjectStateProps {
@ -61,16 +61,10 @@ export function DegradedProjectState({
<div className="mt-6 flex flex-wrap gap-3">
<Link
href={projectDashboardPath(projectId)}
href="/"
className="rounded-lg border border-[var(--color-border-default)] px-4 py-2 text-sm font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-bg-elevated-hover)]"
>
Back to project
</Link>
<Link
href={projectDashboardPath(projectId)}
className="rounded-lg border border-[var(--color-border-default)] px-4 py-2 text-sm font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-bg-elevated-hover)]"
>
Open dashboard view
Back to dashboard
</Link>
</div>
</div>

View File

@ -0,0 +1,86 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { DegradedProjectState } from "@/components/DegradedProjectState";
vi.mock("next/link", () => ({
default: ({
children,
...props
}: React.PropsWithChildren<React.AnchorHTMLAttributes<HTMLAnchorElement>>) => (
<a {...props}>{children}</a>
),
}));
vi.mock("@/components/RepairDegradedProjectButton", () => ({
RepairDegradedProjectButton: ({ projectId }: { projectId: string }) => (
<button>Repair {projectId}</button>
),
}));
const baseProps = {
projectId: "my-project",
resolveError: "Local config at /tmp/my-project/agent-orchestrator.yaml failed validation: bad field",
projectPath: "/tmp/my-project",
};
describe("DegradedProjectState", () => {
it("renders the default heading", () => {
render(<DegradedProjectState {...baseProps} />);
expect(screen.getByText("This project's config failed to load")).toBeInTheDocument();
});
it("renders a custom heading when provided", () => {
render(<DegradedProjectState {...baseProps} heading="Custom heading" />);
expect(screen.getByText("Custom heading")).toBeInTheDocument();
});
it("displays the resolve error", () => {
render(<DegradedProjectState {...baseProps} />);
expect(screen.getByText(baseProps.resolveError)).toBeInTheDocument();
});
it("extracts and displays the config path from the resolve error", () => {
render(<DegradedProjectState {...baseProps} />);
expect(
screen.getByText("/tmp/my-project/agent-orchestrator.yaml"),
).toBeInTheDocument();
});
it("falls back to the project path when the config path cannot be parsed from the error", () => {
render(
<DegradedProjectState
{...baseProps}
resolveError="Something went wrong"
/>,
);
expect(
screen.getByText("/tmp/my-project/agent-orchestrator.yaml or .yml"),
).toBeInTheDocument();
});
it("renders 'Back to dashboard' link pointing to /", () => {
render(<DegradedProjectState {...baseProps} />);
const link = screen.getByRole("link", { name: "Back to dashboard" });
expect(link).toHaveAttribute("href", "/");
});
it("does not render an 'Edit settings' link", () => {
render(<DegradedProjectState {...baseProps} />);
expect(screen.queryByRole("link", { name: "Edit settings" })).not.toBeInTheDocument();
});
it("does not show the repair button for a generic validation error", () => {
render(<DegradedProjectState {...baseProps} />);
expect(screen.queryByRole("button", { name: /repair/i })).not.toBeInTheDocument();
});
it("shows the repair button when the error indicates a wrapped projects format", () => {
render(
<DegradedProjectState
{...baseProps}
resolveError="Local config at /tmp/x/agent-orchestrator.yaml still uses a wrapped projects: format"
/>,
);
expect(screen.getByRole("button", { name: /repair/i })).toBeInTheDocument();
});
});