From b5ec38f4b87d0998917ff7a9b9f8f96027e95220 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Mon, 23 Feb 2026 00:35:46 +0800 Subject: [PATCH] Fix kanban subtask view, project statuses, column order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add blocked/review/on_hold to ProjectStatus (backend + frontend) - ProjectForm: add new status options to dropdown - ProjectDetail: add status colors/labels for new statuses - KanbanBoard: reorder columns (review before completed) - KanbanBoard: decouple subtask view from selectedTaskId via kanbanParentTaskId — closing task panel stays in subtask view, "Back to all tasks" button now works - TaskDetailPanel: show status badge on subtask rows so kanban drag-and-drop status changes are visible Co-Authored-By: Claude Opus 4.6 --- backend/app/schemas/project.py | 2 +- .../src/components/projects/KanbanBoard.tsx | 20 +++++----- .../src/components/projects/ProjectDetail.tsx | 39 +++++++++++++++++-- .../src/components/projects/ProjectForm.tsx | 3 ++ .../components/projects/TaskDetailPanel.tsx | 5 +++ frontend/src/types/index.ts | 2 +- 6 files changed, 57 insertions(+), 14 deletions(-) diff --git a/backend/app/schemas/project.py b/backend/app/schemas/project.py index 61d6b49..650a4eb 100644 --- a/backend/app/schemas/project.py +++ b/backend/app/schemas/project.py @@ -3,7 +3,7 @@ from datetime import datetime, date from typing import Optional, List, Literal from app.schemas.project_task import ProjectTaskResponse -ProjectStatus = Literal["not_started", "in_progress", "completed"] +ProjectStatus = Literal["not_started", "in_progress", "completed", "blocked", "review", "on_hold"] class ProjectCreate(BaseModel): diff --git a/frontend/src/components/projects/KanbanBoard.tsx b/frontend/src/components/projects/KanbanBoard.tsx index 8654572..b6b629b 100644 --- a/frontend/src/components/projects/KanbanBoard.tsx +++ b/frontend/src/components/projects/KanbanBoard.tsx @@ -16,8 +16,8 @@ const COLUMNS: { id: string; label: string; color: string }[] = [ { id: 'pending', label: 'Pending', color: 'text-gray-400' }, { id: 'in_progress', label: 'In Progress', color: 'text-blue-400' }, { id: 'blocked', label: 'Blocked', color: 'text-red-400' }, - { id: 'review', label: 'Review', color: 'text-yellow-400' }, { id: 'on_hold', label: 'On Hold', color: 'text-orange-400' }, + { id: 'review', label: 'Review', color: 'text-yellow-400' }, { id: 'completed', label: 'Completed', color: 'text-green-400' }, ]; @@ -31,9 +31,10 @@ const priorityColors: Record = { interface KanbanBoardProps { tasks: ProjectTask[]; selectedTaskId: number | null; - selectedTask?: ProjectTask | null; + kanbanParentTask?: ProjectTask | null; onSelectTask: (taskId: number) => void; onStatusChange: (taskId: number, status: string) => void; + onBackToAllTasks?: () => void; } function KanbanColumn({ @@ -145,17 +146,18 @@ function KanbanCard({ export default function KanbanBoard({ tasks, selectedTaskId, - selectedTask, + kanbanParentTask, onSelectTask, onStatusChange, + onBackToAllTasks, }: KanbanBoardProps) { const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }) ); - // When a task is selected and has subtasks, show subtask kanban - const isSubtaskView = selectedTask != null && (selectedTask.subtasks?.length ?? 0) > 0; - const activeTasks: ProjectTask[] = isSubtaskView ? (selectedTask.subtasks ?? []) : tasks; + // Subtask view is driven by kanbanParentTask (decoupled from selected task) + const isSubtaskView = kanbanParentTask != null && (kanbanParentTask.subtasks?.length ?? 0) > 0; + const activeTasks: ProjectTask[] = isSubtaskView ? (kanbanParentTask.subtasks ?? []) : tasks; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; @@ -178,17 +180,17 @@ export default function KanbanBoard({ return (
{/* Subtask view header */} - {isSubtaskView && ( + {isSubtaskView && kanbanParentTask && (
/ - Subtasks of: {selectedTask.title} + Subtasks of: {kanbanParentTask.title}
)} diff --git a/frontend/src/components/projects/ProjectDetail.tsx b/frontend/src/components/projects/ProjectDetail.tsx index 215810a..ab055a3 100644 --- a/frontend/src/components/projects/ProjectDetail.tsx +++ b/frontend/src/components/projects/ProjectDetail.tsx @@ -42,12 +42,18 @@ const statusColors: Record = { not_started: 'bg-gray-500/10 text-gray-400 border-gray-500/20', in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20', completed: 'bg-green-500/10 text-green-400 border-green-500/20', + blocked: 'bg-red-500/10 text-red-400 border-red-500/20', + review: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20', + on_hold: 'bg-orange-500/10 text-orange-400 border-orange-500/20', }; const statusLabels: Record = { not_started: 'Not Started', in_progress: 'In Progress', completed: 'Completed', + blocked: 'Blocked', + review: 'Review', + on_hold: 'On Hold', }; type SortMode = 'manual' | 'priority' | 'due_date'; @@ -117,6 +123,7 @@ export default function ProjectDetail() { const [subtaskParentId, setSubtaskParentId] = useState(null); const [expandedTasks, setExpandedTasks] = useState>(new Set()); const [selectedTaskId, setSelectedTaskId] = useState(null); + const [kanbanParentTaskId, setKanbanParentTaskId] = useState(null); const [sortMode, setSortMode] = useState('manual'); const [viewMode, setViewMode] = useState('list'); @@ -264,6 +271,31 @@ export default function ProjectDetail() { return null; }, [selectedTaskId, allTasks]); + const kanbanParentTask = useMemo(() => { + if (!kanbanParentTaskId) return null; + return topLevelTasks.find((t) => t.id === kanbanParentTaskId) || null; + }, [kanbanParentTaskId, topLevelTasks]); + + const handleKanbanSelectTask = useCallback( + (taskId: number) => { + setSelectedTaskId(taskId); + // Only enter subtask view when clicking a top-level task with subtasks + // and we're not already in subtask view + if (!kanbanParentTaskId) { + const task = topLevelTasks.find((t) => t.id === taskId); + if (task && task.subtasks && task.subtasks.length > 0) { + setKanbanParentTaskId(taskId); + } + } + }, + [kanbanParentTaskId, topLevelTasks] + ); + + const handleBackToAllTasks = useCallback(() => { + setKanbanParentTaskId(null); + setSelectedTaskId(null); + }, []); + const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; @@ -455,7 +487,7 @@ export default function ProjectDetail() { {/* View toggle */}
diff --git a/frontend/src/components/projects/TaskDetailPanel.tsx b/frontend/src/components/projects/TaskDetailPanel.tsx index 6b48041..9fe751d 100644 --- a/frontend/src/components/projects/TaskDetailPanel.tsx +++ b/frontend/src/components/projects/TaskDetailPanel.tsx @@ -462,6 +462,11 @@ export default function TaskDetailPanel({ > {subtask.title} + + {taskStatusLabels[subtask.status] ?? subtask.status} + diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 406c5db..319d502 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -91,7 +91,7 @@ export interface Project { id: number; name: string; description?: string; - status: 'not_started' | 'in_progress' | 'completed'; + status: 'not_started' | 'in_progress' | 'completed' | 'blocked' | 'review' | 'on_hold'; color?: string; due_date?: string; created_at: string;