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 { useState, useCallback } from "react";
|
||||||
import type { TaskDetail, TaskCreateInput, Task, ThemeMode } from "@fusion/core";
|
import type { TaskDetail } from "@fusion/core";
|
||||||
import { updateProject, unregisterProject } from "./api";
|
|
||||||
import type { ProjectInfo } from "./api";
|
|
||||||
import { Header, useViewportMode } from "./components/Header";
|
import { Header, useViewportMode } from "./components/Header";
|
||||||
import { Board } from "./components/Board";
|
import { Board } from "./components/Board";
|
||||||
import { ListView } from "./components/ListView";
|
import { ListView } from "./components/ListView";
|
||||||
import { ProjectOverview } from "./components/ProjectOverview";
|
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 { AgentsView } from "./components/AgentsView";
|
||||||
import { NodesView } from "./components/NodesView";
|
import { NodesView } from "./components/NodesView";
|
||||||
import { MailboxModal } from "./components/MailboxModal";
|
import { AppModals } from "./components/AppModals";
|
||||||
import { ScriptsModal } from "./components/ScriptsModal";
|
|
||||||
import { ExecutorStatusBar } from "./components/ExecutorStatusBar";
|
import { ExecutorStatusBar } from "./components/ExecutorStatusBar";
|
||||||
import { MobileNavBar } from "./components/MobileNavBar";
|
import { MobileNavBar } from "./components/MobileNavBar";
|
||||||
import { QuickChatFAB } from "./components/QuickChatFAB";
|
import { QuickChatFAB } from "./components/QuickChatFAB";
|
||||||
@@ -43,10 +22,9 @@ import { useAppSettings } from "./hooks/useAppSettings";
|
|||||||
import { useDeepLink } from "./hooks/useDeepLink";
|
import { useDeepLink } from "./hooks/useDeepLink";
|
||||||
import { useFavorites } from "./hooks/useFavorites";
|
import { useFavorites } from "./hooks/useFavorites";
|
||||||
import { useAuthOnboarding } from "./hooks/useAuthOnboarding";
|
import { useAuthOnboarding } from "./hooks/useAuthOnboarding";
|
||||||
import { getScopedItem, setScopedItem } from "./utils/projectStorage";
|
import { useViewState } from "./hooks/useViewState";
|
||||||
|
import { useProjectActions } from "./hooks/useProjectActions";
|
||||||
type ViewMode = "overview" | "project";
|
import { useTaskHandlers } from "./hooks/useTaskHandlers";
|
||||||
type TaskView = "board" | "list" | "agents";
|
|
||||||
|
|
||||||
function AppInner() {
|
function AppInner() {
|
||||||
const { toasts, addToast, removeToast } = useToast();
|
const { toasts, addToast, removeToast } = useToast();
|
||||||
@@ -68,109 +46,11 @@ function AppInner() {
|
|||||||
// Background AI sessions
|
// Background AI sessions
|
||||||
const { sessions: bgSessions, generating: bgGenerating, needsInput: bgNeedsInput, planningSessions: bgPlanningSessions, dismissSession: bgDismiss } = useBackgroundSessions(currentProject?.id);
|
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 viewportMode = useViewportMode();
|
||||||
const isMobile = viewportMode === "mobile";
|
const isMobile = viewportMode === "mobile";
|
||||||
|
|
||||||
// Modal state/handlers extracted to a dedicated manager hook.
|
// Modal state/handlers extracted to a dedicated manager hook.
|
||||||
const {
|
const modalManager = useModalManager({
|
||||||
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({
|
|
||||||
projectId: currentProject?.id,
|
projectId: currentProject?.id,
|
||||||
planningSessions: bgPlanningSessions,
|
planningSessions: bgPlanningSessions,
|
||||||
});
|
});
|
||||||
@@ -200,85 +80,50 @@ function AppInner() {
|
|||||||
toggleFavoriteModel,
|
toggleFavoriteModel,
|
||||||
} = useFavorites();
|
} = useFavorites();
|
||||||
|
|
||||||
// Persist view mode
|
const { viewMode, setViewMode, taskView, handleChangeTaskView, handleToggleTheme } = useViewState({
|
||||||
useEffect(() => {
|
projectsLoading,
|
||||||
localStorage.setItem("kb-dashboard-view-mode", viewMode);
|
currentProjectLoading,
|
||||||
}, [viewMode]);
|
currentProject,
|
||||||
|
projectsLength: projects.length,
|
||||||
// Load project-scoped task view when project context changes
|
setupWizardOpen: modalManager.setupWizardOpen,
|
||||||
useEffect(() => {
|
openSetupWizard: modalManager.openSetupWizard,
|
||||||
const saved = getScopedItem("kb-dashboard-task-view", currentProject?.id);
|
themeMode,
|
||||||
if (saved === "board" || saved === "list" || saved === "agents") {
|
setThemeMode,
|
||||||
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]);
|
|
||||||
|
|
||||||
// Auth and onboarding bootstrap logic extracted to a dedicated hook.
|
// Auth and onboarding bootstrap logic extracted to a dedicated hook.
|
||||||
useAuthOnboarding({
|
useAuthOnboarding({
|
||||||
projectId: currentProject?.id,
|
projectId: currentProject?.id,
|
||||||
openModelOnboarding,
|
openModelOnboarding: modalManager.openModelOnboarding,
|
||||||
openSettings,
|
openSettings: modalManager.openSettings,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleToggleFavorite = useCallback(async (provider: string) => {
|
const {
|
||||||
try {
|
handleSelectProject,
|
||||||
await toggleFavoriteProvider(provider);
|
handleViewAllProjects,
|
||||||
} catch {
|
handleOpenSettings,
|
||||||
addToast("Failed to update favorites", "error");
|
handleAddProject,
|
||||||
}
|
handleSetupComplete,
|
||||||
}, [toggleFavoriteProvider, addToast]);
|
handleModelOnboardingComplete,
|
||||||
|
handlePauseProject,
|
||||||
const handleToggleModelFavorite = useCallback(async (modelId: string) => {
|
handleResumeProject,
|
||||||
try {
|
handleRemoveProject,
|
||||||
await toggleFavoriteModel(modelId);
|
handleToggleFavorite,
|
||||||
} catch {
|
handleToggleModelFavorite,
|
||||||
addToast("Failed to update model favorites", "error");
|
} = useProjectActions({
|
||||||
}
|
setCurrentProject,
|
||||||
}, [toggleFavoriteModel, addToast]);
|
clearCurrentProject,
|
||||||
|
setViewMode,
|
||||||
|
currentProject,
|
||||||
|
refreshProjects,
|
||||||
|
toggleFavoriteProvider,
|
||||||
|
toggleFavoriteModel,
|
||||||
|
addToast,
|
||||||
|
openSettings: modalManager.openSettings,
|
||||||
|
openSetupWizard: modalManager.openSetupWizard,
|
||||||
|
closeSetupWizard: modalManager.closeSetupWizard,
|
||||||
|
closeModelOnboarding: modalManager.closeModelOnboarding,
|
||||||
|
});
|
||||||
|
|
||||||
const { handleDetailClose } = useDeepLink({
|
const { handleDetailClose } = useDeepLink({
|
||||||
projectId: currentProject?.id,
|
projectId: currentProject?.id,
|
||||||
@@ -287,186 +132,37 @@ function AppInner() {
|
|||||||
currentProject,
|
currentProject,
|
||||||
setCurrentProject,
|
setCurrentProject,
|
||||||
addToast,
|
addToast,
|
||||||
openTaskDetail: openDetailTask,
|
openTaskDetail: modalManager.openDetailTask,
|
||||||
closeTaskDetail: closeDetailTask,
|
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
|
// Task handlers
|
||||||
const handleNewTaskOpen = openNewTask;
|
const {
|
||||||
const handleNewTaskClose = closeNewTask;
|
handleBoardQuickCreate,
|
||||||
|
handleModalCreate,
|
||||||
const handleBoardQuickCreate = useCallback(
|
handlePlanningTaskCreated,
|
||||||
async (input: TaskCreateInput): Promise<void> => {
|
handlePlanningTasksCreated,
|
||||||
await createTask({ ...input, column: "triage" });
|
handleSubtaskTasksCreated,
|
||||||
},
|
handleGitHubImport,
|
||||||
[createTask],
|
} = useTaskHandlers({
|
||||||
);
|
createTask,
|
||||||
|
onPlanningTaskCreated: modalManager.onPlanningTaskCreated,
|
||||||
const handleModalCreate = useCallback(
|
onPlanningTasksCreated: modalManager.onPlanningTasksCreated,
|
||||||
async (input: TaskCreateInput): Promise<Task> => {
|
onSubtaskTasksCreated: modalManager.onSubtaskTasksCreated,
|
||||||
const task = await createTask({ ...input, column: "triage" });
|
addToast,
|
||||||
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 handleOpenDetailWithTab = useCallback((task: TaskDetail, initialTab: "changes") => {
|
const handleOpenDetailWithTab = useCallback((task: TaskDetail, initialTab: "changes") => {
|
||||||
if (initialTab === "changes") {
|
if (initialTab === "changes") {
|
||||||
openDetailWithChangesTab(task);
|
modalManager.openDetailWithChangesTab(task);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openDetailTask(task, initialTab);
|
modalManager.openDetailTask(task, initialTab);
|
||||||
}, [openDetailTask, openDetailWithChangesTab]);
|
}, [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(() => {
|
const handleOpenNodes = useCallback(() => {
|
||||||
setNodesOpen((prev) => !prev);
|
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
|
// Render main content based on view mode
|
||||||
const renderMainContent = () => {
|
const renderMainContent = () => {
|
||||||
@@ -474,7 +170,7 @@ function AppInner() {
|
|||||||
return (
|
return (
|
||||||
<div className="nodes-management-overlay">
|
<div className="nodes-management-overlay">
|
||||||
<div className="nodes-management-overlay__header">
|
<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>
|
</div>
|
||||||
<NodesView addToast={addToast} />
|
<NodesView addToast={addToast} />
|
||||||
</div>
|
</div>
|
||||||
@@ -508,14 +204,14 @@ function AppInner() {
|
|||||||
projectId={currentProject?.id}
|
projectId={currentProject?.id}
|
||||||
maxConcurrent={maxConcurrent}
|
maxConcurrent={maxConcurrent}
|
||||||
onMoveTask={moveTask}
|
onMoveTask={moveTask}
|
||||||
onOpenDetail={handleDetailOpen}
|
onOpenDetail={modalManager.openDetailTask}
|
||||||
addToast={addToast}
|
addToast={addToast}
|
||||||
onQuickCreate={handleBoardQuickCreate}
|
onQuickCreate={handleBoardQuickCreate}
|
||||||
onNewTask={handleNewTaskOpen}
|
onNewTask={modalManager.openNewTask}
|
||||||
onPlanningMode={handleNewTaskPlanningMode}
|
onPlanningMode={modalManager.openPlanningWithInitialPlan}
|
||||||
onSubtaskBreakdown={handleSubtaskBreakdown}
|
onSubtaskBreakdown={modalManager.openSubtaskBreakdown}
|
||||||
autoMerge={autoMerge}
|
autoMerge={autoMerge}
|
||||||
onToggleAutoMerge={handleToggleAutoMerge}
|
onToggleAutoMerge={toggleAutoMerge}
|
||||||
globalPaused={globalPaused}
|
globalPaused={globalPaused}
|
||||||
onUpdateTask={updateTask}
|
onUpdateTask={updateTask}
|
||||||
onArchiveTask={archiveTask}
|
onArchiveTask={archiveTask}
|
||||||
@@ -529,7 +225,7 @@ function AppInner() {
|
|||||||
onToggleFavorite={handleToggleFavorite}
|
onToggleFavorite={handleToggleFavorite}
|
||||||
onToggleModelFavorite={handleToggleModelFavorite}
|
onToggleModelFavorite={handleToggleModelFavorite}
|
||||||
taskStuckTimeoutMs={taskStuckTimeoutMs}
|
taskStuckTimeoutMs={taskStuckTimeoutMs}
|
||||||
onOpenMission={handleOpenMission}
|
onOpenMission={modalManager.openMissionById}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -540,13 +236,13 @@ function AppInner() {
|
|||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
projectId={currentProject?.id}
|
projectId={currentProject?.id}
|
||||||
onMoveTask={moveTask}
|
onMoveTask={moveTask}
|
||||||
onOpenDetail={handleDetailOpen}
|
onOpenDetail={modalManager.openDetailTask}
|
||||||
addToast={addToast}
|
addToast={addToast}
|
||||||
globalPaused={globalPaused}
|
globalPaused={globalPaused}
|
||||||
onNewTask={handleNewTaskOpen}
|
onNewTask={modalManager.openNewTask}
|
||||||
onQuickCreate={handleBoardQuickCreate}
|
onQuickCreate={handleBoardQuickCreate}
|
||||||
onPlanningMode={handleNewTaskPlanningMode}
|
onPlanningMode={modalManager.openPlanningWithInitialPlan}
|
||||||
onSubtaskBreakdown={handleSubtaskBreakdown}
|
onSubtaskBreakdown={modalManager.openSubtaskBreakdown}
|
||||||
availableModels={availableModels}
|
availableModels={availableModels}
|
||||||
favoriteProviders={favoriteProviders}
|
favoriteProviders={favoriteProviders}
|
||||||
favoriteModels={favoriteModels}
|
favoriteModels={favoriteModels}
|
||||||
@@ -562,28 +258,28 @@ function AppInner() {
|
|||||||
<Header
|
<Header
|
||||||
isElectron={isElectron}
|
isElectron={isElectron}
|
||||||
onOpenSettings={handleOpenSettings}
|
onOpenSettings={handleOpenSettings}
|
||||||
onOpenGitHubImport={openGitHubImport}
|
onOpenGitHubImport={modalManager.openGitHubImport}
|
||||||
onOpenPlanning={handlePlanningOpen}
|
onOpenPlanning={modalManager.openPlanning}
|
||||||
onResumePlanning={handleResumePlanning}
|
onResumePlanning={modalManager.resumePlanning}
|
||||||
activePlanningSessionCount={bgPlanningSessions.length}
|
activePlanningSessionCount={bgPlanningSessions.length}
|
||||||
onOpenUsage={handleOpenUsage}
|
onOpenUsage={modalManager.openUsage}
|
||||||
onOpenActivityLog={handleOpenActivityLog}
|
onOpenActivityLog={modalManager.openActivityLog}
|
||||||
onOpenMailbox={handleOpenMailbox}
|
onOpenMailbox={modalManager.openMailbox}
|
||||||
mailboxUnreadCount={mailboxUnreadCount}
|
mailboxUnreadCount={modalManager.mailboxUnreadCount}
|
||||||
onOpenSchedules={handleOpenSchedules}
|
onOpenSchedules={modalManager.openSchedules}
|
||||||
onOpenGitManager={handleOpenGitManager}
|
onOpenGitManager={modalManager.openGitManager}
|
||||||
onOpenNodes={handleOpenNodes}
|
onOpenNodes={handleOpenNodes}
|
||||||
onOpenWorkflowSteps={openWorkflowSteps}
|
onOpenWorkflowSteps={modalManager.openWorkflowSteps}
|
||||||
onOpenMissions={viewMode === "project" && currentProject ? openMissions : undefined}
|
onOpenMissions={viewMode === "project" && currentProject ? modalManager.openMissions : undefined}
|
||||||
onOpenScripts={handleOpenScripts}
|
onOpenScripts={modalManager.openScripts}
|
||||||
onRunScript={handleRunScript}
|
onRunScript={modalManager.runScript}
|
||||||
onToggleTerminal={handleToggleTerminal}
|
onToggleTerminal={modalManager.toggleTerminal}
|
||||||
onOpenFiles={handleOpenFiles}
|
onOpenFiles={modalManager.openFiles}
|
||||||
filesOpen={filesOpen}
|
filesOpen={modalManager.filesOpen}
|
||||||
globalPaused={globalPaused}
|
globalPaused={globalPaused}
|
||||||
enginePaused={enginePaused}
|
enginePaused={enginePaused}
|
||||||
onToggleGlobalPause={handleToggleGlobalPause}
|
onToggleGlobalPause={toggleGlobalPause}
|
||||||
onToggleEnginePause={handleToggleEnginePause}
|
onToggleEnginePause={toggleEnginePause}
|
||||||
view={taskView}
|
view={taskView}
|
||||||
onChangeView={viewMode === "project" && currentProject ? handleChangeTaskView : undefined}
|
onChangeView={viewMode === "project" && currentProject ? handleChangeTaskView : undefined}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
@@ -610,11 +306,11 @@ function AppInner() {
|
|||||||
backgroundNeedsInput={bgNeedsInput}
|
backgroundNeedsInput={bgNeedsInput}
|
||||||
onOpenBackgroundSession={(session) => {
|
onOpenBackgroundSession={(session) => {
|
||||||
if (session.type === "planning") {
|
if (session.type === "planning") {
|
||||||
openPlanningWithSession(session.id);
|
modalManager.openPlanningWithSession(session.id);
|
||||||
} else if (session.type === "subtask") {
|
} else if (session.type === "subtask") {
|
||||||
openSubtaskWithSession(session.id);
|
modalManager.openSubtaskWithSession(session.id);
|
||||||
} else if (session.type === "mission_interview") {
|
} else if (session.type === "mission_interview") {
|
||||||
openMissionWithSession(session.id);
|
modalManager.openMissionWithSession(session.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDismissBackgroundSession={bgDismiss}
|
onDismissBackgroundSession={bgDismiss}
|
||||||
@@ -624,192 +320,50 @@ function AppInner() {
|
|||||||
view={taskView}
|
view={taskView}
|
||||||
onChangeView={viewMode === "project" && currentProject ? handleChangeTaskView : () => {}}
|
onChangeView={viewMode === "project" && currentProject ? handleChangeTaskView : () => {}}
|
||||||
footerVisible={viewMode === "project" && !!currentProject}
|
footerVisible={viewMode === "project" && !!currentProject}
|
||||||
modalOpen={anyModalOpen}
|
modalOpen={modalManager.anyModalOpen}
|
||||||
onOpenSettings={handleOpenSettings}
|
onOpenSettings={handleOpenSettings}
|
||||||
onOpenActivityLog={handleOpenActivityLog}
|
onOpenActivityLog={modalManager.openActivityLog}
|
||||||
onOpenMailbox={handleOpenMailbox}
|
onOpenMailbox={modalManager.openMailbox}
|
||||||
mailboxUnreadCount={mailboxUnreadCount}
|
mailboxUnreadCount={modalManager.mailboxUnreadCount}
|
||||||
onOpenGitManager={handleOpenGitManager}
|
onOpenGitManager={modalManager.openGitManager}
|
||||||
onOpenWorkflowSteps={openWorkflowSteps}
|
onOpenWorkflowSteps={modalManager.openWorkflowSteps}
|
||||||
onOpenMissions={viewMode === "project" && currentProject ? openMissions : undefined}
|
onOpenMissions={viewMode === "project" && currentProject ? modalManager.openMissions : undefined}
|
||||||
onOpenSchedules={handleOpenSchedules}
|
onOpenSchedules={modalManager.openSchedules}
|
||||||
onOpenScripts={handleOpenScripts}
|
onOpenScripts={modalManager.openScripts}
|
||||||
onToggleTerminal={handleToggleTerminal}
|
onToggleTerminal={modalManager.toggleTerminal}
|
||||||
onOpenFiles={handleOpenFiles}
|
onOpenFiles={modalManager.openFiles}
|
||||||
onOpenGitHubImport={openGitHubImport}
|
onOpenGitHubImport={modalManager.openGitHubImport}
|
||||||
onOpenPlanning={handlePlanningOpen}
|
onOpenPlanning={modalManager.openPlanning}
|
||||||
onResumePlanning={handleResumePlanning}
|
onResumePlanning={modalManager.resumePlanning}
|
||||||
activePlanningSessionCount={bgPlanningSessions.length}
|
activePlanningSessionCount={bgPlanningSessions.length}
|
||||||
onOpenUsage={handleOpenUsage}
|
onOpenUsage={modalManager.openUsage}
|
||||||
onRunScript={handleRunScript}
|
onRunScript={modalManager.runScript}
|
||||||
projectId={currentProject?.id}
|
projectId={currentProject?.id}
|
||||||
/>
|
/>
|
||||||
{viewMode === "project" && currentProject && (
|
{viewMode === "project" && currentProject && (
|
||||||
<QuickChatFAB projectId={currentProject.id} addToast={addToast} />
|
<QuickChatFAB projectId={currentProject.id} addToast={addToast} />
|
||||||
)}
|
)}
|
||||||
{detailTask && (
|
<AppModals
|
||||||
<TaskDetailModal
|
projectId={currentProject?.id}
|
||||||
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}
|
|
||||||
tasks={tasks}
|
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}
|
projects={projects}
|
||||||
currentProject={currentProject}
|
currentProject={currentProject}
|
||||||
onOpenTaskDetail={(taskId) => {
|
addToast={addToast}
|
||||||
const task = tasks.find((t) => t.id === taskId);
|
toasts={toasts}
|
||||||
if (task) {
|
removeToast={removeToast}
|
||||||
handleDetailOpen(task as TaskDetail);
|
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