Files
fusion/packages/dashboard/app/components/ErrorBoundary.tsx
gsxdsm ed77561c44 feat(FN-1286): add ErrorBoundary component with page-level, modal, and root-level wrapping
- Create ErrorBoundary and ModalErrorBoundary components with recovery actions (retry, reload)
- Add error boundary CSS styles with fallback UI styling
- Wrap main dashboard pages (Board, List, Settings, Agents) with PageErrorBoundary in App.tsx
- Wrap all key modals (task detail, planning, subtask breakdown, mission) with ModalErrorBoundary in AppModals.tsx
- Wrap entire App with RootErrorBoundary in main.tsx as final safety net
- Add 11 comprehensive ErrorBoundary tests covering all rendering and error scenarios
2026-04-08 20:04:23 -07:00

95 lines
2.7 KiB
TypeScript

import { Component, type ReactNode, type ErrorInfo } from "react";
import { AlertTriangle } from "lucide-react";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
level?: "page" | "modal" | "root";
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error("[ErrorBoundary]", error, errorInfo);
this.props.onError?.(error, errorInfo);
}
resetErrorBoundary = (): void => {
this.setState({ hasError: false, error: null });
};
render(): ReactNode {
if (!this.state.hasError) {
return this.props.children;
}
if (this.props.fallback) {
return this.props.fallback;
}
const level = this.props.level ?? "page";
const isModal = level === "modal";
const title = isModal
? "This section encountered an error"
: "Something went wrong";
return (
<div className={`error-boundary error-boundary--${level}`}>
<div className="error-boundary__icon">
<AlertTriangle size={40} />
</div>
<div className="error-boundary__title">{title}</div>
{this.state.error && (
<pre className="error-boundary__message">{this.state.error.message}</pre>
)}
<div className="error-boundary__actions">
<button className="btn btn-primary" onClick={this.resetErrorBoundary}>
Retry
</button>
<button className="btn" onClick={() => window.location.reload()}>
Reload page
</button>
</div>
</div>
);
}
}
export function PageErrorBoundary({ children, onError }: { children: ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void }) {
return (
<ErrorBoundary level="page" onError={onError}>
{children}
</ErrorBoundary>
);
}
export function ModalErrorBoundary({ children, onError }: { children: ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void }) {
return (
<ErrorBoundary level="modal" onError={onError}>
{children}
</ErrorBoundary>
);
}
export function RootErrorBoundary({ children, onError }: { children: ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void }) {
return (
<ErrorBoundary level="root" onError={onError}>
{children}
</ErrorBoundary>
);
}