diff --git a/apps/desktop-ui/src/app/app/to-do/KanbanCard.tsx b/apps/desktop-ui/src/app/app/to-do/KanbanCard.tsx index f586e6f4..93b24c16 100644 --- a/apps/desktop-ui/src/app/app/to-do/KanbanCard.tsx +++ b/apps/desktop-ui/src/app/app/to-do/KanbanCard.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect, lazy, Suspense } from "react"; import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { GripVertical, Edit, Calendar, Tag, CheckCircle2, MoreHorizontal, Trash2, Copy, Check, Play, Pause, Timer, Archive, ArchiveRestore } from "lucide-react"; +import { GripVertical, Edit, Calendar, CheckCircle2, MoreHorizontal, Trash2, Copy, Check, Play, Pause, Timer, Archive, ArchiveRestore } from "lucide-react"; import { formatElapsed, getElapsedMinutes } from "@/app/app/to-do/utils/taskTimeUtils"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -103,10 +103,9 @@ export default function KanbanCard({ task, onUpdateTask, onDeleteTask }: KanbanC {...attributes} {...listeners} className={cn( - "group relative p-3 md:p-4 rounded-xl border transition-all duration-200", - "hover:shadow-md hover:-translate-y-0.5 bg-card", - "cursor-grab active:cursor-grabbing border-border", - "hover:border-primary/30", + "group relative p-3 rounded-md border border-border/50 bg-card shadow-xs transition-all duration-200", + "hover:shadow-sm hover:border-border", + "cursor-grab active:cursor-grabbing", isDragging && "shadow-2xl scale-105 z-50 rotate-1 opacity-90", task.status === "completed" && "opacity-75 bg-muted/30" )} @@ -121,52 +120,46 @@ export default function KanbanCard({ task, onUpdateTask, onDeleteTask }: KanbanC - {/* Status Icon and Task Text - Enhanced */} -
-
+
+ {task.priority && task.priority !== "medium" ? ( + + + + + {React.createElement(PRIORITY_CONFIG[task.priority].icon, { className: "h-3.5 w-3.5" })} + + + +

{PRIORITY_CONFIG[task.priority].label} Priority

+
+
+
+ ) : ( + )} - > - +
-
-
-

- {task.text} -

- {task.priority && task.priority !== "medium" && ( - - - - - {React.createElement(PRIORITY_CONFIG[task.priority].icon, { className: "h-3.5 w-3.5" })} - - - -

{PRIORITY_CONFIG[task.priority].label} Priority

-
-
-
+
+

+ title={task.text} + > + {task.text} +

{/* Project Badge */} {project && ( -
+
{project.name} @@ -187,10 +180,9 @@ export default function KanbanCard({ task, onUpdateTask, onDeleteTask }: KanbanC - + {tag.name} ))} diff --git a/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx b/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx index b2c827e3..36abbf9e 100644 --- a/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx +++ b/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx @@ -3,8 +3,8 @@ import { useState, useMemo, useRef, useEffect, useCallback, lazy, Suspense } from "react"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import TaskForm from "@/app/app/to-do/TaskForm"; import TaskList from "@/app/app/to-do/TaskList"; +import type { Task } from "@/app/app/to-do/types/Task"; import PaginationDemo from "@/app/app/to-do/PaginationS"; import { LazyBoundary } from "@/app/app/to-do/components/LazyBoundary"; @@ -16,6 +16,7 @@ const TaskCommandPalette = lazy(() => })) ); const ManageStatusesDialog = lazy(() => import("@/app/app/to-do/ManageStatusesDialog")); +const TaskEditDialog = lazy(() => import("@/app/app/to-do/TaskEditDialog")); const ProjectManagerDialog = lazy(() => import("@/app/app/to-do/components/ProjectManagerDialog").then((m) => ({ default: m.ProjectManagerDialog, @@ -36,16 +37,6 @@ import { useStatuses } from "./hooks/useStatuses"; import { cn } from "@/lib/utils"; import { useIsMobile } from "@/components/hooks/use-mobile"; import { useTranslations } from "next-intl"; -import { - Drawer, - DrawerContent, - DrawerHeader, - DrawerTitle, - DrawerDescription, - DrawerFooter, - DrawerClose, - DrawerTrigger, -} from "@/components/ui/drawer"; import { Badge } from "@/components/ui/badge"; import { Select, @@ -74,10 +65,10 @@ export const TaskContainer = () => { }, [isMobile]); const [searchQuery, setSearchQuery] = useState(""); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [isAddTaskOpen, setIsAddTaskOpen] = useState(false); + const [addTaskStatus, setAddTaskStatus] = useState(null); const [isPaletteOpen, setIsPaletteOpen] = useState(false); const searchInputRef = useRef(null); - const taskFormInputRef = useRef(null); const { tasks, isLoading, @@ -150,13 +141,34 @@ export const TaskContainer = () => { // Calculate statistics using all tasks stats const completionRate = allTaskStats.total > 0 ? Math.round((allTaskStats.completed / allTaskStats.total) * 100) : 0; - // New tasks land in the centrally selected project; "all" creates them unassigned. - const handleAddTask = useCallback( - (taskText: string) => { - addTask(taskText, filterProject !== "all" ? filterProject : undefined); - setIsDrawerOpen(false); + // Template for the create dialog — preselects the centrally selected project + // and, when opened from a status group's "+", that status. + const blankTask = useMemo( + () => ({ + id: "", + text: "", + status: addTaskStatus ?? "not-started", + statusOrder: 0, + createdAt: "", + created_by: "", + tags: [], + subTasks: [], + projectId: filterProject !== "all" ? filterProject : undefined, + }), + [filterProject, addTaskStatus] + ); + + const openAddTask = useCallback((statusId?: string) => { + setAddTaskStatus(statusId ?? null); + setIsAddTaskOpen(true); + }, []); + + const handleCreateTask = useCallback( + async (updates: Partial) => { + const { text = "", projectId, ...rest } = updates; + await addTask(text, projectId, rest); }, - [addTask, filterProject] + [addTask] ); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); @@ -213,7 +225,7 @@ export const TaskContainer = () => { if (!isMobile && event.key?.toLowerCase() === "n" && !isTypingInField) { event.preventDefault(); - taskFormInputRef.current?.focus(); + openAddTask(); } }; @@ -426,6 +438,16 @@ export const TaskContainer = () => { )}
+ {/* Add Task */} + + {/* Project filter lives in the ToolSidebarLayout panel. */} {/* Assignee Filter — only meaningful in a shared workspace */} @@ -618,11 +640,6 @@ export const TaskContainer = () => { )} - {/* Task Form - Hidden on mobile, visible on desktop */} -
- -
- {/* Task View */}
{viewMode === "kanban" ? ( @@ -658,6 +675,7 @@ export const TaskContainer = () => { onUpdateStatus={updateTaskStatus} onUpdateTask={updateTask} onDeleteTask={deleteTask} + onAddInStatus={openAddTask} />
) : ( @@ -670,6 +688,7 @@ export const TaskContainer = () => { onUpdateStatus={updateTaskStatus} onUpdateTask={updateTask} onDeleteTask={deleteTask} + onAddInStatus={openAddTask} /> @@ -694,35 +713,30 @@ export const TaskContainer = () => { {/* Floating Action Button (FAB) - Mobile Only */}
- - - - - -
- - {tDrawer("addNewTaskTitle")} - {tDrawer("addNewTaskDescription")} - -
- -
- - - - - -
-
-
+
+ {isAddTaskOpen && ( + + + + + + )} + {isExportDialogOpen && ( @@ -755,10 +769,7 @@ export const TaskContainer = () => { onOpenChange={setIsPaletteOpen} viewMode={viewMode} onViewModeChange={setViewMode} - onNewTask={() => { - if (isMobile) setIsDrawerOpen(true); - else taskFormInputRef.current?.focus(); - }} + onNewTask={() => openAddTask()} /> diff --git a/apps/desktop-ui/src/app/app/to-do/TaskEditDialog.tsx b/apps/desktop-ui/src/app/app/to-do/TaskEditDialog.tsx index 9c0cd679..1ce96287 100644 --- a/apps/desktop-ui/src/app/app/to-do/TaskEditDialog.tsx +++ b/apps/desktop-ui/src/app/app/to-do/TaskEditDialog.tsx @@ -28,6 +28,8 @@ interface TaskEditDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSave: (updatedTask: Partial) => Promise; + /** "create" reuses this dialog for new tasks: add labels, same form. */ + mode?: "edit" | "create"; } const priorityConfig = { @@ -83,10 +85,16 @@ const predefinedTags = [ { name: "Meeting", color: "#06b6d4" }, ]; -export default function TaskEditDialog({ task, open, onOpenChange, onSave }: TaskEditDialogProps) { +export default function TaskEditDialog({ task, open, onOpenChange, onSave, mode = "edit" }: TaskEditDialogProps) { const t = useTranslations("Tasks.editDialog"); + const tDrawer = useTranslations("Tasks.mobileDrawer"); + const tForm = useTranslations("Tasks.form"); const { statuses } = useStatuses(); const tPriorities = useTranslations("Tasks.priorities"); + const isCreate = mode === "create"; + const titleText = isCreate ? tDrawer("addNewTaskTitle") : t("title"); + const descriptionText = isCreate ? tDrawer("addNewTaskDescription") : t("description"); + const saveText = isCreate ? tForm("addButton") : t("saveChanges"); const [editedTask, setEditedTask] = useState>({}); const [newSubTask, setNewSubTask] = useState(""); const [customTagName, setCustomTagName] = useState(""); @@ -545,9 +553,9 @@ export default function TaskEditDialog({ task, open, onOpenChange, onSave }: Tas - {t("title")} + {titleText} - {t("description")} + {descriptionText} {FormContent} @@ -556,7 +564,7 @@ export default function TaskEditDialog({ task, open, onOpenChange, onSave }: Tas {t("cancel")} @@ -569,9 +577,9 @@ export default function TaskEditDialog({ task, open, onOpenChange, onSave }: Tas
- {t("title")} + {titleText} - {t("description")} + {descriptionText}
@@ -579,7 +587,7 @@ export default function TaskEditDialog({ task, open, onOpenChange, onSave }: Tas
diff --git a/apps/desktop-ui/src/app/app/to-do/TaskForm.tsx b/apps/desktop-ui/src/app/app/to-do/TaskForm.tsx deleted file mode 100644 index 7dbd06a6..00000000 --- a/apps/desktop-ui/src/app/app/to-do/TaskForm.tsx +++ /dev/null @@ -1,175 +0,0 @@ -"use client"; - -import { useState, useRef, useEffect } from "react"; -import { Textarea } from "@/components/ui/textarea"; -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { Plus, Sparkles, Keyboard } from "lucide-react"; -import { Label } from "@/components/ui/label"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; -import { useTranslations } from "next-intl"; - -interface TaskFormProps { - // The project comes from the central sidebar selection, not the form. - onAddTask: (task: string) => void; - inputRef?: React.RefObject; -} - -export default function TaskForm({ onAddTask, inputRef }: TaskFormProps) { - const t = useTranslations("Tasks.form"); - const [newTask, setNewTask] = useState(""); - const [isFocused, setIsFocused] = useState(false); - const [isMultiline, setIsMultiline] = useState(false); - const textareaRef = useRef(null); - const internalRef = inputRef || textareaRef; - - // Auto-resize textarea - useEffect(() => { - if (internalRef.current && 'scrollHeight' in internalRef.current) { - const textarea = internalRef.current as HTMLTextAreaElement; - textarea.style.height = 'auto'; - textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`; - } - }, [newTask, internalRef]); - - const handleAddTask = () => { - if (newTask.trim() === "") return; - - onAddTask(newTask.trim()); - setNewTask(""); - setIsMultiline(false); - // Reset textarea height - if (internalRef.current && 'style' in internalRef.current) { - (internalRef.current as HTMLTextAreaElement).style.height = 'auto'; - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - // Enter to submit (unless Shift is held) - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleAddTask(); - } - // Shift+Enter for new line - else if (e.key === "Enter" && e.shiftKey) { - setIsMultiline(true); - // Allow default behavior (new line) - } - // Escape to clear - else if (e.key === "Escape") { - setNewTask(""); - setIsMultiline(false); - internalRef.current?.blur(); - } - }; - - const handleChange = (e: React.ChangeEvent) => { - setNewTask(e.target.value); - if (e.target.value.includes('\n')) { - setIsMultiline(true); - } - }; - - return ( - -
-
- {/* Icon */} -
- -
- - {/* Input Field */} -
-
- -