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
This commit is contained in:
gsxdsm
2026-04-08 20:04:23 -07:00
parent 2842088216
commit 47a035fde1
7 changed files with 538 additions and 143 deletions

View File

@@ -4,6 +4,7 @@ import type { UseProjectActionsResult } from "../hooks/useProjectActions";
import type { ModalManager } from "../hooks/useModalManager";
import type { UseTaskHandlersResult } from "../hooks/useTaskHandlers";
import type { Toast, ToastType } from "../hooks/useToast";
import { ModalErrorBoundary } from "./ErrorBoundary";
import { TaskDetailModal } from "./TaskDetailModal";
import { SettingsModal } from "./SettingsModal";
import { GitHubImportModal } from "./GitHubImportModal";
@@ -72,35 +73,39 @@ export function AppModals({
return (
<>
{modalManager.detailTask && (
<TaskDetailModal
task={modalManager.detailTask}
projectId={projectId}
tasks={tasks}
onClose={deepLink.handleDetailClose}
onOpenDetail={modalManager.openDetailTask}
onMoveTask={taskOperations.moveTask}
onDeleteTask={taskOperations.deleteTask}
onMergeTask={taskOperations.mergeTask}
onRetryTask={taskOperations.retryTask}
onDuplicateTask={taskOperations.duplicateTask}
onTaskUpdated={modalManager.updateDetailTask}
addToast={addToast}
githubTokenConfigured={settings.githubTokenConfigured}
initialTab={modalManager.detailTaskInitialTab}
/>
<ModalErrorBoundary>
<TaskDetailModal
task={modalManager.detailTask}
projectId={projectId}
tasks={tasks}
onClose={deepLink.handleDetailClose}
onOpenDetail={modalManager.openDetailTask}
onMoveTask={taskOperations.moveTask}
onDeleteTask={taskOperations.deleteTask}
onMergeTask={taskOperations.mergeTask}
onRetryTask={taskOperations.retryTask}
onDuplicateTask={taskOperations.duplicateTask}
onTaskUpdated={modalManager.updateDetailTask}
addToast={addToast}
githubTokenConfigured={settings.githubTokenConfigured}
initialTab={modalManager.detailTaskInitialTab}
/>
</ModalErrorBoundary>
)}
{modalManager.settingsOpen && (
<SettingsModal
onClose={modalManager.closeSettings}
addToast={addToast}
initialSection={modalManager.settingsInitialSection}
projectId={projectId}
themeMode={settings.themeMode}
colorTheme={settings.colorTheme}
onThemeModeChange={settings.setThemeMode}
onColorThemeChange={settings.setColorTheme}
/>
<ModalErrorBoundary>
<SettingsModal
onClose={modalManager.closeSettings}
addToast={addToast}
initialSection={modalManager.settingsInitialSection}
projectId={projectId}
themeMode={settings.themeMode}
colorTheme={settings.colorTheme}
onThemeModeChange={settings.setThemeMode}
onColorThemeChange={settings.setColorTheme}
/>
</ModalErrorBoundary>
)}
<GitHubImportModal
@@ -110,25 +115,29 @@ export function AppModals({
tasks={tasks}
/>
<PlanningModeModal
isOpen={modalManager.isPlanningOpen}
onClose={modalManager.closePlanning}
onTaskCreated={taskHandlers.handlePlanningTaskCreated}
onTasksCreated={taskHandlers.handlePlanningTasksCreated}
tasks={tasks}
initialPlan={modalManager.planningInitialPlan ?? undefined}
projectId={projectId}
resumeSessionId={modalManager.planningResumeSessionId}
/>
<ModalErrorBoundary>
<PlanningModeModal
isOpen={modalManager.isPlanningOpen}
onClose={modalManager.closePlanning}
onTaskCreated={taskHandlers.handlePlanningTaskCreated}
onTasksCreated={taskHandlers.handlePlanningTasksCreated}
tasks={tasks}
initialPlan={modalManager.planningInitialPlan ?? undefined}
projectId={projectId}
resumeSessionId={modalManager.planningResumeSessionId}
/>
</ModalErrorBoundary>
<SubtaskBreakdownModal
isOpen={modalManager.isSubtaskOpen}
onClose={modalManager.closeSubtask}
initialDescription={modalManager.subtaskInitialDescription ?? ""}
onTasksCreated={taskHandlers.handleSubtaskTasksCreated}
projectId={projectId}
resumeSessionId={modalManager.subtaskResumeSessionId}
/>
<ModalErrorBoundary>
<SubtaskBreakdownModal
isOpen={modalManager.isSubtaskOpen}
onClose={modalManager.closeSubtask}
initialDescription={modalManager.subtaskInitialDescription ?? ""}
onTasksCreated={taskHandlers.handleSubtaskTasksCreated}
projectId={projectId}
resumeSessionId={modalManager.subtaskResumeSessionId}
/>
</ModalErrorBoundary>
<TerminalModal
isOpen={modalManager.terminalOpen}
@@ -167,16 +176,18 @@ export function AppModals({
/>
)}
<NewTaskModal
isOpen={modalManager.newTaskModalOpen}
onClose={modalManager.closeNewTask}
tasks={tasks}
onCreateTask={taskHandlers.handleModalCreate}
addToast={addToast}
projectId={projectId}
onPlanningMode={modalManager.openPlanningWithInitialPlan}
onSubtaskBreakdown={modalManager.openSubtaskBreakdown}
/>
<ModalErrorBoundary>
<NewTaskModal
isOpen={modalManager.newTaskModalOpen}
onClose={modalManager.closeNewTask}
tasks={tasks}
onCreateTask={taskHandlers.handleModalCreate}
addToast={addToast}
projectId={projectId}
onPlanningMode={modalManager.openPlanningWithInitialPlan}
onSubtaskBreakdown={modalManager.openSubtaskBreakdown}
/>
</ModalErrorBoundary>
<ActivityLogModal
isOpen={modalManager.activityLogOpen}
@@ -193,19 +204,23 @@ export function AppModals({
}}
/>
<GitManagerModal
isOpen={modalManager.gitManagerOpen}
onClose={modalManager.closeGitManager}
tasks={tasks}
addToast={addToast}
/>
<ModalErrorBoundary>
<GitManagerModal
isOpen={modalManager.gitManagerOpen}
onClose={modalManager.closeGitManager}
tasks={tasks}
addToast={addToast}
/>
</ModalErrorBoundary>
<WorkflowStepManager
isOpen={modalManager.workflowStepsOpen}
onClose={modalManager.closeWorkflowSteps}
addToast={addToast}
projectId={projectId}
/>
<ModalErrorBoundary>
<WorkflowStepManager
isOpen={modalManager.workflowStepsOpen}
onClose={modalManager.closeWorkflowSteps}
addToast={addToast}
projectId={projectId}
/>
</ModalErrorBoundary>
<AgentListModal
isOpen={modalManager.agentsOpen}