diff --git a/frontend/src/components/KanbanView.jsx b/frontend/src/components/KanbanView.jsx
index 0f85aa2..e9c1ffe 100644
--- a/frontend/src/components/KanbanView.jsx
+++ b/frontend/src/components/KanbanView.jsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
-import { Plus, Check, X, Flag, Clock, ChevronDown, ChevronRight } from 'lucide-react'
+import { Plus, Check, X, Flag, Clock, ChevronDown, ChevronRight, ChevronsDown, ChevronsUp } from 'lucide-react'
import {
getProjectTasks,
createTask,
@@ -27,6 +27,18 @@ const FLAG_COLORS = {
pink: 'bg-pink-500'
}
+// Helper function to get all descendant tasks recursively
+function getAllDescendants(taskId, allTasks) {
+ const children = allTasks.filter(t => t.parent_task_id === taskId)
+ let descendants = [...children]
+
+ for (const child of children) {
+ descendants = descendants.concat(getAllDescendants(child.id, allTasks))
+ }
+
+ return descendants
+}
+
// Helper function to get all descendant tasks of a parent in a specific status
function getDescendantsInStatus(taskId, allTasks, status) {
const children = allTasks.filter(t => t.parent_task_id === taskId)
@@ -48,11 +60,19 @@ function hasDescendantsInStatus(taskId, allTasks, status) {
return getDescendantsInStatus(taskId, allTasks, status).length > 0
}
-function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatus }) {
+function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatus, expandedCards, setExpandedCards }) {
const [isEditing, setIsEditing] = useState(false)
- const [isExpanded, setIsExpanded] = useState(false)
const [editTitle, setEditTitle] = useState(task.title)
+ // Use global expanded state
+ const isExpanded = expandedCards[task.id] || false
+ const toggleExpanded = () => {
+ setExpandedCards(prev => ({
+ ...prev,
+ [task.id]: !prev[task.id]
+ }))
+ }
+
const handleSave = async () => {
try {
await updateTask(task.id, { title: editTitle })
@@ -80,13 +100,13 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatu
return (
!isParent && onDragStart(e, task)}
+ draggable={!isEditing}
+ onDragStart={(e) => onDragStart(e, task, isParent)}
className={`${
isParent
? 'bg-cyber-darker border-2 border-cyber-orange/50'
: 'bg-cyber-darkest border border-cyber-orange/30'
- } rounded-lg p-3 ${!isParent ? 'cursor-move' : ''} hover:border-cyber-orange/60 transition-all group`}
+ } rounded-lg p-3 cursor-move hover:border-cyber-orange/60 transition-all group`}
>
{isEditing ? (
@@ -121,7 +141,7 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatu
{/* Expand/collapse for parent cards */}
{isParent && childrenInColumn.length > 0 && (
)}
+
+ {/* Description */}
+ {task.description && (
+
+ {task.description}
+
+ )}
@@ -201,6 +228,8 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatu
onDragStart={onDragStart}
isParent={false}
columnStatus={columnStatus}
+ expandedCards={expandedCards}
+ setExpandedCards={setExpandedCards}
/>
))}
@@ -209,7 +238,7 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatu
)
}
-function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOver }) {
+function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOver, expandedCards, setExpandedCards }) {
const [showAddTask, setShowAddTask] = useState(false)
const handleAddTask = async (taskData) => {
@@ -218,6 +247,7 @@ function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOve
project_id: parseInt(projectId),
parent_task_id: null,
title: taskData.title,
+ description: taskData.description,
status: status.key,
tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes,
@@ -289,11 +319,14 @@ function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOve
task={task}
allTasks={allTasks}
onUpdate={onUpdate}
- onDragStart={(e, task) => {
+ onDragStart={(e, task, isParent) => {
e.dataTransfer.setData('taskId', task.id.toString())
+ e.dataTransfer.setData('isParent', isParent.toString())
}}
isParent={isParent}
columnStatus={status.key}
+ expandedCards={expandedCards}
+ setExpandedCards={setExpandedCards}
/>
)
})}
@@ -306,6 +339,7 @@ function KanbanView({ projectId }) {
const [allTasks, setAllTasks] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
+ const [expandedCards, setExpandedCards] = useState({})
useEffect(() => {
loadTasks()
@@ -323,6 +357,19 @@ function KanbanView({ projectId }) {
}
}
+ const handleExpandAll = () => {
+ const parentTasks = allTasks.filter(t => allTasks.some(child => child.parent_task_id === t.id))
+ const newExpandedState = {}
+ parentTasks.forEach(task => {
+ newExpandedState[task.id] = true
+ })
+ setExpandedCards(newExpandedState)
+ }
+
+ const handleCollapseAll = () => {
+ setExpandedCards({})
+ }
+
const handleDragOver = (e) => {
e.preventDefault()
}
@@ -330,11 +377,22 @@ function KanbanView({ projectId }) {
const handleDrop = async (e, newStatus) => {
e.preventDefault()
const taskId = parseInt(e.dataTransfer.getData('taskId'))
+ const isParent = e.dataTransfer.getData('isParent') === 'true'
if (!taskId) return
try {
+ // Update the dragged task
await updateTask(taskId, { status: newStatus })
+
+ // If it's a parent task, update all descendants
+ if (isParent) {
+ const descendants = getAllDescendants(taskId, allTasks)
+ for (const descendant of descendants) {
+ await updateTask(descendant.id, { status: newStatus })
+ }
+ }
+
loadTasks()
} catch (err) {
alert(`Error: ${err.message}`)
@@ -351,7 +409,25 @@ function KanbanView({ projectId }) {
return (
-
Kanban Board (Nested View)
+
+
Kanban Board (Nested View)
+
+
+
+
+
{STATUSES.map(status => (
@@ -363,6 +439,8 @@ function KanbanView({ projectId }) {
onUpdate={loadTasks}
onDrop={handleDrop}
onDragOver={handleDragOver}
+ expandedCards={expandedCards}
+ setExpandedCards={setExpandedCards}
/>
))}
diff --git a/frontend/src/components/TaskForm.jsx b/frontend/src/components/TaskForm.jsx
index 8a85ae4..049d893 100644
--- a/frontend/src/components/TaskForm.jsx
+++ b/frontend/src/components/TaskForm.jsx
@@ -14,6 +14,7 @@ const FLAG_COLORS = [
function TaskForm({ onSubmit, onCancel, submitLabel = "Add" }) {
const [title, setTitle] = useState('')
+ const [description, setDescription] = useState('')
const [tags, setTags] = useState('')
const [hours, setHours] = useState('')
const [minutes, setMinutes] = useState('')
@@ -33,6 +34,7 @@ function TaskForm({ onSubmit, onCancel, submitLabel = "Add" }) {
const taskData = {
title: title.trim(),
+ description: description.trim() || null,
tags: tagList && tagList.length > 0 ? tagList : null,
estimated_minutes: totalMinutes > 0 ? totalMinutes : null,
flag_color: flagColor
@@ -56,6 +58,18 @@ function TaskForm({ onSubmit, onCancel, submitLabel = "Add" }) {
/>
+ {/* Description */}
+
+
+
+
{/* Tags */}
diff --git a/frontend/src/components/TaskMenu.jsx b/frontend/src/components/TaskMenu.jsx
index 5c4ff49..9afd65e 100644
--- a/frontend/src/components/TaskMenu.jsx
+++ b/frontend/src/components/TaskMenu.jsx
@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react'
-import { MoreVertical, Clock, Tag, Flag, Edit2, Trash2, X, Check, ListTodo } from 'lucide-react'
+import { MoreVertical, Clock, Tag, Flag, Edit2, Trash2, X, Check, ListTodo, FileText } from 'lucide-react'
import { updateTask } from '../utils/api'
const FLAG_COLORS = [
@@ -22,6 +22,7 @@ const STATUSES = [
function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
const [isOpen, setIsOpen] = useState(false)
const [showTimeEdit, setShowTimeEdit] = useState(false)
+ const [showDescriptionEdit, setShowDescriptionEdit] = useState(false)
const [showTagsEdit, setShowTagsEdit] = useState(false)
const [showFlagEdit, setShowFlagEdit] = useState(false)
const [showStatusEdit, setShowStatusEdit] = useState(false)
@@ -32,6 +33,7 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
const [editHours, setEditHours] = useState(initialHours)
const [editMinutes, setEditMinutes] = useState(initialMinutes)
+ const [editDescription, setEditDescription] = useState(task.description || '')
const [editTags, setEditTags] = useState(task.tags ? task.tags.join(', ') : '')
const menuRef = useRef(null)
@@ -40,6 +42,7 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setIsOpen(false)
setShowTimeEdit(false)
+ setShowDescriptionEdit(false)
setShowTagsEdit(false)
setShowFlagEdit(false)
setShowStatusEdit(false)
@@ -65,6 +68,18 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
}
}
+ const handleUpdateDescription = async () => {
+ try {
+ const description = editDescription.trim() || null
+ await updateTask(task.id, { description })
+ setShowDescriptionEdit(false)
+ setIsOpen(false)
+ onUpdate()
+ } catch (err) {
+ alert(`Error: ${err.message}`)
+ }
+ }
+
const handleUpdateTags = async () => {
try {
const tags = editTags
@@ -184,6 +199,52 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
)}
+ {/* Description Edit */}
+ {showDescriptionEdit ? (
+
+ ) : (
+
+ )}
+
{/* Tags Edit */}
{showTagsEdit ? (
diff --git a/frontend/src/components/TreeView.jsx b/frontend/src/components/TreeView.jsx
index 7a3a2ce..3274378 100644
--- a/frontend/src/components/TreeView.jsx
+++ b/frontend/src/components/TreeView.jsx
@@ -80,6 +80,7 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
project_id: parseInt(projectId),
parent_task_id: task.id,
title: taskData.title,
+ description: taskData.description,
status: 'backlog',
tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes,
@@ -187,6 +188,13 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
)}
)}
+
+ {/* Description */}
+ {task.description && (
+
+ {task.description}
+
+ )}
{/* Actions */}
@@ -266,6 +274,7 @@ function TreeView({ projectId }) {
project_id: parseInt(projectId),
parent_task_id: null,
title: taskData.title,
+ description: taskData.description,
status: 'backlog',
tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes,