import { useState, useCallback, useMemo } from "react"; import { LayoutGrid, List as ListIcon, ArrowUpDown, ArrowUp, ArrowDown, Search, Link } from "lucide-react"; import type { Task, TaskDetail, Column, TaskStep } from "@kb/core"; import { COLUMN_LABELS, COLUMNS } from "@kb/core"; import { fetchTaskDetail } from "../api"; import type { ToastType } from "../hooks/useToast"; const COLUMN_COLOR_MAP: Record = { triage: "var(--triage)", todo: "var(--todo)", "in-progress": "var(--in-progress)", "in-review": "var(--in-review)", done: "var(--done)", }; const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "specifying"]); type SortField = "id" | "title" | "status" | "column" | "createdAt" | "updatedAt"; type SortDirection = "asc" | "desc"; interface ListViewProps { tasks: Task[]; onMoveTask: (id: string, column: Column) => Promise; onOpenDetail: (task: TaskDetail) => void; addToast: (message: string, type?: ToastType) => void; globalPaused?: boolean; isCreating?: boolean; onCancelCreate?: () => void; onCreateTask?: (input: { description: string; column: Column; dependencies?: string[] }) => Promise; onNewTask?: () => void; } function formatDate(iso: string): string { const date = new Date(iso); return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function getStepProgress(steps: TaskStep[]): string { if (steps.length === 0) return "-"; const done = steps.filter((s) => s.status === "done").length; return `${done}/${steps.length}`; } function getStepProgressPercent(steps: TaskStep[]): number { if (steps.length === 0) return 0; const done = steps.filter((s) => s.status === "done").length; return (done / steps.length) * 100; } export function ListView({ tasks, onMoveTask, onOpenDetail, addToast, globalPaused, onNewTask, }: ListViewProps) { const [sortField, setSortField] = useState("createdAt"); const [sortDirection, setSortDirection] = useState("desc"); const [filter, setFilter] = useState(""); const [draggingTaskId, setDraggingTaskId] = useState(null); const [dragOverColumn, setDragOverColumn] = useState(null); const handleSort = useCallback((field: SortField) => { if (sortField === field) { setSortDirection((prev) => (prev === "asc" ? "desc" : "asc")); } else { setSortField(field); setSortDirection("asc"); } }, [sortField]); const filteredAndSortedTasks = useMemo(() => { const filtered = filter ? tasks.filter( (t) => t.id.toLowerCase().includes(filter.toLowerCase()) || (t.title && t.title.toLowerCase().includes(filter.toLowerCase())) || t.description.toLowerCase().includes(filter.toLowerCase()) ) : tasks; return [...filtered].sort((a, b) => { let comparison = 0; switch (sortField) { case "id": comparison = a.id.localeCompare(b.id); break; case "title": comparison = (a.title || a.description).localeCompare(b.title || b.description); break; case "status": comparison = (a.status || "").localeCompare(b.status || ""); break; case "column": comparison = a.column.localeCompare(b.column); break; case "createdAt": comparison = a.createdAt.localeCompare(b.createdAt); break; case "updatedAt": comparison = a.updatedAt.localeCompare(b.updatedAt); break; } return sortDirection === "asc" ? comparison : -comparison; }); }, [tasks, filter, sortField, sortDirection]); const handleRowClick = useCallback( async (task: Task) => { try { const detail = await fetchTaskDetail(task.id); onOpenDetail(detail); } catch (err: any) { addToast("Failed to load task details", "error"); } }, [onOpenDetail, addToast] ); const handleDragStart = useCallback( (e: React.DragEvent, task: Task) => { if (task.paused) { e.preventDefault(); return; } e.dataTransfer.setData("text/plain", task.id); e.dataTransfer.effectAllowed = "move"; setDraggingTaskId(task.id); }, [] ); const handleDragEnd = useCallback(() => { setDraggingTaskId(null); setDragOverColumn(null); }, []); const handleColumnDragOver = useCallback( (e: React.DragEvent, column: Column) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOverColumn(column); }, [] ); const handleColumnDragLeave = useCallback(() => { setDragOverColumn(null); }, []); const handleColumnDrop = useCallback( async (e: React.DragEvent, column: Column) => { e.preventDefault(); setDragOverColumn(null); const taskId = e.dataTransfer.getData("text/plain"); if (!taskId) return; try { await onMoveTask(taskId, column); } catch (err: any) { addToast(err.message, "error"); } }, [onMoveTask, addToast] ); const getSortIcon = (field: SortField) => { if (sortField !== field) return ; return sortDirection === "asc" ? ( ) : ( ); }; return (
setFilter(e.target.value)} className="filter-input" /> {filter && ( )}
{filteredAndSortedTasks.length} of {tasks.length} tasks
{onNewTask && ( )}
{COLUMNS.map((column) => (
handleColumnDragOver(e, column)} onDragLeave={handleColumnDragLeave} onDrop={(e) => handleColumnDrop(e, column)} data-column={column} > {COLUMN_LABELS[column]} {tasks.filter((t) => t.column === column).length}
))}
{filteredAndSortedTasks.length === 0 ? (
{filter ? "No tasks match your filter" : "No tasks yet"}
) : ( {filteredAndSortedTasks.map((task) => { const isFailed = task.status === "failed"; const isPaused = task.paused === true; const isAgentActive = !globalPaused && !isFailed && !isPaused && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string)); const isDragging = draggingTaskId === task.id; return ( handleRowClick(task)} draggable={!isPaused} onDragStart={(e) => handleDragStart(e, task)} onDragEnd={handleDragEnd} data-id={task.id} > ); })}
handleSort("id")}> ID {getSortIcon("id")} handleSort("title")}> Title {getSortIcon("title")} handleSort("status")}> Status {getSortIcon("status")} handleSort("column")}> Column {getSortIcon("column")} handleSort("createdAt")}> Created {getSortIcon("createdAt")} handleSort("updatedAt")}> Updated {getSortIcon("updatedAt")} Dependencies Progress
{task.id} {task.title || task.description.slice(0, 60) + (task.description.length > 60 ? "…" : "")} {task.status ? ( {task.status} ) : ( - )} {COLUMN_LABELS[task.column]} {formatDate(task.createdAt)} {formatDate(task.updatedAt)} {task.dependencies && task.dependencies.length > 0 ? ( {task.dependencies.length} ) : ( "-" )} {task.steps.length > 0 ? (
{getStepProgress(task.steps)}
) : ( "-" )}
)}
); }