mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 05:16:50 +01:00
fix(ui): contain routed-page render errors with a boundary
A render-time throw on a routed page (e.g. the New Agent page at /agents/new?adapterType=claude_local) had no boundary, so it unmounted the whole SPA and left a blank page. Because the custom router updates location without remounting, the back button changed the URL but the tree stayed dead until a hard refresh. Wrap the routed <Outlet/> in a RouteErrorBoundary that renders a recoverable error card, keeps the sidebar/nav mounted, and resets its error state whenever the route (pathname+search) changes so navigating away — back button included — recovers without a hard refresh.
This commit is contained in:
1 parent
6756ae8289
commit
b600e18626
2 files changed
+77
-1
No files matched your search
@@ -17,6 +17,7 @@ import { MobileBottomNav } from "./MobileBottomNav";
|
||||
import { WorktreeBanner } from "./WorktreeBanner";
|
||||
import { DevRestartBanner } from "./DevRestartBanner";
|
||||
import { StandaloneBrowserControls } from "./StandaloneBrowserControls";
|
||||
import { RouteErrorBoundary } from "./RouteErrorBoundary";
|
||||
import { SidebarShell } from "./SidebarShell";
|
||||
import { SecondarySidebar } from "./SecondarySidebar";
|
||||
import { SidebarAccountMenu } from "./SidebarAccountMenu";
|
||||
@@ -566,7 +567,9 @@ export function Layout() {
|
||||
requestedPrefix={companyPrefix ?? selectedCompany?.issuePrefix}
|
||||
/>
|
||||
) : (
|
||||
<Outlet />
|
||||
<RouteErrorBoundary>
|
||||
<Outlet />
|
||||
</RouteErrorBoundary>
|
||||
)}
|
||||
</main>
|
||||
<PropertiesPanel />
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
import { useLocation, useNavigate } from "@/lib/router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type RouteErrorBoundaryInnerProps = {
|
||||
resetKey: string;
|
||||
onReset: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
type RouteErrorBoundaryState = {
|
||||
error: Error | null;
|
||||
};
|
||||
|
||||
class RouteErrorBoundaryInner extends Component<RouteErrorBoundaryInnerProps, RouteErrorBoundaryState> {
|
||||
override state: RouteErrorBoundaryState = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: unknown): RouteErrorBoundaryState {
|
||||
return { error: error instanceof Error ? error : new Error(String(error)) };
|
||||
}
|
||||
|
||||
override componentDidCatch(error: unknown, info: ErrorInfo): void {
|
||||
console.error("Page render failed", { error, componentStack: info.componentStack });
|
||||
}
|
||||
|
||||
override componentDidUpdate(prevProps: RouteErrorBoundaryInnerProps): void {
|
||||
// A render throw with no boundary unmounts the whole app, so navigating
|
||||
// away (back button included) can't recover without a hard refresh. We sit
|
||||
// above the routed <Outlet />, so reset whenever the route changes.
|
||||
if (this.state.error && prevProps.resetKey !== this.props.resetKey) {
|
||||
this.setState({ error: null });
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { error } = this.state;
|
||||
if (!error) return this.props.children;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-4 px-4 py-10">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">This page hit an error</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Something went wrong while rendering this page. You can go back and try again, or reload.
|
||||
</p>
|
||||
</div>
|
||||
<pre className="overflow-auto rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive whitespace-pre-wrap">
|
||||
{error.message}
|
||||
</pre>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={this.props.onReset}>
|
||||
Go back
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => window.location.reload()}>
|
||||
Reload page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function RouteErrorBoundary({ children }: { children: ReactNode }) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const resetKey = `${location.pathname}${location.search}`;
|
||||
|
||||
return (
|
||||
<RouteErrorBoundaryInner resetKey={resetKey} onReset={() => navigate(-1)}>
|
||||
{children}
|
||||
</RouteErrorBoundaryInner>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user