From 32e021376c3f5e27cc07c16b5e9beaed98e70af4 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 1 Mar 2026 16:30:51 -0600 Subject: [PATCH] feat: inline add-task form in Kanban columns --- .../src/app/(authenticated)/kanban/page.tsx | 152 +++++++++++++++++- 1 file changed, 148 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/(authenticated)/kanban/page.tsx b/apps/web/src/app/(authenticated)/kanban/page.tsx index 6b1a8e6..a55979b 100644 --- a/apps/web/src/app/(authenticated)/kanban/page.tsx +++ b/apps/web/src/app/(authenticated)/kanban/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import type { ReactElement } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; @@ -12,7 +12,7 @@ import type { } from "@hello-pangea/dnd"; import { MosaicSpinner } from "@/components/ui/MosaicSpinner"; -import { fetchTasks, updateTask, type TaskFilters } from "@/lib/api/tasks"; +import { fetchTasks, updateTask, createTask, type TaskFilters } from "@/lib/api/tasks"; import { fetchProjects, type Project } from "@/lib/api/projects"; import { useWorkspaceId } from "@/lib/hooks"; import type { Task } from "@mosaic/shared"; @@ -184,9 +184,47 @@ function TaskCard({ task, provided, snapshot, columnAccent }: TaskCardProps): Re interface KanbanColumnProps { config: ColumnConfig; tasks: Task[]; + onAddTask: (status: TaskStatus, title: string) => Promise; } -function KanbanColumn({ config, tasks }: KanbanColumnProps): ReactElement { +function KanbanColumn({ config, tasks, onAddTask }: KanbanColumnProps): ReactElement { + const [showAddForm, setShowAddForm] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const inputRef = useRef(null); + + // Focus input when form is shown + useEffect(() => { + if (showAddForm && inputRef.current) { + inputRef.current.focus(); + } + }, [showAddForm]); + + const handleSubmit = async (e: React.SyntheticEvent): Promise => { + e.preventDefault(); + if (!inputValue.trim() || isSubmitting) { + return; + } + + setIsSubmitting(true); + try { + await onAddTask(config.status, inputValue.trim()); + setInputValue(""); + setShowAddForm(false); + } catch (err) { + console.error("[KanbanColumn] Failed to add task:", err); + } finally { + setIsSubmitting(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent): void => { + if (e.key === "Escape") { + setShowAddForm(false); + setInputValue(""); + } + }; + return (
)} + + {/* Add Task Form */} + {!showAddForm ? ( + + ) : ( +
+ { + setInputValue(e.target.value); + }} + onKeyDown={handleKeyDown} + placeholder="Task title..." + disabled={isSubmitting} + style={{ + width: "100%", + padding: "8px 10px", + borderRadius: "var(--r)", + border: `1px solid ${inputValue ? "var(--primary)" : "var(--border)"}`, + background: "var(--surface)", + color: "var(--text)", + fontSize: "0.85rem", + outline: "none", + opacity: isSubmitting ? 0.6 : 1, + }} + autoFocus + /> +
+ Press{" "} + + Enter + {" "} + to save,{" "} + + Escape + {" "} + to cancel +
+
+ )}
); } @@ -621,6 +742,24 @@ export default function KanbanPage(): ReactElement { void loadTasks(workspaceId); } + /* --- add task handler --- */ + + const handleAddTask = useCallback( + async (status: TaskStatus, title: string) => { + try { + const wsId = workspaceId ?? undefined; + const newTask = await createTask({ title, status }, wsId); + // Optimistically add to local state + setTasks((prev) => [...prev, newTask]); + } catch (err: unknown) { + console.error("[Kanban] Failed to create task:", err); + // Re-fetch on error to get consistent state + void loadTasks(workspaceId); + } + }, + [workspaceId, loadTasks] + ); + /* --- render --- */ return ( @@ -755,7 +894,12 @@ export default function KanbanPage(): ReactElement { }} > {COLUMNS.map((col) => ( - + ))} -- 2.49.1