Files
fusion/packages/dashboard/app/components/Column.tsx
gsxdsm 8c46ef3cc6 feat(KB-074): add quick task creation with QuickEntryBox and NewTaskModal
- Add QuickEntryBox component for inline quick task entry in columns
- Add NewTaskModal component with full-featured task creation UI
- Integrate quick create flow through Column → Board → App component hierarchy
- Add comprehensive tests for both new components and updated existing tests
- Add CSS styles for modal, form inputs, and quick entry UI
- Include changeset for @dustinbyrne/kb package release
2026-03-29 22:40:04 -07:00

159 lines
5.8 KiB
TypeScript

import { useState, useCallback } from "react";
import { useFlashOnIncrease } from "../hooks/useFlashOnIncrease";
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@kb/core";
import { COLUMN_LABELS, COLUMN_DESCRIPTIONS } from "@kb/core";
import { TaskCard } from "./TaskCard";
import { WorktreeGroup } from "./WorktreeGroup";
import { QuickEntryBox } from "./QuickEntryBox";
import { groupByWorktree } from "../utils/worktreeGrouping";
import type { ToastType } from "../hooks/useToast";
import { ChevronDown, ChevronUp } from "lucide-react";
interface ColumnProps {
column: ColumnType;
tasks: Task[];
allTasks: Task[];
maxConcurrent: number;
onMoveTask: (id: string, column: ColumnType) => Promise<Task>;
onOpenDetail: (task: TaskDetail) => void;
addToast: (message: string, type?: ToastType) => void;
onQuickCreate?: (description: string) => Promise<void>;
onNewTask?: () => void;
autoMerge?: boolean;
onToggleAutoMerge?: () => void;
globalPaused?: boolean;
onUpdateTask?: (
id: string,
updates: { title?: string; description?: string; dependencies?: string[] }
) => Promise<Task>;
onArchiveTask?: (id: string) => Promise<Task>;
onUnarchiveTask?: (id: string) => Promise<Task>;
collapsed?: boolean;
onToggleCollapse?: () => void;
}
export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, collapsed, onToggleCollapse }: ColumnProps) {
const [dragOver, setDragOver] = useState(false);
const countFlashing = useFlashOnIncrease(tasks.length);
// Archived column is collapsed by default - don't show drag state when collapsed
const isArchived = column === "archived";
const isCollapsed = isArchived && collapsed;
const handleDragOver = useCallback((e: React.DragEvent) => {
// Don't allow dropping into archived column via drag-drop
if (isArchived) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setDragOver(true);
}, [isArchived]);
const handleDragLeave = useCallback((e: React.DragEvent) => {
const el = e.currentTarget as HTMLElement;
if (!el.contains(e.relatedTarget as Node)) {
setDragOver(false);
}
}, []);
const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
const taskId = e.dataTransfer.getData("text/plain");
if (!taskId) return;
try {
await onMoveTask(taskId, column);
} catch (err: any) {
addToast(err.message, "error");
}
}, [column, onMoveTask, addToast]);
return (
<div
className={`column${dragOver ? " drag-over" : ""}${isArchived ? " column-archived" : ""}${isCollapsed ? " column-collapsed" : ""}`}
data-column={column}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<div className="column-header">
<div className={`column-dot dot-${column}`} />
<h2>{COLUMN_LABELS[column]}</h2>
<span className={`column-count${countFlashing ? " count-flash" : ""}`}>{tasks.length}</span>
{column === "in-review" && onToggleAutoMerge && (
<label className="auto-merge-toggle" title={autoMerge ? "Auto-merge enabled" : "Auto-merge disabled"}>
<input
type="checkbox"
checked={!!autoMerge}
onChange={onToggleAutoMerge}
/>
<span className="toggle-slider" />
<span className="toggle-label">Auto-merge</span>
</label>
)}
{onNewTask && (
<button className="btn btn-primary btn-sm" onClick={onNewTask}>
+ New Task
</button>
)}
{isArchived && onToggleCollapse && (
<button
className="btn btn-icon btn-sm"
onClick={onToggleCollapse}
title={collapsed ? "Expand archived tasks" : "Collapse archived tasks"}
aria-label={collapsed ? "Expand archived tasks" : "Collapse archived tasks"}
>
{collapsed ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
</button>
)}
</div>
{!isCollapsed && <p className="column-desc">{COLUMN_DESCRIPTIONS[column]}</p>}
{!isCollapsed && (
<div className="column-body">
{column === "triage" && onQuickCreate && (
<QuickEntryBox onCreate={onQuickCreate} addToast={addToast} />
)}
{column === "in-progress" ? (
(() => {
const groups = groupByWorktree(tasks, allTasks, maxConcurrent);
return groups.length === 0 ? (
<div className="empty-column">No tasks</div>
) : (
groups.map((group) => (
<WorktreeGroup
key={group.label}
label={group.label}
activeTasks={group.activeTasks}
queuedTasks={group.queuedTasks}
onOpenDetail={onOpenDetail}
addToast={addToast}
globalPaused={globalPaused}
tasks={allTasks}
onUpdateTask={onUpdateTask}
/>
))
);
})()
) : tasks.length === 0 ? (
<div className="empty-column">No tasks</div>
) : (
tasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onOpenDetail={onOpenDetail}
addToast={addToast}
globalPaused={globalPaused}
tasks={allTasks}
onUpdateTask={onUpdateTask}
onArchiveTask={onArchiveTask}
onUnarchiveTask={onUnarchiveTask}
/>
))
)}
</div>
)}
</div>
);
}