refactor(FN-1289): extract AppInner logic into hooks and modal component
- Extract view state, project actions, and task handler concerns from AppInner into dedicated hooks - Add AppModals component to centralize modal rendering and related props wiring - Inline thin aliases and complete AppInner cleanup after moving responsibilities out - Add unit tests for useViewState, useProjectActions, and useTaskHandlers hooks
This commit is contained in:
@@ -1,33 +1,12 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import type { TaskDetail, TaskCreateInput, Task, ThemeMode } from "@fusion/core";
|
||||
import { updateProject, unregisterProject } from "./api";
|
||||
import type { ProjectInfo } from "./api";
|
||||
import { useState, useCallback } from "react";
|
||||
import type { TaskDetail } from "@fusion/core";
|
||||
import { Header, useViewportMode } from "./components/Header";
|
||||
import { Board } from "./components/Board";
|
||||
import { ListView } from "./components/ListView";
|
||||
import { ProjectOverview } from "./components/ProjectOverview";
|
||||
import { SetupWizardModal } from "./components/SetupWizardModal";
|
||||
import { TaskDetailModal } from "./components/TaskDetailModal";
|
||||
import { TerminalModal } from "./components/TerminalModal";
|
||||
import { FileBrowserModal } from "./components/FileBrowserModal";
|
||||
import { SettingsModal } from "./components/SettingsModal";
|
||||
import { ModelOnboardingModal } from "./components/ModelOnboardingModal";
|
||||
import { PlanningModeModal } from "./components/PlanningModeModal";
|
||||
import { SubtaskBreakdownModal } from "./components/SubtaskBreakdownModal";
|
||||
import { ToastContainer } from "./components/ToastContainer";
|
||||
import { GitHubImportModal } from "./components/GitHubImportModal";
|
||||
import { GitManagerModal } from "./components/GitManagerModal";
|
||||
import { UsageIndicator } from "./components/UsageIndicator";
|
||||
import { NewTaskModal } from "./components/NewTaskModal";
|
||||
import { ScheduledTasksModal } from "./components/ScheduledTasksModal";
|
||||
import { ActivityLogModal } from "./components/ActivityLogModal";
|
||||
import { WorkflowStepManager } from "./components/WorkflowStepManager";
|
||||
import { MissionManager } from "./components/MissionManager";
|
||||
import { AgentListModal } from "./components/AgentListModal";
|
||||
import { AgentsView } from "./components/AgentsView";
|
||||
import { NodesView } from "./components/NodesView";
|
||||
import { MailboxModal } from "./components/MailboxModal";
|
||||
import { ScriptsModal } from "./components/ScriptsModal";
|
||||
import { AppModals } from "./components/AppModals";
|
||||
import { ExecutorStatusBar } from "./components/ExecutorStatusBar";
|
||||
import { MobileNavBar } from "./components/MobileNavBar";
|
||||
import { QuickChatFAB } from "./components/QuickChatFAB";
|
||||
@@ -43,10 +22,9 @@ import { useAppSettings } from "./hooks/useAppSettings";
|
||||
import { useDeepLink } from "./hooks/useDeepLink";
|
||||
import { useFavorites } from "./hooks/useFavorites";
|
||||
import { useAuthOnboarding } from "./hooks/useAuthOnboarding";
|
||||
import { getScopedItem, setScopedItem } from "./utils/projectStorage";
|
||||
|
||||
type ViewMode = "overview" | "project";
|
||||
type TaskView = "board" | "list" | "agents";
|
||||
import { useViewState } from "./hooks/useViewState";
|
||||
import { useProjectActions } from "./hooks/useProjectActions";
|
||||
import { useTaskHandlers } from "./hooks/useTaskHandlers";
|
||||
|
||||
function AppInner() {
|
||||
const { toasts, addToast, removeToast } = useToast();
|
||||
@@ -68,109 +46,11 @@ function AppInner() {
|
||||
// Background AI sessions
|
||||
const { sessions: bgSessions, generating: bgGenerating, needsInput: bgNeedsInput, planningSessions: bgPlanningSessions, dismissSession: bgDismiss } = useBackgroundSessions(currentProject?.id);
|
||||
|
||||
// View state
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem("kb-dashboard-view-mode");
|
||||
if (saved === "overview" || saved === "project") return saved;
|
||||
}
|
||||
return "overview";
|
||||
});
|
||||
|
||||
const [taskView, setTaskView] = useState<TaskView>(() => {
|
||||
const saved = getScopedItem("kb-dashboard-task-view");
|
||||
if (saved === "board" || saved === "list" || saved === "agents") return saved;
|
||||
return "board";
|
||||
});
|
||||
|
||||
const viewportMode = useViewportMode();
|
||||
const isMobile = viewportMode === "mobile";
|
||||
|
||||
// Modal state/handlers extracted to a dedicated manager hook.
|
||||
const {
|
||||
newTaskModalOpen,
|
||||
isPlanningOpen,
|
||||
planningInitialPlan,
|
||||
planningResumeSessionId,
|
||||
isSubtaskOpen,
|
||||
subtaskInitialDescription,
|
||||
subtaskResumeSessionId,
|
||||
detailTask,
|
||||
detailTaskInitialTab,
|
||||
settingsOpen,
|
||||
settingsInitialSection,
|
||||
schedulesOpen,
|
||||
githubImportOpen,
|
||||
usageOpen,
|
||||
terminalOpen,
|
||||
terminalInitialCommand,
|
||||
filesOpen,
|
||||
fileBrowserWorkspace,
|
||||
activityLogOpen,
|
||||
mailboxOpen,
|
||||
mailboxUnreadCount,
|
||||
mailboxAgents,
|
||||
gitManagerOpen,
|
||||
workflowStepsOpen,
|
||||
missionsOpen,
|
||||
missionResumeSessionId,
|
||||
missionTargetId,
|
||||
agentsOpen,
|
||||
scriptsOpen,
|
||||
setupWizardOpen,
|
||||
modelOnboardingOpen,
|
||||
anyModalOpen,
|
||||
openNewTask,
|
||||
closeNewTask,
|
||||
openPlanning,
|
||||
openPlanningWithInitialPlan,
|
||||
resumePlanning,
|
||||
openPlanningWithSession,
|
||||
closePlanning,
|
||||
openSubtaskBreakdown,
|
||||
openSubtaskWithSession,
|
||||
closeSubtask,
|
||||
openDetailTask,
|
||||
openDetailWithChangesTab,
|
||||
updateDetailTask,
|
||||
closeDetailTask,
|
||||
openSettings,
|
||||
closeSettings,
|
||||
openSchedules,
|
||||
closeSchedules,
|
||||
openGitHubImport,
|
||||
closeGitHubImport,
|
||||
openUsage,
|
||||
closeUsage,
|
||||
toggleTerminal,
|
||||
closeTerminal,
|
||||
openFiles,
|
||||
closeFiles,
|
||||
setFileWorkspace,
|
||||
openActivityLog,
|
||||
closeActivityLog,
|
||||
openMailbox,
|
||||
closeMailbox,
|
||||
openGitManager,
|
||||
closeGitManager,
|
||||
openWorkflowSteps,
|
||||
closeWorkflowSteps,
|
||||
openMissions,
|
||||
openMissionById,
|
||||
openMissionWithSession,
|
||||
closeMissions,
|
||||
closeAgents,
|
||||
openScripts,
|
||||
closeScripts,
|
||||
runScript,
|
||||
openSetupWizard,
|
||||
closeSetupWizard,
|
||||
openModelOnboarding,
|
||||
closeModelOnboarding,
|
||||
onPlanningTaskCreated,
|
||||
onPlanningTasksCreated,
|
||||
onSubtaskTasksCreated,
|
||||
} = useModalManager({
|
||||
const modalManager = useModalManager({
|
||||
projectId: currentProject?.id,
|
||||
planningSessions: bgPlanningSessions,
|
||||
});
|
||||
@@ -200,85 +80,50 @@ function AppInner() {
|
||||
toggleFavoriteModel,
|
||||
} = useFavorites();
|
||||
|
||||
// Persist view mode
|
||||
useEffect(() => {
|
||||
localStorage.setItem("kb-dashboard-view-mode", viewMode);
|
||||
}, [viewMode]);
|
||||
|
||||
// Load project-scoped task view when project context changes
|
||||
useEffect(() => {
|
||||
const saved = getScopedItem("kb-dashboard-task-view", currentProject?.id);
|
||||
if (saved === "board" || saved === "list" || saved === "agents") {
|
||||
setTaskView(saved);
|
||||
return;
|
||||
}
|
||||
setTaskView("board");
|
||||
}, [currentProject?.id]);
|
||||
|
||||
// Persist task view
|
||||
useEffect(() => {
|
||||
setScopedItem("kb-dashboard-task-view", taskView, currentProject?.id);
|
||||
}, [currentProject?.id, taskView]);
|
||||
|
||||
// Sync view mode when current project is restored from localStorage
|
||||
useEffect(() => {
|
||||
// Wait for both loading states to complete before syncing
|
||||
if (projectsLoading || currentProjectLoading) return;
|
||||
|
||||
// If we have a restored current project but viewMode is overview, sync to project view
|
||||
if (currentProject && viewMode === "overview") {
|
||||
setViewMode("project");
|
||||
}
|
||||
}, [projectsLoading, currentProjectLoading, currentProject, viewMode]);
|
||||
|
||||
// Auto-open setup wizard on first run (no projects)
|
||||
useEffect(() => {
|
||||
// Wait for both loading states to complete before making decision
|
||||
if (projectsLoading || currentProjectLoading) return;
|
||||
|
||||
// Don't open if wizard is already open
|
||||
if (setupWizardOpen) return;
|
||||
|
||||
// Don't open if we have projects OR a saved current project
|
||||
if (projects.length > 0 || currentProject) return;
|
||||
|
||||
// Only open when truly no projects exist and no project is being restored
|
||||
const timer = setTimeout(() => {
|
||||
openSetupWizard();
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [projectsLoading, projects.length, currentProjectLoading, currentProject, setupWizardOpen, openSetupWizard]);
|
||||
|
||||
// Theme toggle handler: cycles Dark → Light → System → Dark
|
||||
const handleToggleTheme = useCallback(() => {
|
||||
const cycle: ThemeMode[] = ["dark", "light", "system"];
|
||||
const currentIndex = cycle.indexOf(themeMode);
|
||||
const nextMode = cycle[(currentIndex + 1) % cycle.length];
|
||||
setThemeMode(nextMode);
|
||||
}, [themeMode, setThemeMode]);
|
||||
const { viewMode, setViewMode, taskView, handleChangeTaskView, handleToggleTheme } = useViewState({
|
||||
projectsLoading,
|
||||
currentProjectLoading,
|
||||
currentProject,
|
||||
projectsLength: projects.length,
|
||||
setupWizardOpen: modalManager.setupWizardOpen,
|
||||
openSetupWizard: modalManager.openSetupWizard,
|
||||
themeMode,
|
||||
setThemeMode,
|
||||
});
|
||||
|
||||
// Auth and onboarding bootstrap logic extracted to a dedicated hook.
|
||||
useAuthOnboarding({
|
||||
projectId: currentProject?.id,
|
||||
openModelOnboarding,
|
||||
openSettings,
|
||||
openModelOnboarding: modalManager.openModelOnboarding,
|
||||
openSettings: modalManager.openSettings,
|
||||
});
|
||||
|
||||
const handleToggleFavorite = useCallback(async (provider: string) => {
|
||||
try {
|
||||
await toggleFavoriteProvider(provider);
|
||||
} catch {
|
||||
addToast("Failed to update favorites", "error");
|
||||
}
|
||||
}, [toggleFavoriteProvider, addToast]);
|
||||
|
||||
const handleToggleModelFavorite = useCallback(async (modelId: string) => {
|
||||
try {
|
||||
await toggleFavoriteModel(modelId);
|
||||
} catch {
|
||||
addToast("Failed to update model favorites", "error");
|
||||
}
|
||||
}, [toggleFavoriteModel, addToast]);
|
||||
const {
|
||||
handleSelectProject,
|
||||
handleViewAllProjects,
|
||||
handleOpenSettings,
|
||||
handleAddProject,
|
||||
handleSetupComplete,
|
||||
handleModelOnboardingComplete,
|
||||
handlePauseProject,
|
||||
handleResumeProject,
|
||||
handleRemoveProject,
|
||||
handleToggleFavorite,
|
||||
handleToggleModelFavorite,
|
||||
} = useProjectActions({
|
||||
setCurrentProject,
|
||||
clearCurrentProject,
|
||||
setViewMode,
|
||||
currentProject,
|
||||
refreshProjects,
|
||||
toggleFavoriteProvider,
|
||||
toggleFavoriteModel,
|
||||
addToast,
|
||||
openSettings: modalManager.openSettings,
|
||||
openSetupWizard: modalManager.openSetupWizard,
|
||||
closeSetupWizard: modalManager.closeSetupWizard,
|
||||
closeModelOnboarding: modalManager.closeModelOnboarding,
|
||||
});
|
||||
|
||||
const { handleDetailClose } = useDeepLink({
|
||||
projectId: currentProject?.id,
|
||||
@@ -287,186 +132,37 @@ function AppInner() {
|
||||
currentProject,
|
||||
setCurrentProject,
|
||||
addToast,
|
||||
openTaskDetail: openDetailTask,
|
||||
closeTaskDetail: closeDetailTask,
|
||||
openTaskDetail: modalManager.openDetailTask,
|
||||
closeTaskDetail: modalManager.closeDetailTask,
|
||||
});
|
||||
|
||||
// View change handlers
|
||||
const handleChangeTaskView = useCallback((newView: TaskView) => {
|
||||
setTaskView(newView);
|
||||
}, []);
|
||||
|
||||
// Project selection handlers
|
||||
const handleSelectProject = useCallback((project: ProjectInfo) => {
|
||||
setCurrentProject(project);
|
||||
setViewMode("project");
|
||||
}, [setCurrentProject]);
|
||||
|
||||
const handleViewAllProjects = useCallback(() => {
|
||||
clearCurrentProject();
|
||||
setViewMode("overview");
|
||||
}, [clearCurrentProject]);
|
||||
|
||||
const handleOpenSettings = useCallback(() => {
|
||||
openSettings();
|
||||
}, [openSettings]);
|
||||
|
||||
const handleAddProject = useCallback(() => {
|
||||
openSetupWizard();
|
||||
}, [openSetupWizard]);
|
||||
|
||||
const handleSetupComplete = useCallback((project: ProjectInfo) => {
|
||||
closeSetupWizard();
|
||||
setCurrentProject(project);
|
||||
setViewMode("project");
|
||||
addToast(`Project ${project.name} registered successfully`, "success");
|
||||
refreshProjects();
|
||||
}, [closeSetupWizard, setCurrentProject, addToast, refreshProjects]);
|
||||
|
||||
const handleModelOnboardingComplete = useCallback(() => {
|
||||
closeModelOnboarding();
|
||||
}, [closeModelOnboarding]);
|
||||
|
||||
const handlePauseProject = useCallback(async (project: ProjectInfo) => {
|
||||
try {
|
||||
await updateProject(project.id, { status: "paused" });
|
||||
addToast(`Project ${project.name} paused`, "success");
|
||||
refreshProjects();
|
||||
} catch {
|
||||
addToast(`Failed to pause project ${project.name}`, "error");
|
||||
}
|
||||
}, [addToast, refreshProjects]);
|
||||
|
||||
const handleResumeProject = useCallback(async (project: ProjectInfo) => {
|
||||
try {
|
||||
await updateProject(project.id, { status: "active" });
|
||||
addToast(`Project ${project.name} resumed`, "success");
|
||||
refreshProjects();
|
||||
} catch {
|
||||
addToast(`Failed to resume project ${project.name}`, "error");
|
||||
}
|
||||
}, [addToast, refreshProjects]);
|
||||
|
||||
const handleRemoveProject = useCallback(async (project: ProjectInfo) => {
|
||||
try {
|
||||
await unregisterProject(project.id);
|
||||
addToast(`Project ${project.name} removed`, "success");
|
||||
// If we removed the current project, go back to overview
|
||||
if (currentProject?.id === project.id) {
|
||||
clearCurrentProject();
|
||||
setViewMode("overview");
|
||||
}
|
||||
refreshProjects();
|
||||
} catch {
|
||||
addToast(`Failed to remove project ${project.name}`, "error");
|
||||
}
|
||||
}, [unregisterProject, currentProject, clearCurrentProject, addToast, refreshProjects]);
|
||||
|
||||
// Task handlers
|
||||
const handleNewTaskOpen = openNewTask;
|
||||
const handleNewTaskClose = closeNewTask;
|
||||
|
||||
const handleBoardQuickCreate = useCallback(
|
||||
async (input: TaskCreateInput): Promise<void> => {
|
||||
await createTask({ ...input, column: "triage" });
|
||||
},
|
||||
[createTask],
|
||||
);
|
||||
|
||||
const handleModalCreate = useCallback(
|
||||
async (input: TaskCreateInput): Promise<Task> => {
|
||||
const task = await createTask({ ...input, column: "triage" });
|
||||
return task;
|
||||
},
|
||||
[createTask],
|
||||
);
|
||||
|
||||
// Planning mode handlers
|
||||
const handlePlanningOpen = openPlanning;
|
||||
const handleResumePlanning = resumePlanning;
|
||||
const handlePlanningClose = closePlanning;
|
||||
const handlePlanningTaskCreated = useCallback((task: Task) => {
|
||||
onPlanningTaskCreated(task, addToast);
|
||||
}, [onPlanningTaskCreated, addToast]);
|
||||
|
||||
const handlePlanningTasksCreated = useCallback((createdTasks: Task[]) => {
|
||||
onPlanningTasksCreated(createdTasks, addToast);
|
||||
}, [onPlanningTasksCreated, addToast]);
|
||||
|
||||
// Handle planning mode from new task dialog
|
||||
const handleNewTaskPlanningMode = openPlanningWithInitialPlan;
|
||||
|
||||
// Handle subtask breakdown from inline/quick create
|
||||
const handleSubtaskBreakdown = openSubtaskBreakdown;
|
||||
const handleSubtaskClose = closeSubtask;
|
||||
|
||||
const handleSubtaskTasksCreated = useCallback((createdTasks: Task[]) => {
|
||||
onSubtaskTasksCreated(createdTasks, addToast);
|
||||
}, [onSubtaskTasksCreated, addToast]);
|
||||
|
||||
// Usage indicator handlers
|
||||
const handleOpenUsage = openUsage;
|
||||
const handleCloseUsage = closeUsage;
|
||||
|
||||
// Schedules modal handlers
|
||||
const handleOpenSchedules = openSchedules;
|
||||
const handleCloseSchedules = closeSchedules;
|
||||
|
||||
const handleToggleAutoMerge = toggleAutoMerge;
|
||||
const handleToggleGlobalPause = toggleGlobalPause;
|
||||
const handleToggleEnginePause = toggleEnginePause;
|
||||
|
||||
const handleDetailOpen = openDetailTask;
|
||||
const {
|
||||
handleBoardQuickCreate,
|
||||
handleModalCreate,
|
||||
handlePlanningTaskCreated,
|
||||
handlePlanningTasksCreated,
|
||||
handleSubtaskTasksCreated,
|
||||
handleGitHubImport,
|
||||
} = useTaskHandlers({
|
||||
createTask,
|
||||
onPlanningTaskCreated: modalManager.onPlanningTaskCreated,
|
||||
onPlanningTasksCreated: modalManager.onPlanningTasksCreated,
|
||||
onSubtaskTasksCreated: modalManager.onSubtaskTasksCreated,
|
||||
addToast,
|
||||
});
|
||||
|
||||
const handleOpenDetailWithTab = useCallback((task: TaskDetail, initialTab: "changes") => {
|
||||
if (initialTab === "changes") {
|
||||
openDetailWithChangesTab(task);
|
||||
modalManager.openDetailWithChangesTab(task);
|
||||
return;
|
||||
}
|
||||
openDetailTask(task, initialTab);
|
||||
}, [openDetailTask, openDetailWithChangesTab]);
|
||||
modalManager.openDetailTask(task, initialTab);
|
||||
}, [modalManager]);
|
||||
|
||||
|
||||
const handleGitHubImport = useCallback((task: Task) => {
|
||||
addToast(`Imported ${task.id} from GitHub`, "success");
|
||||
}, [addToast]);
|
||||
|
||||
const handleToggleTerminal = toggleTerminal;
|
||||
const handleOpenFiles = openFiles;
|
||||
const handleWorkspaceChange = setFileWorkspace;
|
||||
|
||||
// Activity log handlers
|
||||
const handleOpenActivityLog = openActivityLog;
|
||||
const handleCloseActivityLog = closeActivityLog;
|
||||
|
||||
const handleOpenMailbox = openMailbox;
|
||||
const handleCloseMailbox = closeMailbox;
|
||||
|
||||
// Mission link handler from TaskCard
|
||||
const handleOpenMission = openMissionById;
|
||||
|
||||
// Git Manager handlers
|
||||
const handleOpenGitManager = openGitManager;
|
||||
const handleCloseGitManager = closeGitManager;
|
||||
|
||||
// Agent handlers
|
||||
const handleCloseAgents = closeAgents;
|
||||
|
||||
// Node management view handlers
|
||||
const handleOpenNodes = useCallback(() => {
|
||||
setNodesOpen((prev) => !prev);
|
||||
}, []);
|
||||
const handleCloseNodes = useCallback(() => {
|
||||
setNodesOpen(false);
|
||||
}, []);
|
||||
|
||||
// Scripts handlers
|
||||
const handleOpenScripts = openScripts;
|
||||
const handleCloseScripts = closeScripts;
|
||||
const handleRunScript = runScript;
|
||||
|
||||
// Terminal close handler
|
||||
const handleTerminalClose = closeTerminal;
|
||||
|
||||
// Render main content based on view mode
|
||||
const renderMainContent = () => {
|
||||
@@ -474,7 +170,7 @@ function AppInner() {
|
||||
return (
|
||||
<div className="nodes-management-overlay">
|
||||
<div className="nodes-management-overlay__header">
|
||||
<button className="btn btn-sm" onClick={handleCloseNodes}>Close Nodes</button>
|
||||
<button className="btn btn-sm" onClick={() => setNodesOpen(false)}>Close Nodes</button>
|
||||
</div>
|
||||
<NodesView addToast={addToast} />
|
||||
</div>
|
||||
@@ -508,14 +204,14 @@ function AppInner() {
|
||||
projectId={currentProject?.id}
|
||||
maxConcurrent={maxConcurrent}
|
||||
onMoveTask={moveTask}
|
||||
onOpenDetail={handleDetailOpen}
|
||||
onOpenDetail={modalManager.openDetailTask}
|
||||
addToast={addToast}
|
||||
onQuickCreate={handleBoardQuickCreate}
|
||||
onNewTask={handleNewTaskOpen}
|
||||
onPlanningMode={handleNewTaskPlanningMode}
|
||||
onSubtaskBreakdown={handleSubtaskBreakdown}
|
||||
onNewTask={modalManager.openNewTask}
|
||||
onPlanningMode={modalManager.openPlanningWithInitialPlan}
|
||||
onSubtaskBreakdown={modalManager.openSubtaskBreakdown}
|
||||
autoMerge={autoMerge}
|
||||
onToggleAutoMerge={handleToggleAutoMerge}
|
||||
onToggleAutoMerge={toggleAutoMerge}
|
||||
globalPaused={globalPaused}
|
||||
onUpdateTask={updateTask}
|
||||
onArchiveTask={archiveTask}
|
||||
@@ -529,7 +225,7 @@ function AppInner() {
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
taskStuckTimeoutMs={taskStuckTimeoutMs}
|
||||
onOpenMission={handleOpenMission}
|
||||
onOpenMission={modalManager.openMissionById}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -540,13 +236,13 @@ function AppInner() {
|
||||
tasks={tasks}
|
||||
projectId={currentProject?.id}
|
||||
onMoveTask={moveTask}
|
||||
onOpenDetail={handleDetailOpen}
|
||||
onOpenDetail={modalManager.openDetailTask}
|
||||
addToast={addToast}
|
||||
globalPaused={globalPaused}
|
||||
onNewTask={handleNewTaskOpen}
|
||||
onNewTask={modalManager.openNewTask}
|
||||
onQuickCreate={handleBoardQuickCreate}
|
||||
onPlanningMode={handleNewTaskPlanningMode}
|
||||
onSubtaskBreakdown={handleSubtaskBreakdown}
|
||||
onPlanningMode={modalManager.openPlanningWithInitialPlan}
|
||||
onSubtaskBreakdown={modalManager.openSubtaskBreakdown}
|
||||
availableModels={availableModels}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteModels={favoriteModels}
|
||||
@@ -562,28 +258,28 @@ function AppInner() {
|
||||
<Header
|
||||
isElectron={isElectron}
|
||||
onOpenSettings={handleOpenSettings}
|
||||
onOpenGitHubImport={openGitHubImport}
|
||||
onOpenPlanning={handlePlanningOpen}
|
||||
onResumePlanning={handleResumePlanning}
|
||||
onOpenGitHubImport={modalManager.openGitHubImport}
|
||||
onOpenPlanning={modalManager.openPlanning}
|
||||
onResumePlanning={modalManager.resumePlanning}
|
||||
activePlanningSessionCount={bgPlanningSessions.length}
|
||||
onOpenUsage={handleOpenUsage}
|
||||
onOpenActivityLog={handleOpenActivityLog}
|
||||
onOpenMailbox={handleOpenMailbox}
|
||||
mailboxUnreadCount={mailboxUnreadCount}
|
||||
onOpenSchedules={handleOpenSchedules}
|
||||
onOpenGitManager={handleOpenGitManager}
|
||||
onOpenUsage={modalManager.openUsage}
|
||||
onOpenActivityLog={modalManager.openActivityLog}
|
||||
onOpenMailbox={modalManager.openMailbox}
|
||||
mailboxUnreadCount={modalManager.mailboxUnreadCount}
|
||||
onOpenSchedules={modalManager.openSchedules}
|
||||
onOpenGitManager={modalManager.openGitManager}
|
||||
onOpenNodes={handleOpenNodes}
|
||||
onOpenWorkflowSteps={openWorkflowSteps}
|
||||
onOpenMissions={viewMode === "project" && currentProject ? openMissions : undefined}
|
||||
onOpenScripts={handleOpenScripts}
|
||||
onRunScript={handleRunScript}
|
||||
onToggleTerminal={handleToggleTerminal}
|
||||
onOpenFiles={handleOpenFiles}
|
||||
filesOpen={filesOpen}
|
||||
onOpenWorkflowSteps={modalManager.openWorkflowSteps}
|
||||
onOpenMissions={viewMode === "project" && currentProject ? modalManager.openMissions : undefined}
|
||||
onOpenScripts={modalManager.openScripts}
|
||||
onRunScript={modalManager.runScript}
|
||||
onToggleTerminal={modalManager.toggleTerminal}
|
||||
onOpenFiles={modalManager.openFiles}
|
||||
filesOpen={modalManager.filesOpen}
|
||||
globalPaused={globalPaused}
|
||||
enginePaused={enginePaused}
|
||||
onToggleGlobalPause={handleToggleGlobalPause}
|
||||
onToggleEnginePause={handleToggleEnginePause}
|
||||
onToggleGlobalPause={toggleGlobalPause}
|
||||
onToggleEnginePause={toggleEnginePause}
|
||||
view={taskView}
|
||||
onChangeView={viewMode === "project" && currentProject ? handleChangeTaskView : undefined}
|
||||
searchQuery={searchQuery}
|
||||
@@ -610,11 +306,11 @@ function AppInner() {
|
||||
backgroundNeedsInput={bgNeedsInput}
|
||||
onOpenBackgroundSession={(session) => {
|
||||
if (session.type === "planning") {
|
||||
openPlanningWithSession(session.id);
|
||||
modalManager.openPlanningWithSession(session.id);
|
||||
} else if (session.type === "subtask") {
|
||||
openSubtaskWithSession(session.id);
|
||||
modalManager.openSubtaskWithSession(session.id);
|
||||
} else if (session.type === "mission_interview") {
|
||||
openMissionWithSession(session.id);
|
||||
modalManager.openMissionWithSession(session.id);
|
||||
}
|
||||
}}
|
||||
onDismissBackgroundSession={bgDismiss}
|
||||
@@ -624,192 +320,50 @@ function AppInner() {
|
||||
view={taskView}
|
||||
onChangeView={viewMode === "project" && currentProject ? handleChangeTaskView : () => {}}
|
||||
footerVisible={viewMode === "project" && !!currentProject}
|
||||
modalOpen={anyModalOpen}
|
||||
modalOpen={modalManager.anyModalOpen}
|
||||
onOpenSettings={handleOpenSettings}
|
||||
onOpenActivityLog={handleOpenActivityLog}
|
||||
onOpenMailbox={handleOpenMailbox}
|
||||
mailboxUnreadCount={mailboxUnreadCount}
|
||||
onOpenGitManager={handleOpenGitManager}
|
||||
onOpenWorkflowSteps={openWorkflowSteps}
|
||||
onOpenMissions={viewMode === "project" && currentProject ? openMissions : undefined}
|
||||
onOpenSchedules={handleOpenSchedules}
|
||||
onOpenScripts={handleOpenScripts}
|
||||
onToggleTerminal={handleToggleTerminal}
|
||||
onOpenFiles={handleOpenFiles}
|
||||
onOpenGitHubImport={openGitHubImport}
|
||||
onOpenPlanning={handlePlanningOpen}
|
||||
onResumePlanning={handleResumePlanning}
|
||||
onOpenActivityLog={modalManager.openActivityLog}
|
||||
onOpenMailbox={modalManager.openMailbox}
|
||||
mailboxUnreadCount={modalManager.mailboxUnreadCount}
|
||||
onOpenGitManager={modalManager.openGitManager}
|
||||
onOpenWorkflowSteps={modalManager.openWorkflowSteps}
|
||||
onOpenMissions={viewMode === "project" && currentProject ? modalManager.openMissions : undefined}
|
||||
onOpenSchedules={modalManager.openSchedules}
|
||||
onOpenScripts={modalManager.openScripts}
|
||||
onToggleTerminal={modalManager.toggleTerminal}
|
||||
onOpenFiles={modalManager.openFiles}
|
||||
onOpenGitHubImport={modalManager.openGitHubImport}
|
||||
onOpenPlanning={modalManager.openPlanning}
|
||||
onResumePlanning={modalManager.resumePlanning}
|
||||
activePlanningSessionCount={bgPlanningSessions.length}
|
||||
onOpenUsage={handleOpenUsage}
|
||||
onRunScript={handleRunScript}
|
||||
onOpenUsage={modalManager.openUsage}
|
||||
onRunScript={modalManager.runScript}
|
||||
projectId={currentProject?.id}
|
||||
/>
|
||||
{viewMode === "project" && currentProject && (
|
||||
<QuickChatFAB projectId={currentProject.id} addToast={addToast} />
|
||||
)}
|
||||
{detailTask && (
|
||||
<TaskDetailModal
|
||||
task={detailTask}
|
||||
projectId={currentProject?.id}
|
||||
tasks={tasks}
|
||||
onClose={handleDetailClose}
|
||||
onOpenDetail={handleDetailOpen}
|
||||
onMoveTask={moveTask}
|
||||
onDeleteTask={deleteTask}
|
||||
onMergeTask={mergeTask}
|
||||
onRetryTask={retryTask}
|
||||
onDuplicateTask={duplicateTask}
|
||||
onTaskUpdated={updateDetailTask}
|
||||
addToast={addToast}
|
||||
githubTokenConfigured={githubTokenConfigured}
|
||||
initialTab={detailTaskInitialTab}
|
||||
/>
|
||||
)}
|
||||
{settingsOpen && (
|
||||
<SettingsModal
|
||||
onClose={closeSettings}
|
||||
addToast={addToast}
|
||||
initialSection={settingsInitialSection}
|
||||
projectId={currentProject?.id}
|
||||
themeMode={themeMode}
|
||||
colorTheme={colorTheme}
|
||||
onThemeModeChange={setThemeMode}
|
||||
onColorThemeChange={setColorTheme}
|
||||
/>
|
||||
)}
|
||||
<GitHubImportModal
|
||||
isOpen={githubImportOpen}
|
||||
onClose={closeGitHubImport}
|
||||
onImport={handleGitHubImport}
|
||||
<AppModals
|
||||
projectId={currentProject?.id}
|
||||
tasks={tasks}
|
||||
/>
|
||||
<PlanningModeModal
|
||||
isOpen={isPlanningOpen}
|
||||
onClose={handlePlanningClose}
|
||||
onTaskCreated={handlePlanningTaskCreated}
|
||||
onTasksCreated={handlePlanningTasksCreated}
|
||||
tasks={tasks}
|
||||
initialPlan={planningInitialPlan ?? undefined}
|
||||
projectId={currentProject?.id}
|
||||
resumeSessionId={planningResumeSessionId}
|
||||
/>
|
||||
<SubtaskBreakdownModal
|
||||
isOpen={isSubtaskOpen}
|
||||
onClose={handleSubtaskClose}
|
||||
initialDescription={subtaskInitialDescription ?? ""}
|
||||
onTasksCreated={handleSubtaskTasksCreated}
|
||||
projectId={currentProject?.id}
|
||||
resumeSessionId={subtaskResumeSessionId}
|
||||
/>
|
||||
<TerminalModal
|
||||
isOpen={terminalOpen}
|
||||
onClose={handleTerminalClose}
|
||||
initialCommand={terminalInitialCommand}
|
||||
projectId={currentProject?.id}
|
||||
/>
|
||||
<ScriptsModal
|
||||
isOpen={scriptsOpen}
|
||||
onClose={handleCloseScripts}
|
||||
addToast={addToast}
|
||||
onRunScript={handleRunScript}
|
||||
projectId={currentProject?.id}
|
||||
/>
|
||||
{filesOpen && (
|
||||
<FileBrowserModal
|
||||
initialWorkspace={fileBrowserWorkspace}
|
||||
isOpen={true}
|
||||
onClose={closeFiles}
|
||||
onWorkspaceChange={handleWorkspaceChange}
|
||||
/>
|
||||
)}
|
||||
<UsageIndicator
|
||||
isOpen={usageOpen}
|
||||
onClose={handleCloseUsage}
|
||||
projectId={currentProject?.id}
|
||||
/>
|
||||
{schedulesOpen && (
|
||||
<ScheduledTasksModal
|
||||
onClose={handleCloseSchedules}
|
||||
addToast={addToast}
|
||||
/>
|
||||
)}
|
||||
<NewTaskModal
|
||||
isOpen={newTaskModalOpen}
|
||||
onClose={handleNewTaskClose}
|
||||
tasks={tasks}
|
||||
onCreateTask={handleModalCreate}
|
||||
addToast={addToast}
|
||||
projectId={currentProject?.id}
|
||||
onPlanningMode={handleNewTaskPlanningMode}
|
||||
onSubtaskBreakdown={handleSubtaskBreakdown}
|
||||
/>
|
||||
<ActivityLogModal
|
||||
isOpen={activityLogOpen}
|
||||
onClose={handleCloseActivityLog}
|
||||
tasks={tasks}
|
||||
projectId={currentProject?.id}
|
||||
projects={projects}
|
||||
currentProject={currentProject}
|
||||
onOpenTaskDetail={(taskId) => {
|
||||
const task = tasks.find((t) => t.id === taskId);
|
||||
if (task) {
|
||||
handleDetailOpen(task as TaskDetail);
|
||||
}
|
||||
addToast={addToast}
|
||||
toasts={toasts}
|
||||
removeToast={removeToast}
|
||||
modalManager={modalManager}
|
||||
projectActions={{ handleSetupComplete, handleModelOnboardingComplete }}
|
||||
taskHandlers={{
|
||||
handleModalCreate,
|
||||
handlePlanningTaskCreated,
|
||||
handlePlanningTasksCreated,
|
||||
handleSubtaskTasksCreated,
|
||||
handleGitHubImport,
|
||||
}}
|
||||
taskOperations={{ moveTask, deleteTask, mergeTask, retryTask, duplicateTask }}
|
||||
deepLink={{ handleDetailClose }}
|
||||
settings={{ githubTokenConfigured, themeMode, colorTheme, setThemeMode, setColorTheme }}
|
||||
/>
|
||||
<GitManagerModal
|
||||
isOpen={gitManagerOpen}
|
||||
onClose={handleCloseGitManager}
|
||||
tasks={tasks}
|
||||
addToast={addToast}
|
||||
/>
|
||||
<WorkflowStepManager
|
||||
isOpen={workflowStepsOpen}
|
||||
onClose={closeWorkflowSteps}
|
||||
addToast={addToast}
|
||||
projectId={currentProject?.id}
|
||||
/>
|
||||
<MissionManager
|
||||
isOpen={missionsOpen}
|
||||
onClose={closeMissions}
|
||||
addToast={addToast}
|
||||
projectId={currentProject?.id}
|
||||
resumeSessionId={missionResumeSessionId}
|
||||
targetMissionId={missionTargetId}
|
||||
availableTasks={tasks.map((t) => ({ id: t.id, title: t.title }))}
|
||||
onSelectTask={(taskId) => {
|
||||
const task = tasks.find((t) => t.id === taskId);
|
||||
if (task) {
|
||||
openDetailTask(task as TaskDetail);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<AgentListModal
|
||||
isOpen={agentsOpen}
|
||||
onClose={handleCloseAgents}
|
||||
addToast={addToast}
|
||||
projectId={currentProject?.id}
|
||||
/>
|
||||
<MailboxModal
|
||||
isOpen={mailboxOpen}
|
||||
onClose={handleCloseMailbox}
|
||||
projectId={currentProject?.id}
|
||||
addToast={addToast}
|
||||
agents={mailboxAgents}
|
||||
/>
|
||||
{setupWizardOpen && (
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={handleSetupComplete}
|
||||
onClose={closeSetupWizard}
|
||||
/>
|
||||
)}
|
||||
{modelOnboardingOpen && (
|
||||
<ModelOnboardingModal
|
||||
onComplete={handleModelOnboardingComplete}
|
||||
addToast={addToast}
|
||||
/>
|
||||
)}
|
||||
<ToastContainer toasts={toasts} onRemove={removeToast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
259
packages/dashboard/app/components/AppModals.tsx
Normal file
259
packages/dashboard/app/components/AppModals.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import type { ProjectInfo } from "../api";
|
||||
import type { ColorTheme, Column, MergeResult, Task, TaskCreateInput, TaskDetail, ThemeMode } from "@fusion/core";
|
||||
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 { TaskDetailModal } from "./TaskDetailModal";
|
||||
import { SettingsModal } from "./SettingsModal";
|
||||
import { GitHubImportModal } from "./GitHubImportModal";
|
||||
import { PlanningModeModal } from "./PlanningModeModal";
|
||||
import { SubtaskBreakdownModal } from "./SubtaskBreakdownModal";
|
||||
import { TerminalModal } from "./TerminalModal";
|
||||
import { ScriptsModal } from "./ScriptsModal";
|
||||
import { FileBrowserModal } from "./FileBrowserModal";
|
||||
import { UsageIndicator } from "./UsageIndicator";
|
||||
import { ScheduledTasksModal } from "./ScheduledTasksModal";
|
||||
import { NewTaskModal } from "./NewTaskModal";
|
||||
import { ActivityLogModal } from "./ActivityLogModal";
|
||||
import { GitManagerModal } from "./GitManagerModal";
|
||||
import { WorkflowStepManager } from "./WorkflowStepManager";
|
||||
import { MissionManager } from "./MissionManager";
|
||||
import { AgentListModal } from "./AgentListModal";
|
||||
import { MailboxModal } from "./MailboxModal";
|
||||
import { SetupWizardModal } from "./SetupWizardModal";
|
||||
import { ModelOnboardingModal } from "./ModelOnboardingModal";
|
||||
import { ToastContainer } from "./ToastContainer";
|
||||
|
||||
interface AppModalsProps {
|
||||
projectId?: string;
|
||||
tasks: Task[];
|
||||
projects: ProjectInfo[];
|
||||
currentProject: ProjectInfo | null;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
toasts: Toast[];
|
||||
removeToast: (id: number) => void;
|
||||
modalManager: ModalManager;
|
||||
projectActions: Pick<UseProjectActionsResult, "handleSetupComplete" | "handleModelOnboardingComplete">;
|
||||
taskHandlers: Pick<UseTaskHandlersResult, "handleModalCreate" | "handlePlanningTaskCreated" | "handlePlanningTasksCreated" | "handleSubtaskTasksCreated" | "handleGitHubImport">;
|
||||
taskOperations: {
|
||||
moveTask: (taskId: string, column: Column, position?: number) => Promise<Task>;
|
||||
deleteTask: (taskId: string) => Promise<Task>;
|
||||
mergeTask: (taskId: string) => Promise<MergeResult>;
|
||||
retryTask: (taskId: string) => Promise<Task>;
|
||||
duplicateTask: (taskId: string) => Promise<Task>;
|
||||
};
|
||||
deepLink: {
|
||||
handleDetailClose: () => void;
|
||||
};
|
||||
settings: {
|
||||
githubTokenConfigured: boolean;
|
||||
themeMode: ThemeMode;
|
||||
colorTheme: ColorTheme;
|
||||
setThemeMode: (mode: ThemeMode) => void;
|
||||
setColorTheme: (theme: ColorTheme) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function AppModals({
|
||||
projectId,
|
||||
tasks,
|
||||
projects,
|
||||
currentProject,
|
||||
addToast,
|
||||
toasts,
|
||||
removeToast,
|
||||
modalManager,
|
||||
projectActions,
|
||||
taskHandlers,
|
||||
taskOperations,
|
||||
deepLink,
|
||||
settings,
|
||||
}: AppModalsProps) {
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
|
||||
<GitHubImportModal
|
||||
isOpen={modalManager.githubImportOpen}
|
||||
onClose={modalManager.closeGitHubImport}
|
||||
onImport={taskHandlers.handleGitHubImport}
|
||||
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}
|
||||
/>
|
||||
|
||||
<SubtaskBreakdownModal
|
||||
isOpen={modalManager.isSubtaskOpen}
|
||||
onClose={modalManager.closeSubtask}
|
||||
initialDescription={modalManager.subtaskInitialDescription ?? ""}
|
||||
onTasksCreated={taskHandlers.handleSubtaskTasksCreated}
|
||||
projectId={projectId}
|
||||
resumeSessionId={modalManager.subtaskResumeSessionId}
|
||||
/>
|
||||
|
||||
<TerminalModal
|
||||
isOpen={modalManager.terminalOpen}
|
||||
onClose={modalManager.closeTerminal}
|
||||
initialCommand={modalManager.terminalInitialCommand}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<ScriptsModal
|
||||
isOpen={modalManager.scriptsOpen}
|
||||
onClose={modalManager.closeScripts}
|
||||
addToast={addToast}
|
||||
onRunScript={modalManager.runScript}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
{modalManager.filesOpen && (
|
||||
<FileBrowserModal
|
||||
initialWorkspace={modalManager.fileBrowserWorkspace}
|
||||
isOpen={true}
|
||||
onClose={modalManager.closeFiles}
|
||||
onWorkspaceChange={modalManager.setFileWorkspace}
|
||||
/>
|
||||
)}
|
||||
|
||||
<UsageIndicator
|
||||
isOpen={modalManager.usageOpen}
|
||||
onClose={modalManager.closeUsage}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
{modalManager.schedulesOpen && (
|
||||
<ScheduledTasksModal
|
||||
onClose={modalManager.closeSchedules}
|
||||
addToast={addToast}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NewTaskModal
|
||||
isOpen={modalManager.newTaskModalOpen}
|
||||
onClose={modalManager.closeNewTask}
|
||||
tasks={tasks}
|
||||
onCreateTask={taskHandlers.handleModalCreate}
|
||||
addToast={addToast}
|
||||
projectId={projectId}
|
||||
onPlanningMode={modalManager.openPlanningWithInitialPlan}
|
||||
onSubtaskBreakdown={modalManager.openSubtaskBreakdown}
|
||||
/>
|
||||
|
||||
<ActivityLogModal
|
||||
isOpen={modalManager.activityLogOpen}
|
||||
onClose={modalManager.closeActivityLog}
|
||||
tasks={tasks}
|
||||
projectId={projectId}
|
||||
projects={projects}
|
||||
currentProject={currentProject}
|
||||
onOpenTaskDetail={(taskId) => {
|
||||
const task = tasks.find((candidate) => candidate.id === taskId);
|
||||
if (task) {
|
||||
modalManager.openDetailTask(task as TaskDetail);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<GitManagerModal
|
||||
isOpen={modalManager.gitManagerOpen}
|
||||
onClose={modalManager.closeGitManager}
|
||||
tasks={tasks}
|
||||
addToast={addToast}
|
||||
/>
|
||||
|
||||
<WorkflowStepManager
|
||||
isOpen={modalManager.workflowStepsOpen}
|
||||
onClose={modalManager.closeWorkflowSteps}
|
||||
addToast={addToast}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<MissionManager
|
||||
isOpen={modalManager.missionsOpen}
|
||||
onClose={modalManager.closeMissions}
|
||||
addToast={addToast}
|
||||
projectId={projectId}
|
||||
resumeSessionId={modalManager.missionResumeSessionId}
|
||||
targetMissionId={modalManager.missionTargetId}
|
||||
availableTasks={tasks.map((task) => ({ id: task.id, title: task.title }))}
|
||||
onSelectTask={(taskId) => {
|
||||
const task = tasks.find((candidate) => candidate.id === taskId);
|
||||
if (task) {
|
||||
modalManager.openDetailTask(task as TaskDetail);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<AgentListModal
|
||||
isOpen={modalManager.agentsOpen}
|
||||
onClose={modalManager.closeAgents}
|
||||
addToast={addToast}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<MailboxModal
|
||||
isOpen={modalManager.mailboxOpen}
|
||||
onClose={modalManager.closeMailbox}
|
||||
projectId={projectId}
|
||||
addToast={addToast}
|
||||
agents={modalManager.mailboxAgents}
|
||||
/>
|
||||
|
||||
{modalManager.setupWizardOpen && (
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={projectActions.handleSetupComplete}
|
||||
onClose={modalManager.closeSetupWizard}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalManager.modelOnboardingOpen && (
|
||||
<ModelOnboardingModal
|
||||
onComplete={projectActions.handleModelOnboardingComplete}
|
||||
addToast={addToast}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ToastContainer toasts={toasts} onRemove={removeToast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
169
packages/dashboard/app/hooks/__tests__/useProjectActions.test.ts
Normal file
169
packages/dashboard/app/hooks/__tests__/useProjectActions.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useProjectActions } from "../useProjectActions";
|
||||
import * as api from "../../api";
|
||||
import type { ProjectInfo } from "../../api";
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
updateProject: vi.fn(),
|
||||
unregisterProject: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUpdateProject = vi.mocked(api.updateProject);
|
||||
const mockUnregisterProject = vi.mocked(api.unregisterProject);
|
||||
|
||||
const PROJECT: ProjectInfo = {
|
||||
id: "proj_123",
|
||||
name: "Demo",
|
||||
path: "/demo",
|
||||
status: "active",
|
||||
isolationMode: "in-process",
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
};
|
||||
|
||||
function createOptions(overrides: Partial<Parameters<typeof useProjectActions>[0]> = {}): Parameters<typeof useProjectActions>[0] {
|
||||
return {
|
||||
setCurrentProject: vi.fn(),
|
||||
clearCurrentProject: vi.fn(),
|
||||
setViewMode: vi.fn(),
|
||||
currentProject: PROJECT,
|
||||
refreshProjects: vi.fn().mockResolvedValue(undefined),
|
||||
toggleFavoriteProvider: vi.fn().mockResolvedValue(undefined),
|
||||
toggleFavoriteModel: vi.fn().mockResolvedValue(undefined),
|
||||
addToast: vi.fn(),
|
||||
openSettings: vi.fn(),
|
||||
openSetupWizard: vi.fn(),
|
||||
closeSetupWizard: vi.fn(),
|
||||
closeModelOnboarding: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("useProjectActions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUpdateProject.mockResolvedValue(PROJECT);
|
||||
mockUnregisterProject.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("handleSelectProject sets current project and view mode", () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useProjectActions(options));
|
||||
|
||||
act(() => {
|
||||
result.current.handleSelectProject(PROJECT);
|
||||
});
|
||||
|
||||
expect(options.setCurrentProject).toHaveBeenCalledWith(PROJECT);
|
||||
expect(options.setViewMode).toHaveBeenCalledWith("project");
|
||||
});
|
||||
|
||||
it("handleViewAllProjects clears current project and sets overview", () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useProjectActions(options));
|
||||
|
||||
act(() => {
|
||||
result.current.handleViewAllProjects();
|
||||
});
|
||||
|
||||
expect(options.clearCurrentProject).toHaveBeenCalledTimes(1);
|
||||
expect(options.setViewMode).toHaveBeenCalledWith("overview");
|
||||
});
|
||||
|
||||
it("handleSetupComplete closes wizard, sets project, toasts, and refreshes", async () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useProjectActions(options));
|
||||
|
||||
act(() => {
|
||||
result.current.handleSetupComplete(PROJECT);
|
||||
});
|
||||
|
||||
expect(options.closeSetupWizard).toHaveBeenCalledTimes(1);
|
||||
expect(options.setCurrentProject).toHaveBeenCalledWith(PROJECT);
|
||||
expect(options.setViewMode).toHaveBeenCalledWith("project");
|
||||
expect(options.addToast).toHaveBeenCalledWith("Project Demo registered successfully", "success");
|
||||
expect(options.refreshProjects).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handlePauseProject calls updateProject and shows success toast", async () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useProjectActions(options));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handlePauseProject(PROJECT);
|
||||
});
|
||||
|
||||
expect(mockUpdateProject).toHaveBeenCalledWith(PROJECT.id, { status: "paused" });
|
||||
expect(options.addToast).toHaveBeenCalledWith("Project Demo paused", "success");
|
||||
expect(options.refreshProjects).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handleResumeProject calls updateProject and shows success toast", async () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useProjectActions(options));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleResumeProject(PROJECT);
|
||||
});
|
||||
|
||||
expect(mockUpdateProject).toHaveBeenCalledWith(PROJECT.id, { status: "active" });
|
||||
expect(options.addToast).toHaveBeenCalledWith("Project Demo resumed", "success");
|
||||
expect(options.refreshProjects).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handleRemoveProject unregisters, clears current selection, toasts, and refreshes", async () => {
|
||||
const options = createOptions({ currentProject: PROJECT });
|
||||
const { result } = renderHook(() => useProjectActions(options));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRemoveProject(PROJECT);
|
||||
});
|
||||
|
||||
expect(mockUnregisterProject).toHaveBeenCalledWith(PROJECT.id);
|
||||
expect(options.addToast).toHaveBeenCalledWith("Project Demo removed", "success");
|
||||
expect(options.clearCurrentProject).toHaveBeenCalledTimes(1);
|
||||
expect(options.setViewMode).toHaveBeenCalledWith("overview");
|
||||
expect(options.refreshProjects).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handleRemoveProject shows error toast on API failure", async () => {
|
||||
mockUnregisterProject.mockRejectedValueOnce(new Error("boom"));
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useProjectActions(options));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRemoveProject(PROJECT);
|
||||
});
|
||||
|
||||
expect(options.addToast).toHaveBeenCalledWith("Failed to remove project Demo", "error");
|
||||
});
|
||||
|
||||
it("handleToggleFavorite delegates and shows error toast on failure", async () => {
|
||||
const options = createOptions({
|
||||
toggleFavoriteProvider: vi.fn().mockRejectedValue(new Error("boom")),
|
||||
});
|
||||
const { result } = renderHook(() => useProjectActions(options));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleToggleFavorite("anthropic");
|
||||
});
|
||||
|
||||
expect(options.toggleFavoriteProvider).toHaveBeenCalledWith("anthropic");
|
||||
expect(options.addToast).toHaveBeenCalledWith("Failed to update favorites", "error");
|
||||
});
|
||||
|
||||
it("handleToggleModelFavorite delegates and shows error toast on failure", async () => {
|
||||
const options = createOptions({
|
||||
toggleFavoriteModel: vi.fn().mockRejectedValue(new Error("boom")),
|
||||
});
|
||||
const { result } = renderHook(() => useProjectActions(options));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleToggleModelFavorite("claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
expect(options.toggleFavoriteModel).toHaveBeenCalledWith("claude-sonnet-4-5");
|
||||
expect(options.addToast).toHaveBeenCalledWith("Failed to update model favorites", "error");
|
||||
});
|
||||
});
|
||||
106
packages/dashboard/app/hooks/__tests__/useTaskHandlers.test.ts
Normal file
106
packages/dashboard/app/hooks/__tests__/useTaskHandlers.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useTaskHandlers } from "../useTaskHandlers";
|
||||
import type { Task, TaskCreateInput } from "@fusion/core";
|
||||
|
||||
const CREATED_TASK: Task = {
|
||||
id: "FN-123",
|
||||
title: "Test",
|
||||
description: "Created task",
|
||||
status: "pending",
|
||||
column: "triage",
|
||||
steps: [],
|
||||
dependencies: [],
|
||||
log: [],
|
||||
attachments: [],
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
size: "M",
|
||||
reviewLevel: 0,
|
||||
};
|
||||
|
||||
function createOptions(overrides: Partial<Parameters<typeof useTaskHandlers>[0]> = {}): Parameters<typeof useTaskHandlers>[0] {
|
||||
return {
|
||||
createTask: vi.fn().mockResolvedValue(CREATED_TASK),
|
||||
onPlanningTaskCreated: vi.fn(),
|
||||
onPlanningTasksCreated: vi.fn(),
|
||||
onSubtaskTasksCreated: vi.fn(),
|
||||
addToast: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("useTaskHandlers", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("handleBoardQuickCreate calls createTask with triage column", async () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useTaskHandlers(options));
|
||||
const input: TaskCreateInput = { description: "Do work" };
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleBoardQuickCreate(input);
|
||||
});
|
||||
|
||||
expect(options.createTask).toHaveBeenCalledWith({ description: "Do work", column: "triage" });
|
||||
});
|
||||
|
||||
it("handleModalCreate calls createTask with triage column and returns task", async () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useTaskHandlers(options));
|
||||
|
||||
let created: Task | null = null;
|
||||
await act(async () => {
|
||||
created = await result.current.handleModalCreate({ description: "From modal" });
|
||||
});
|
||||
|
||||
expect(options.createTask).toHaveBeenCalledWith({ description: "From modal", column: "triage" });
|
||||
expect(created).toEqual(CREATED_TASK);
|
||||
});
|
||||
|
||||
it("handlePlanningTaskCreated delegates with addToast", () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useTaskHandlers(options));
|
||||
|
||||
act(() => {
|
||||
result.current.handlePlanningTaskCreated(CREATED_TASK);
|
||||
});
|
||||
|
||||
expect(options.onPlanningTaskCreated).toHaveBeenCalledWith(CREATED_TASK, options.addToast);
|
||||
});
|
||||
|
||||
it("handlePlanningTasksCreated delegates with addToast", () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useTaskHandlers(options));
|
||||
|
||||
act(() => {
|
||||
result.current.handlePlanningTasksCreated([CREATED_TASK]);
|
||||
});
|
||||
|
||||
expect(options.onPlanningTasksCreated).toHaveBeenCalledWith([CREATED_TASK], options.addToast);
|
||||
});
|
||||
|
||||
it("handleSubtaskTasksCreated delegates with addToast", () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useTaskHandlers(options));
|
||||
|
||||
act(() => {
|
||||
result.current.handleSubtaskTasksCreated([CREATED_TASK]);
|
||||
});
|
||||
|
||||
expect(options.onSubtaskTasksCreated).toHaveBeenCalledWith([CREATED_TASK], options.addToast);
|
||||
});
|
||||
|
||||
it("handleGitHubImport shows success toast with task ID", () => {
|
||||
const options = createOptions();
|
||||
const { result } = renderHook(() => useTaskHandlers(options));
|
||||
|
||||
act(() => {
|
||||
result.current.handleGitHubImport(CREATED_TASK);
|
||||
});
|
||||
|
||||
expect(options.addToast).toHaveBeenCalledWith("Imported FN-123 from GitHub", "success");
|
||||
});
|
||||
});
|
||||
167
packages/dashboard/app/hooks/__tests__/useViewState.test.ts
Normal file
167
packages/dashboard/app/hooks/__tests__/useViewState.test.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useViewState } from "../useViewState";
|
||||
import type { ProjectInfo } from "../../api";
|
||||
import type { ThemeMode } from "@fusion/core";
|
||||
|
||||
const PROJECT: ProjectInfo = {
|
||||
id: "proj_123",
|
||||
name: "Demo Project",
|
||||
path: "/demo",
|
||||
status: "active",
|
||||
isolationMode: "in-process",
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
};
|
||||
|
||||
function createOptions(overrides: Partial<Parameters<typeof useViewState>[0]> = {}): Parameters<typeof useViewState>[0] {
|
||||
return {
|
||||
projectsLoading: false,
|
||||
currentProjectLoading: false,
|
||||
currentProject: null,
|
||||
projectsLength: 1,
|
||||
setupWizardOpen: false,
|
||||
openSetupWizard: vi.fn(),
|
||||
themeMode: "dark",
|
||||
setThemeMode: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("useViewState", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("returns default viewMode and taskView when no localStorage exists", async () => {
|
||||
const { result } = renderHook(() => useViewState(createOptions()));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.viewMode).toBe("overview");
|
||||
expect(result.current.taskView).toBe("board");
|
||||
});
|
||||
});
|
||||
|
||||
it("reads saved viewMode from localStorage on init", async () => {
|
||||
localStorage.setItem("kb-dashboard-view-mode", "project");
|
||||
|
||||
const { result } = renderHook(() => useViewState(createOptions()));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.viewMode).toBe("project");
|
||||
});
|
||||
});
|
||||
|
||||
it("reads saved taskView from localStorage on init", async () => {
|
||||
localStorage.setItem("kb-dashboard-task-view", "list");
|
||||
|
||||
const { result } = renderHook(() => useViewState(createOptions()));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.taskView).toBe("list");
|
||||
});
|
||||
});
|
||||
|
||||
it("persists viewMode changes to localStorage", async () => {
|
||||
const { result } = renderHook(() => useViewState(createOptions()));
|
||||
|
||||
await act(async () => {
|
||||
result.current.setViewMode("project");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem("kb-dashboard-view-mode")).toBe("project");
|
||||
});
|
||||
|
||||
it("persists taskView changes to localStorage", async () => {
|
||||
const { result } = renderHook(() => useViewState(createOptions()));
|
||||
|
||||
await act(async () => {
|
||||
result.current.setTaskView("list");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem("kb-dashboard-task-view")).toBe("list");
|
||||
});
|
||||
|
||||
it("handleChangeTaskView updates taskView state", async () => {
|
||||
const { result } = renderHook(() => useViewState(createOptions()));
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleChangeTaskView("agents");
|
||||
});
|
||||
|
||||
expect(result.current.taskView).toBe("agents");
|
||||
});
|
||||
|
||||
it("handleToggleTheme cycles dark → light → system → dark", async () => {
|
||||
let themeMode: ThemeMode = "dark";
|
||||
const setThemeMode = vi.fn((mode: ThemeMode) => {
|
||||
themeMode = mode;
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useViewState(
|
||||
createOptions({
|
||||
themeMode,
|
||||
setThemeMode,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleToggleTheme();
|
||||
});
|
||||
expect(setThemeMode).toHaveBeenLastCalledWith("light");
|
||||
|
||||
rerender();
|
||||
await act(async () => {
|
||||
result.current.handleToggleTheme();
|
||||
});
|
||||
expect(setThemeMode).toHaveBeenLastCalledWith("system");
|
||||
|
||||
rerender();
|
||||
await act(async () => {
|
||||
result.current.handleToggleTheme();
|
||||
});
|
||||
expect(setThemeMode).toHaveBeenLastCalledWith("dark");
|
||||
});
|
||||
|
||||
it("syncs viewMode to project when currentProject is restored after loading", async () => {
|
||||
localStorage.setItem("kb-dashboard-view-mode", "overview");
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useViewState(
|
||||
createOptions({
|
||||
currentProject: PROJECT,
|
||||
projectsLength: 1,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.viewMode).toBe("project");
|
||||
});
|
||||
});
|
||||
|
||||
it("calls openSetupWizard when no projects and no current project after loading", async () => {
|
||||
vi.useFakeTimers();
|
||||
const openSetupWizard = vi.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useViewState(
|
||||
createOptions({
|
||||
projectsLength: 0,
|
||||
currentProject: null,
|
||||
openSetupWizard,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
expect(openSetupWizard).toHaveBeenCalledTimes(1);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
147
packages/dashboard/app/hooks/useProjectActions.ts
Normal file
147
packages/dashboard/app/hooks/useProjectActions.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useCallback } from "react";
|
||||
import { updateProject, unregisterProject } from "../api";
|
||||
import type { ProjectInfo } from "../api";
|
||||
import type { ViewMode } from "./useViewState";
|
||||
import type { ToastType } from "./useToast";
|
||||
|
||||
interface UseProjectActionsOptions {
|
||||
setCurrentProject: (project: ProjectInfo) => void;
|
||||
clearCurrentProject: () => void;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
currentProject: ProjectInfo | null;
|
||||
refreshProjects: () => Promise<void>;
|
||||
toggleFavoriteProvider: (provider: string) => Promise<void>;
|
||||
toggleFavoriteModel: (modelId: string) => Promise<void>;
|
||||
addToast: (message: string, type: ToastType) => void;
|
||||
openSettings: () => void;
|
||||
openSetupWizard: () => void;
|
||||
closeSetupWizard: () => void;
|
||||
closeModelOnboarding: () => void;
|
||||
}
|
||||
|
||||
export interface UseProjectActionsResult {
|
||||
handleSelectProject: (project: ProjectInfo) => void;
|
||||
handleViewAllProjects: () => void;
|
||||
handleOpenSettings: () => void;
|
||||
handleAddProject: () => void;
|
||||
handleSetupComplete: (project: ProjectInfo) => void;
|
||||
handleModelOnboardingComplete: () => void;
|
||||
handlePauseProject: (project: ProjectInfo) => Promise<void>;
|
||||
handleResumeProject: (project: ProjectInfo) => Promise<void>;
|
||||
handleRemoveProject: (project: ProjectInfo) => Promise<void>;
|
||||
handleToggleFavorite: (provider: string) => Promise<void>;
|
||||
handleToggleModelFavorite: (modelId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useProjectActions(options: UseProjectActionsOptions): UseProjectActionsResult {
|
||||
const {
|
||||
setCurrentProject,
|
||||
clearCurrentProject,
|
||||
setViewMode,
|
||||
currentProject,
|
||||
refreshProjects,
|
||||
toggleFavoriteProvider,
|
||||
toggleFavoriteModel,
|
||||
addToast,
|
||||
openSettings,
|
||||
openSetupWizard,
|
||||
closeSetupWizard,
|
||||
closeModelOnboarding,
|
||||
} = options;
|
||||
|
||||
const handleSelectProject = useCallback((project: ProjectInfo) => {
|
||||
setCurrentProject(project);
|
||||
setViewMode("project");
|
||||
}, [setCurrentProject, setViewMode]);
|
||||
|
||||
const handleViewAllProjects = useCallback(() => {
|
||||
clearCurrentProject();
|
||||
setViewMode("overview");
|
||||
}, [clearCurrentProject, setViewMode]);
|
||||
|
||||
const handleOpenSettings = useCallback(() => {
|
||||
openSettings();
|
||||
}, [openSettings]);
|
||||
|
||||
const handleAddProject = useCallback(() => {
|
||||
openSetupWizard();
|
||||
}, [openSetupWizard]);
|
||||
|
||||
const handleSetupComplete = useCallback((project: ProjectInfo) => {
|
||||
closeSetupWizard();
|
||||
setCurrentProject(project);
|
||||
setViewMode("project");
|
||||
addToast(`Project ${project.name} registered successfully`, "success");
|
||||
void refreshProjects();
|
||||
}, [closeSetupWizard, setCurrentProject, setViewMode, addToast, refreshProjects]);
|
||||
|
||||
const handleModelOnboardingComplete = useCallback(() => {
|
||||
closeModelOnboarding();
|
||||
}, [closeModelOnboarding]);
|
||||
|
||||
const handlePauseProject = useCallback(async (project: ProjectInfo) => {
|
||||
try {
|
||||
await updateProject(project.id, { status: "paused" });
|
||||
addToast(`Project ${project.name} paused`, "success");
|
||||
await refreshProjects();
|
||||
} catch {
|
||||
addToast(`Failed to pause project ${project.name}`, "error");
|
||||
}
|
||||
}, [addToast, refreshProjects]);
|
||||
|
||||
const handleResumeProject = useCallback(async (project: ProjectInfo) => {
|
||||
try {
|
||||
await updateProject(project.id, { status: "active" });
|
||||
addToast(`Project ${project.name} resumed`, "success");
|
||||
await refreshProjects();
|
||||
} catch {
|
||||
addToast(`Failed to resume project ${project.name}`, "error");
|
||||
}
|
||||
}, [addToast, refreshProjects]);
|
||||
|
||||
const handleRemoveProject = useCallback(async (project: ProjectInfo) => {
|
||||
try {
|
||||
await unregisterProject(project.id);
|
||||
addToast(`Project ${project.name} removed`, "success");
|
||||
|
||||
if (currentProject?.id === project.id) {
|
||||
clearCurrentProject();
|
||||
setViewMode("overview");
|
||||
}
|
||||
|
||||
await refreshProjects();
|
||||
} catch {
|
||||
addToast(`Failed to remove project ${project.name}`, "error");
|
||||
}
|
||||
}, [currentProject, clearCurrentProject, setViewMode, addToast, refreshProjects]);
|
||||
|
||||
const handleToggleFavorite = useCallback(async (provider: string) => {
|
||||
try {
|
||||
await toggleFavoriteProvider(provider);
|
||||
} catch {
|
||||
addToast("Failed to update favorites", "error");
|
||||
}
|
||||
}, [toggleFavoriteProvider, addToast]);
|
||||
|
||||
const handleToggleModelFavorite = useCallback(async (modelId: string) => {
|
||||
try {
|
||||
await toggleFavoriteModel(modelId);
|
||||
} catch {
|
||||
addToast("Failed to update model favorites", "error");
|
||||
}
|
||||
}, [toggleFavoriteModel, addToast]);
|
||||
|
||||
return {
|
||||
handleSelectProject,
|
||||
handleViewAllProjects,
|
||||
handleOpenSettings,
|
||||
handleAddProject,
|
||||
handleSetupComplete,
|
||||
handleModelOnboardingComplete,
|
||||
handlePauseProject,
|
||||
handleResumeProject,
|
||||
handleRemoveProject,
|
||||
handleToggleFavorite,
|
||||
handleToggleModelFavorite,
|
||||
};
|
||||
}
|
||||
70
packages/dashboard/app/hooks/useTaskHandlers.ts
Normal file
70
packages/dashboard/app/hooks/useTaskHandlers.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useCallback } from "react";
|
||||
import type { Task, TaskCreateInput } from "@fusion/core";
|
||||
import type { ToastType } from "./useToast";
|
||||
|
||||
interface UseTaskHandlersOptions {
|
||||
createTask: (input: TaskCreateInput) => Promise<Task>;
|
||||
onPlanningTaskCreated: (task: Task, addToast: (msg: string, type?: ToastType) => void) => void;
|
||||
onPlanningTasksCreated: (tasks: Task[], addToast: (msg: string, type?: ToastType) => void) => void;
|
||||
onSubtaskTasksCreated: (tasks: Task[], addToast: (msg: string, type?: ToastType) => void) => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
}
|
||||
|
||||
export interface UseTaskHandlersResult {
|
||||
handleBoardQuickCreate: (input: TaskCreateInput) => Promise<void>;
|
||||
handleModalCreate: (input: TaskCreateInput) => Promise<Task>;
|
||||
handlePlanningTaskCreated: (task: Task) => void;
|
||||
handlePlanningTasksCreated: (tasks: Task[]) => void;
|
||||
handleSubtaskTasksCreated: (tasks: Task[]) => void;
|
||||
handleGitHubImport: (task: Task) => void;
|
||||
}
|
||||
|
||||
export function useTaskHandlers(options: UseTaskHandlersOptions): UseTaskHandlersResult {
|
||||
const {
|
||||
createTask,
|
||||
onPlanningTaskCreated,
|
||||
onPlanningTasksCreated,
|
||||
onSubtaskTasksCreated,
|
||||
addToast,
|
||||
} = options;
|
||||
|
||||
const handleBoardQuickCreate = useCallback(
|
||||
async (input: TaskCreateInput): Promise<void> => {
|
||||
await createTask({ ...input, column: "triage" });
|
||||
},
|
||||
[createTask],
|
||||
);
|
||||
|
||||
const handleModalCreate = useCallback(
|
||||
async (input: TaskCreateInput): Promise<Task> => {
|
||||
const task = await createTask({ ...input, column: "triage" });
|
||||
return task;
|
||||
},
|
||||
[createTask],
|
||||
);
|
||||
|
||||
const handlePlanningTaskCreated = useCallback((task: Task) => {
|
||||
onPlanningTaskCreated(task, addToast);
|
||||
}, [onPlanningTaskCreated, addToast]);
|
||||
|
||||
const handlePlanningTasksCreated = useCallback((tasks: Task[]) => {
|
||||
onPlanningTasksCreated(tasks, addToast);
|
||||
}, [onPlanningTasksCreated, addToast]);
|
||||
|
||||
const handleSubtaskTasksCreated = useCallback((tasks: Task[]) => {
|
||||
onSubtaskTasksCreated(tasks, addToast);
|
||||
}, [onSubtaskTasksCreated, addToast]);
|
||||
|
||||
const handleGitHubImport = useCallback((task: Task) => {
|
||||
addToast(`Imported ${task.id} from GitHub`, "success");
|
||||
}, [addToast]);
|
||||
|
||||
return {
|
||||
handleBoardQuickCreate,
|
||||
handleModalCreate,
|
||||
handlePlanningTaskCreated,
|
||||
handlePlanningTasksCreated,
|
||||
handleSubtaskTasksCreated,
|
||||
handleGitHubImport,
|
||||
};
|
||||
}
|
||||
118
packages/dashboard/app/hooks/useViewState.ts
Normal file
118
packages/dashboard/app/hooks/useViewState.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { ThemeMode } from "@fusion/core";
|
||||
import type { ProjectInfo } from "../api";
|
||||
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||
|
||||
export type ViewMode = "overview" | "project";
|
||||
export type TaskView = "board" | "list" | "agents";
|
||||
|
||||
interface UseViewStateOptions {
|
||||
projectsLoading: boolean;
|
||||
currentProjectLoading: boolean;
|
||||
currentProject: ProjectInfo | null;
|
||||
projectsLength: number;
|
||||
setupWizardOpen: boolean;
|
||||
openSetupWizard: () => void;
|
||||
themeMode: ThemeMode;
|
||||
setThemeMode: (mode: ThemeMode) => void;
|
||||
}
|
||||
|
||||
export interface UseViewStateResult {
|
||||
viewMode: ViewMode;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
taskView: TaskView;
|
||||
setTaskView: (view: TaskView) => void;
|
||||
handleChangeTaskView: (newView: TaskView) => void;
|
||||
handleToggleTheme: () => void;
|
||||
}
|
||||
|
||||
export function useViewState(options: UseViewStateOptions): UseViewStateResult {
|
||||
const {
|
||||
projectsLoading,
|
||||
currentProjectLoading,
|
||||
currentProject,
|
||||
projectsLength,
|
||||
setupWizardOpen,
|
||||
openSetupWizard,
|
||||
themeMode,
|
||||
setThemeMode,
|
||||
} = options;
|
||||
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = window.localStorage.getItem("kb-dashboard-view-mode");
|
||||
if (saved === "overview" || saved === "project") return saved;
|
||||
}
|
||||
return "overview";
|
||||
});
|
||||
|
||||
const [taskView, setTaskView] = useState<TaskView>(() => {
|
||||
const saved = getScopedItem("kb-dashboard-task-view");
|
||||
if (saved === "board" || saved === "list" || saved === "agents") return saved;
|
||||
return "board";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem("kb-dashboard-view-mode", viewMode);
|
||||
}, [viewMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = getScopedItem("kb-dashboard-task-view", currentProject?.id);
|
||||
if (saved === "board" || saved === "list" || saved === "agents") {
|
||||
setTaskView(saved);
|
||||
return;
|
||||
}
|
||||
setTaskView("board");
|
||||
}, [currentProject?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setScopedItem("kb-dashboard-task-view", taskView, currentProject?.id);
|
||||
}, [currentProject?.id, taskView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectsLoading || currentProjectLoading) return;
|
||||
|
||||
if (currentProject && viewMode === "overview") {
|
||||
setViewMode("project");
|
||||
}
|
||||
}, [projectsLoading, currentProjectLoading, currentProject, viewMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectsLoading || currentProjectLoading) return;
|
||||
if (setupWizardOpen) return;
|
||||
if (projectsLength > 0 || currentProject) return;
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
openSetupWizard();
|
||||
}, 500);
|
||||
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [
|
||||
projectsLoading,
|
||||
projectsLength,
|
||||
currentProjectLoading,
|
||||
currentProject,
|
||||
setupWizardOpen,
|
||||
openSetupWizard,
|
||||
]);
|
||||
|
||||
const handleChangeTaskView = useCallback((newView: TaskView) => {
|
||||
setTaskView(newView);
|
||||
}, []);
|
||||
|
||||
const handleToggleTheme = useCallback(() => {
|
||||
const cycle: ThemeMode[] = ["dark", "light", "system"];
|
||||
const currentIndex = cycle.indexOf(themeMode);
|
||||
const nextMode = cycle[(currentIndex + 1) % cycle.length];
|
||||
setThemeMode(nextMode);
|
||||
}, [themeMode, setThemeMode]);
|
||||
|
||||
return {
|
||||
viewMode,
|
||||
setViewMode,
|
||||
taskView,
|
||||
setTaskView,
|
||||
handleChangeTaskView,
|
||||
handleToggleTheme,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user