Files
fusion/packages/dashboard/app/components/WorktreeGroup.tsx
Fusion 8c715168c6 feat(FN-3897): add blocker fan-out hook, badge, and detail modal section
Implemented the blocker fan-out feature across the dashboard: added a `useBlockerFanout` hook to track blocking relationships, rendered a fan-out badge on task cards, and added a blocking section to the task detail modal, with tests covering the hook and responsive modal behavior.

Fusion-Task-Id: FN-3897
2026-05-09 19:19:41 -07:00

87 lines
3.3 KiB
TypeScript

import { memo } from "react";
import type { Task, TaskDetail } from "@fusion/core";
import { ClipboardList, GitBranch } from "lucide-react";
import { TaskCard } from "./TaskCard";
import type { ToastType } from "../hooks/useToast";
import type { BlockerFanoutEntry } from "../hooks/useBlockerFanout";
interface WorktreeGroupProps {
label: string;
activeTasks: Task[];
queuedTasks: Task[];
projectId?: string;
onOpenDetail: (task: Task | TaskDetail) => void;
addToast: (message: string, type?: ToastType) => void;
globalPaused?: boolean;
onUpdateTask?: (
id: string,
updates: { title?: string; description?: string; dependencies?: string[] }
) => Promise<Task>;
onRetryTask?: (id: string) => Promise<Task>;
onOpenDetailWithTab?: (task: Task | TaskDetail, initialTab: "changes") => void;
/** Project-level stuck task timeout in milliseconds (undefined = disabled) */
taskStuckTimeoutMs?: number;
/** Called when user clicks a mission badge on a task card */
onOpenMission?: (missionId: string) => void;
/** Timestamp (ms) when task data was last confirmed fresh from the server. Used for freshness-aware stuck detection. */
lastFetchTimeMs?: number;
/** Lookup of workflow step IDs to display names, fetched once at board level. */
workflowStepNameLookup?: ReadonlyMap<string, string>;
/** Precomputed blocker fanout keyed by blocker task ID. */
blockerFanoutMap?: ReadonlyMap<string, BlockerFanoutEntry>;
}
function WorktreeGroupComponent({
label,
activeTasks,
queuedTasks,
projectId,
onOpenDetail,
addToast,
globalPaused,
onUpdateTask,
onRetryTask,
onOpenDetailWithTab,
taskStuckTimeoutMs,
onOpenMission,
lastFetchTimeMs,
workflowStepNameLookup,
blockerFanoutMap,
}: WorktreeGroupProps) {
return (
<div className="worktree-group">
<div className="worktree-group-header">
<span className="worktree-icon">
{label === "Up Next" || label === "Unassigned" ? <ClipboardList size={14} /> : <GitBranch size={14} />}
</span>
<span className="worktree-label">{label}</span>
</div>
{activeTasks.map((task) => (
<TaskCard key={task.id} task={task} projectId={projectId} onOpenDetail={onOpenDetail} addToast={addToast} globalPaused={globalPaused} onUpdateTask={onUpdateTask} onRetryTask={onRetryTask} onOpenDetailWithTab={onOpenDetailWithTab} taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} workflowStepNameLookup={workflowStepNameLookup} fanout={blockerFanoutMap?.get(task.id)} />
))}
{queuedTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
projectId={projectId}
queued
onOpenDetail={onOpenDetail}
addToast={addToast}
globalPaused={globalPaused}
onUpdateTask={onUpdateTask}
onRetryTask={onRetryTask}
onOpenDetailWithTab={onOpenDetailWithTab}
taskStuckTimeoutMs={taskStuckTimeoutMs}
onOpenMission={onOpenMission}
lastFetchTimeMs={lastFetchTimeMs}
workflowStepNameLookup={workflowStepNameLookup}
fanout={blockerFanoutMap?.get(task.id)}
/>
))}
</div>
);
}
export const WorktreeGroup = memo(WorktreeGroupComponent);
WorktreeGroup.displayName = "WorktreeGroup";