Main branch Resync on w/ gitea. v0.1.6 #1

Merged
serversdown merged 20 commits from claude/nested-todo-app-mvp-014vUjLpD8wNjkMhLMXS6Kd5 into main 2026-01-04 04:30:52 -05:00
7 changed files with 403 additions and 114 deletions
Showing only changes of commit fa25cc593a - Show all commits

View File

@@ -5,6 +5,31 @@ All notable changes to TESSERACT will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.5] - 2025-01-XX
### Added
- **Nested Kanban View** - Major feature implementation
- Parent tasks now appear in each column where they have subtasks
- Parent cards show "X of Y subtasks in this column" indicator
- Parent cards are expandable/collapsible to show children in that column
- Parent cards have distinct visual styling (thicker orange border, bold text)
- Only leaf tasks (tasks with no children) are draggable
- Parent cards automatically appear in multiple columns as children move
- Helper functions for nested Kanban logic:
- `getDescendantsInStatus()` - Get all descendant tasks in a specific status
- `hasDescendantsInStatus()` - Check if parent has any descendants in a status
### Changed
- Kanban board now labeled "Kanban Board (Nested View)"
- Parent task cards cannot be dragged (only leaf tasks)
- Column task counts now include parent cards
- Improved visual hierarchy with parent/child distinction
### Improved
- Better visualization of task distribution across statuses
- Easier to see project structure while maintaining status-based organization
- Parent tasks provide context for subtasks in each column
## [0.1.4] - 2025-01-XX ## [0.1.4] - 2025-01-XX
### Added ### Added

View File

@@ -2,7 +2,7 @@
**Task Decomposition Engine** - A self-hosted web application for managing deeply nested todo trees with advanced time tracking and project planning capabilities. **Task Decomposition Engine** - A self-hosted web application for managing deeply nested todo trees with advanced time tracking and project planning capabilities.
![Version](https://img.shields.io/badge/version-0.1.4-orange) ![Version](https://img.shields.io/badge/version-0.1.5-orange)
![License](https://img.shields.io/badge/license-MIT-blue) ![License](https://img.shields.io/badge/license-MIT-blue)
## Overview ## Overview
@@ -12,7 +12,9 @@ TESSERACT is designed for complex project management where tasks naturally decom
### Key Features ### Key Features
- **Arbitrary-Depth Nesting**: Create tasks within tasks within tasks - no limits on hierarchy depth - **Arbitrary-Depth Nesting**: Create tasks within tasks within tasks - no limits on hierarchy depth
- **Dual View Modes**: Toggle between Tree View (hierarchical) and Kanban Board (status-based) - **Dual View Modes**:
- **Tree View**: Hierarchical collapsible tree with full nesting
- **Nested Kanban**: Status-based board where parent tasks appear in multiple columns
- **Intelligent Time Tracking**: - **Intelligent Time Tracking**:
- Leaf-based time calculation (parent times = sum of descendant leaf tasks) - Leaf-based time calculation (parent times = sum of descendant leaf tasks)
- Automatic exclusion of completed tasks from time estimates - Automatic exclusion of completed tasks from time estimates
@@ -101,13 +103,29 @@ docker-compose down -v
**Expand/Collapse**: Click chevron icon to show/hide subtasks **Expand/Collapse**: Click chevron icon to show/hide subtasks
#### Kanban View #### Kanban View (Nested)
**Add Task**: Click "+" in any column to create a task with that status The Kanban board displays tasks in a nested hierarchy while maintaining status-based columns:
**Move Tasks**: Drag and drop cards between columns to change status **Parent Cards**:
- Appear in **every column** where they have subtasks
- Display "X of Y subtasks in this column" counter
- Have distinct styling (thick orange border, bold title)
- Click chevron to expand/collapse and see children in that column
- Cannot be dragged (only leaf tasks are draggable)
**Edit Tasks**: Use the three-dot menu same as Tree View **Leaf Tasks**:
- Appear only in their status column
- Fully draggable between columns
- Standard card styling
**Add Task**: Click "+" in any column to create a root-level task
**Move Tasks**: Drag and drop leaf task cards between columns
**Edit Tasks**: Use the three-dot menu on any card (parent or leaf)
**Example**: A project with backend (2 tasks in backlog, 1 in progress) and frontend (1 in done) will show the project card in all three columns with appropriate counts.
### Understanding Time Estimates ### Understanding Time Estimates
@@ -354,7 +372,6 @@ Ensure JSON structure matches schema. Common issues:
See [CHANGELOG.md](CHANGELOG.md) for version history. See [CHANGELOG.md](CHANGELOG.md) for version history.
### v0.2.0 (Planned) ### v0.2.0 (Planned)
- Nested Kanban view (parent cards split across columns)
- Task dependencies and blocking relationships - Task dependencies and blocking relationships
- Due dates and calendar view - Due dates and calendar view
- Progress tracking (% complete) - Progress tracking (% complete)

View File

@@ -13,7 +13,7 @@ function App() {
<h1 className="text-2xl font-bold text-cyber-orange"> <h1 className="text-2xl font-bold text-cyber-orange">
TESSERACT TESSERACT
<span className="ml-3 text-sm text-gray-500">Task Decomposition Engine</span> <span className="ml-3 text-sm text-gray-500">Task Decomposition Engine</span>
<span className="ml-2 text-xs text-gray-600">v0.1.4</span> <span className="ml-2 text-xs text-gray-600">v0.1.5</span>
</h1> </h1>
</div> </div>
<SearchBar /> <SearchBar />

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Plus, Check, X, Flag, Clock } from 'lucide-react' import { Plus, Check, X, Flag, Clock, ChevronDown, ChevronRight, ChevronsDown, ChevronsUp } from 'lucide-react'
import { import {
getProjectTasks, getProjectTasks,
createTask, createTask,
@@ -27,14 +27,51 @@ const FLAG_COLORS = {
pink: 'bg-pink-500' pink: 'bg-pink-500'
} }
function TaskCard({ task, allTasks, onUpdate, onDragStart }) { // 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)
let descendants = []
for (const child of children) {
if (child.status === status) {
descendants.push(child)
}
// Recursively get descendants
descendants = descendants.concat(getDescendantsInStatus(child.id, allTasks, status))
}
return descendants
}
// Helper function to check if a task has any descendants in a status
function hasDescendantsInStatus(taskId, allTasks, status) {
return getDescendantsInStatus(taskId, allTasks, status).length > 0
}
function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatus, expandedCards, setExpandedCards }) {
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [editTitle, setEditTitle] = useState(task.title) const [editTitle, setEditTitle] = useState(task.title)
// Find parent task if this is a subtask // Use global expanded state
const parentTask = task.parent_task_id const isExpanded = expandedCards[task.id] || false
? allTasks.find(t => t.id === task.parent_task_id) const toggleExpanded = () => {
: null setExpandedCards(prev => ({
...prev,
[task.id]: !prev[task.id]
}))
}
const handleSave = async () => { const handleSave = async () => {
try { try {
@@ -56,11 +93,20 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
} }
} }
// For parent cards, get children in this column's status
const childrenInColumn = isParent ? getDescendantsInStatus(task.id, allTasks, columnStatus) : []
const totalChildren = isParent ? allTasks.filter(t => t.parent_task_id === task.id).length : 0
return ( return (
<div className="mb-2">
<div <div
draggable={!isEditing} draggable={!isEditing}
onDragStart={(e) => onDragStart(e, task)} onDragStart={(e) => onDragStart(e, task, isParent)}
className="bg-cyber-darkest border border-cyber-orange/30 rounded-lg p-3 mb-2 cursor-move hover:border-cyber-orange/60 transition-all group" className={`${
isParent
? 'bg-cyber-darker border-2 border-cyber-orange/50'
: 'bg-cyber-darkest border border-cyber-orange/30'
} rounded-lg p-3 cursor-move hover:border-cyber-orange/60 transition-all group`}
> >
{isEditing ? ( {isEditing ? (
<div className="flex gap-2"> <div className="flex gap-2">
@@ -90,19 +136,33 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
) : ( ) : (
<> <>
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-2">
{/* Expand/collapse for parent cards */}
{isParent && childrenInColumn.length > 0 && (
<button
onClick={toggleExpanded}
className="text-cyber-orange hover:text-cyber-orange-bright"
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
)}
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
{/* Flag indicator */} {/* Flag indicator */}
{task.flag_color && FLAG_COLORS[task.flag_color] && ( {task.flag_color && FLAG_COLORS[task.flag_color] && (
<Flag size={12} className={`${FLAG_COLORS[task.flag_color].replace('bg-', 'text-')}`} fill="currentColor" /> <Flag size={12} className={`${FLAG_COLORS[task.flag_color].replace('bg-', 'text-')}`} fill="currentColor" />
)} )}
<span className="text-gray-200">{task.title}</span> <span className={`${isParent ? 'font-semibold text-cyber-orange' : 'text-gray-200'}`}>
{task.title}
</span>
</div> </div>
{/* Parent task context */} {/* Parent card info: show subtask count in this column */}
{parentTask && ( {isParent && (
<div className="text-xs text-gray-500 mt-1"> <div className="text-xs text-gray-500 mt-1">
subtask of: <span className="text-cyber-orange">{parentTask.title}</span> {childrenInColumn.length} of {totalChildren} subtask{totalChildren !== 1 ? 's' : ''} in this column
</div> </div>
)} )}
@@ -132,6 +192,15 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
)} )}
</div> </div>
)} )}
{/* Description */}
{task.description && (
<div className="mt-2 text-xs text-gray-400 italic">
{task.description}
</div>
)}
</div>
</div>
</div> </div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
@@ -146,10 +215,30 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
</> </>
)} )}
</div> </div>
{/* Expanded children */}
{isParent && isExpanded && childrenInColumn.length > 0 && (
<div className="ml-6 mt-2 space-y-2">
{childrenInColumn.map(child => (
<TaskCard
key={child.id}
task={child}
allTasks={allTasks}
onUpdate={onUpdate}
onDragStart={onDragStart}
isParent={false}
columnStatus={columnStatus}
expandedCards={expandedCards}
setExpandedCards={setExpandedCards}
/>
))}
</div>
)}
</div>
) )
} }
function KanbanColumn({ status, tasks, allTasks, projectId, onUpdate, onDrop, onDragOver }) { function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOver, expandedCards, setExpandedCards }) {
const [showAddTask, setShowAddTask] = useState(false) const [showAddTask, setShowAddTask] = useState(false)
const handleAddTask = async (taskData) => { const handleAddTask = async (taskData) => {
@@ -158,6 +247,7 @@ function KanbanColumn({ status, tasks, allTasks, projectId, onUpdate, onDrop, on
project_id: parseInt(projectId), project_id: parseInt(projectId),
parent_task_id: null, parent_task_id: null,
title: taskData.title, title: taskData.title,
description: taskData.description,
status: status.key, status: status.key,
tags: taskData.tags, tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes, estimated_minutes: taskData.estimated_minutes,
@@ -170,16 +260,37 @@ function KanbanColumn({ status, tasks, allTasks, projectId, onUpdate, onDrop, on
} }
} }
// Get tasks to display in this column:
// 1. All leaf tasks (no children) with this status
// 2. All parent tasks that have at least one descendant with this status
const leafTasks = allTasks.filter(t => {
const hasChildren = allTasks.some(child => child.parent_task_id === t.id)
return !hasChildren && t.status === status.key
})
const parentTasks = allTasks.filter(t => {
const hasChildren = allTasks.some(child => child.parent_task_id === t.id)
return hasChildren && hasDescendantsInStatus(t.id, allTasks, status.key)
})
// Only show root-level parents (not nested parents)
const rootParents = parentTasks.filter(t => !t.parent_task_id)
// Only show root-level leaf tasks (leaf tasks without parents)
const rootLeafTasks = leafTasks.filter(t => !t.parent_task_id)
const displayTasks = [...rootParents, ...rootLeafTasks]
return ( return (
<div <div
className="flex-1 min-w-[280px] bg-cyber-darker rounded-lg p-4 border-t-4 ${status.color}" className={`flex-1 min-w-[280px] bg-cyber-darker rounded-lg p-4 border-t-4 ${status.color}`}
onDrop={(e) => onDrop(e, status.key)} onDrop={(e) => onDrop(e, status.key)}
onDragOver={onDragOver} onDragOver={onDragOver}
> >
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3 className="font-semibold text-gray-200"> <h3 className="font-semibold text-gray-200">
{status.label} {status.label}
<span className="ml-2 text-xs text-gray-500">({tasks.length})</span> <span className="ml-2 text-xs text-gray-500">({displayTasks.length})</span>
</h3> </h3>
<button <button
onClick={() => setShowAddTask(true)} onClick={() => setShowAddTask(true)}
@@ -200,17 +311,25 @@ function KanbanColumn({ status, tasks, allTasks, projectId, onUpdate, onDrop, on
)} )}
<div className="space-y-2"> <div className="space-y-2">
{tasks.map(task => ( {displayTasks.map(task => {
const isParent = allTasks.some(t => t.parent_task_id === task.id)
return (
<TaskCard <TaskCard
key={task.id} key={task.id}
task={task} task={task}
allTasks={allTasks} allTasks={allTasks}
onUpdate={onUpdate} onUpdate={onUpdate}
onDragStart={(e, task) => { onDragStart={(e, task, isParent) => {
e.dataTransfer.setData('taskId', task.id.toString()) e.dataTransfer.setData('taskId', task.id.toString())
e.dataTransfer.setData('isParent', isParent.toString())
}} }}
isParent={isParent}
columnStatus={status.key}
expandedCards={expandedCards}
setExpandedCards={setExpandedCards}
/> />
))} )
})}
</div> </div>
</div> </div>
) )
@@ -220,6 +339,7 @@ function KanbanView({ projectId }) {
const [allTasks, setAllTasks] = useState([]) const [allTasks, setAllTasks] = useState([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState('') const [error, setError] = useState('')
const [expandedCards, setExpandedCards] = useState({})
useEffect(() => { useEffect(() => {
loadTasks() loadTasks()
@@ -237,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) => { const handleDragOver = (e) => {
e.preventDefault() e.preventDefault()
} }
@@ -244,11 +377,22 @@ function KanbanView({ projectId }) {
const handleDrop = async (e, newStatus) => { const handleDrop = async (e, newStatus) => {
e.preventDefault() e.preventDefault()
const taskId = parseInt(e.dataTransfer.getData('taskId')) const taskId = parseInt(e.dataTransfer.getData('taskId'))
const isParent = e.dataTransfer.getData('isParent') === 'true'
if (!taskId) return if (!taskId) return
try { try {
// Update the dragged task
await updateTask(taskId, { status: newStatus }) 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() loadTasks()
} catch (err) { } catch (err) {
alert(`Error: ${err.message}`) alert(`Error: ${err.message}`)
@@ -265,19 +409,38 @@ function KanbanView({ projectId }) {
return ( return (
<div> <div>
<h3 className="text-xl font-semibold text-gray-300 mb-4">Kanban Board</h3> <div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-semibold text-gray-300">Kanban Board (Nested View)</h3>
<div className="flex gap-2">
<button
onClick={handleExpandAll}
className="flex items-center gap-1 px-3 py-1.5 text-sm bg-cyber-darker border border-cyber-orange/30 text-gray-300 rounded hover:border-cyber-orange/60 hover:bg-cyber-dark transition-colors"
>
<ChevronsDown size={16} />
Expand All
</button>
<button
onClick={handleCollapseAll}
className="flex items-center gap-1 px-3 py-1.5 text-sm bg-cyber-darker border border-cyber-orange/30 text-gray-300 rounded hover:border-cyber-orange/60 hover:bg-cyber-dark transition-colors"
>
<ChevronsUp size={16} />
Collapse All
</button>
</div>
</div>
<div className="flex gap-4 overflow-x-auto pb-4"> <div className="flex gap-4 overflow-x-auto pb-4">
{STATUSES.map(status => ( {STATUSES.map(status => (
<KanbanColumn <KanbanColumn
key={status.key} key={status.key}
status={status} status={status}
tasks={allTasks.filter(t => t.status === status.key)}
allTasks={allTasks} allTasks={allTasks}
projectId={projectId} projectId={projectId}
onUpdate={loadTasks} onUpdate={loadTasks}
onDrop={handleDrop} onDrop={handleDrop}
onDragOver={handleDragOver} onDragOver={handleDragOver}
expandedCards={expandedCards}
setExpandedCards={setExpandedCards}
/> />
))} ))}
</div> </div>

View File

@@ -14,6 +14,7 @@ const FLAG_COLORS = [
function TaskForm({ onSubmit, onCancel, submitLabel = "Add" }) { function TaskForm({ onSubmit, onCancel, submitLabel = "Add" }) {
const [title, setTitle] = useState('') const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [tags, setTags] = useState('') const [tags, setTags] = useState('')
const [hours, setHours] = useState('') const [hours, setHours] = useState('')
const [minutes, setMinutes] = useState('') const [minutes, setMinutes] = useState('')
@@ -33,6 +34,7 @@ function TaskForm({ onSubmit, onCancel, submitLabel = "Add" }) {
const taskData = { const taskData = {
title: title.trim(), title: title.trim(),
description: description.trim() || null,
tags: tagList && tagList.length > 0 ? tagList : null, tags: tagList && tagList.length > 0 ? tagList : null,
estimated_minutes: totalMinutes > 0 ? totalMinutes : null, estimated_minutes: totalMinutes > 0 ? totalMinutes : null,
flag_color: flagColor flag_color: flagColor
@@ -56,6 +58,18 @@ function TaskForm({ onSubmit, onCancel, submitLabel = "Add" }) {
/> />
</div> </div>
{/* Description */}
<div>
<label className="block text-xs text-gray-400 mb-1">Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optional task description..."
rows="3"
className="w-full px-3 py-2 bg-cyber-darker border border-cyber-orange/50 rounded text-gray-100 text-sm focus:outline-none focus:border-cyber-orange resize-y"
/>
</div>
{/* Tags */} {/* Tags */}
<div> <div>
<label className="block text-xs text-gray-400 mb-1">Tags (comma-separated)</label> <label className="block text-xs text-gray-400 mb-1">Tags (comma-separated)</label>

View File

@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react' 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' import { updateTask } from '../utils/api'
const FLAG_COLORS = [ const FLAG_COLORS = [
@@ -22,6 +22,7 @@ const STATUSES = [
function TaskMenu({ task, onUpdate, onDelete, onEdit }) { function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [showTimeEdit, setShowTimeEdit] = useState(false) const [showTimeEdit, setShowTimeEdit] = useState(false)
const [showDescriptionEdit, setShowDescriptionEdit] = useState(false)
const [showTagsEdit, setShowTagsEdit] = useState(false) const [showTagsEdit, setShowTagsEdit] = useState(false)
const [showFlagEdit, setShowFlagEdit] = useState(false) const [showFlagEdit, setShowFlagEdit] = useState(false)
const [showStatusEdit, setShowStatusEdit] = useState(false) const [showStatusEdit, setShowStatusEdit] = useState(false)
@@ -32,6 +33,7 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
const [editHours, setEditHours] = useState(initialHours) const [editHours, setEditHours] = useState(initialHours)
const [editMinutes, setEditMinutes] = useState(initialMinutes) const [editMinutes, setEditMinutes] = useState(initialMinutes)
const [editDescription, setEditDescription] = useState(task.description || '')
const [editTags, setEditTags] = useState(task.tags ? task.tags.join(', ') : '') const [editTags, setEditTags] = useState(task.tags ? task.tags.join(', ') : '')
const menuRef = useRef(null) const menuRef = useRef(null)
@@ -40,6 +42,7 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
if (menuRef.current && !menuRef.current.contains(event.target)) { if (menuRef.current && !menuRef.current.contains(event.target)) {
setIsOpen(false) setIsOpen(false)
setShowTimeEdit(false) setShowTimeEdit(false)
setShowDescriptionEdit(false)
setShowTagsEdit(false) setShowTagsEdit(false)
setShowFlagEdit(false) setShowFlagEdit(false)
setShowStatusEdit(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 () => { const handleUpdateTags = async () => {
try { try {
const tags = editTags const tags = editTags
@@ -184,6 +199,52 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
</button> </button>
)} )}
{/* Description Edit */}
{showDescriptionEdit ? (
<div className="p-3 border-b border-cyber-orange/20">
<div className="flex items-center gap-2 mb-2">
<FileText size={14} className="text-cyber-orange" />
<span className="text-sm text-gray-300">Description</span>
</div>
<div className="space-y-2">
<textarea
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
placeholder="Task description..."
rows="4"
className="w-full px-2 py-1 bg-cyber-darker border border-cyber-orange/50 rounded text-gray-100 text-sm focus:outline-none focus:border-cyber-orange resize-y"
autoFocus
onClick={(e) => e.stopPropagation()}
/>
<div className="flex gap-2">
<button
onClick={handleUpdateDescription}
className="flex-1 px-2 py-1 text-sm bg-cyber-orange/20 text-cyber-orange rounded hover:bg-cyber-orange/30"
>
Save
</button>
<button
onClick={() => setShowDescriptionEdit(false)}
className="px-2 py-1 text-sm text-gray-400 hover:text-gray-300"
>
Cancel
</button>
</div>
</div>
</div>
) : (
<button
onClick={(e) => {
e.stopPropagation()
setShowDescriptionEdit(true)
}}
className="w-full flex items-center gap-2 px-3 py-2 hover:bg-cyber-darker text-gray-300 text-sm"
>
<FileText size={14} />
<span>Edit Description</span>
</button>
)}
{/* Tags Edit */} {/* Tags Edit */}
{showTagsEdit ? ( {showTagsEdit ? (
<div className="p-3 border-b border-cyber-orange/20"> <div className="p-3 border-b border-cyber-orange/20">

View File

@@ -80,6 +80,7 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
project_id: parseInt(projectId), project_id: parseInt(projectId),
parent_task_id: task.id, parent_task_id: task.id,
title: taskData.title, title: taskData.title,
description: taskData.description,
status: 'backlog', status: 'backlog',
tags: taskData.tags, tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes, estimated_minutes: taskData.estimated_minutes,
@@ -187,6 +188,13 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
)} )}
</div> </div>
)} )}
{/* Description */}
{task.description && (
<div className="mt-2 text-xs text-gray-400 italic">
{task.description}
</div>
)}
</div> </div>
{/* Actions */} {/* Actions */}
@@ -266,6 +274,7 @@ function TreeView({ projectId }) {
project_id: parseInt(projectId), project_id: parseInt(projectId),
parent_task_id: null, parent_task_id: null,
title: taskData.title, title: taskData.title,
description: taskData.description,
status: 'backlog', status: 'backlog',
tags: taskData.tags, tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes, estimated_minutes: taskData.estimated_minutes,