diff --git a/apps/web/components/tasks/tasks-kanban-view.tsx b/apps/web/components/tasks/tasks-kanban-view.tsx index 5906265..cf3cfdf 100644 --- a/apps/web/components/tasks/tasks-kanban-view.tsx +++ b/apps/web/components/tasks/tasks-kanban-view.tsx @@ -13,10 +13,17 @@ import { import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Calendar, GripVertical, Plus } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Calendar, GripVertical, Link2, Loader2, Plus } from 'lucide-react'; import { toast } from 'sonner'; import { TaskDetailPanel } from './task-detail-panel'; import { useRealtimeContext } from '@/components/realtime-provider'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; interface Task { id: string; @@ -59,15 +66,15 @@ function TaskCard({ task, domainName, domainColor, + depCount, onClick, }: { task: Task; domainName: string; domainColor: string | null; + depCount: number; onClick: () => void; }) { - const subtaskCount = 0; // Will be populated from API - return ( @@ -75,6 +82,20 @@ function TaskCard({ {task.title} + {depCount > 0 && ( + + + + + + + + +

{depCount} {depCount === 1 ? 'dependency' : 'dependencies'}

+
+
+
+ )}
; onTaskClick: (task: Task) => void; onAddTask: () => void; + inlineInput: string; + onInlineInputChange: (value: string) => void; + onInlineSubmit: () => void; + inlineLoading: boolean; }) { return (
@@ -173,11 +202,35 @@ function DroppableColumn({ task={task} domainName={domainMap.get(task.domainId)?.name || ''} domainColor={domainMap.get(task.domainId)?.color || null} + depCount={0} onClick={() => onTaskClick(task)} /> )) )}
+ {/* Inline task creation input */} +
+ onInlineInputChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !inlineLoading) { + onInlineSubmit(); + } + }} + className="h-8 text-sm" + disabled={inlineLoading} + /> + +
); } @@ -193,6 +246,8 @@ export function TasksKanbanView({ const [domainMap, setDomainMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [selectedTask, setSelectedTask] = useState(null); + const [inlineInputs, setInlineInputs] = useState>({}); + const [inlineLoading, setInlineLoading] = useState>({}); const { subscribe } = useRealtimeContext(); const sensors = useSensors( @@ -279,11 +334,30 @@ export function TasksKanbanView({ } } - function handleAddTask(status: string) { - // Open create dialog with pre-filled status - document.dispatchEvent( - new CustomEvent('open-create-task', { detail: { status, domainId } }) - ); + async function handleInlineCreate(status: string) { + const title = inlineInputs[status]?.trim(); + if (!title) return; + + setInlineLoading((prev) => ({ ...prev, [status]: true })); + + try { + const response = await fetch(`/api/domains/${domainId}/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, status }), + }); + if (!response.ok) throw new Error('Unable to create task'); + const newTask = await response.json(); + // Prepend the new task to the list + setTasks((prev) => [{ ...newTask, tags: [] }, ...prev]); + setInlineInputs((prev) => ({ ...prev, [status]: '' })); + toast.success(`Created "${title}"`); + } catch (error) { + console.error('Failed to create task:', error); + toast.error('Unable to create task'); + } finally { + setInlineLoading((prev) => ({ ...prev, [status]: false })); + } } if (loading) { @@ -302,7 +376,13 @@ export function TasksKanbanView({ tasks={tasks.filter((t) => t.status === column.id)} domainMap={domainMap} onTaskClick={setSelectedTask} - onAddTask={() => handleAddTask(column.id)} + onAddTask={() => {}} + inlineInput={inlineInputs[column.id] || ''} + onInlineInputChange={(value) => + setInlineInputs((prev) => ({ ...prev, [column.id]: value })) + } + onInlineSubmit={() => handleInlineCreate(column.id)} + inlineLoading={inlineLoading[column.id] || false} /> ))}