v0.1.6 changes

This commit is contained in:
serversdwn
2025-11-25 23:22:44 +00:00
parent 8d5ad6a809
commit 1a6c8cf98c
12 changed files with 650 additions and 101 deletions

View File

@@ -13,7 +13,7 @@ function App() {
<h1 className="text-2xl font-bold text-cyber-orange">
TESSERACT
<span className="ml-3 text-sm text-gray-500">Task Decomposition Engine</span>
<span className="ml-2 text-xs text-gray-600">v{import.meta.env.VITE_APP_VERSION || '0.1.5'}</span>
<span className="ml-2 text-xs text-gray-600">v{import.meta.env.VITE_APP_VERSION || '0.1.6'}</span>
</h1>
</div>
<SearchBar />

View File

@@ -10,12 +10,21 @@ import { formatTimeWithTotal } from '../utils/format'
import TaskMenu from './TaskMenu'
import TaskForm from './TaskForm'
const STATUSES = [
{ key: 'backlog', label: 'Backlog', color: 'border-gray-600' },
{ key: 'in_progress', label: 'In Progress', color: 'border-blue-500' },
{ key: 'blocked', label: 'Blocked', color: 'border-red-500' },
{ key: 'done', label: 'Done', color: 'border-green-500' }
]
// Helper to format status label
const formatStatusLabel = (status) => {
return status.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
}
// Helper to get status color based on common patterns
const getStatusColor = (status) => {
const lowerStatus = status.toLowerCase()
if (lowerStatus === 'backlog') return 'border-gray-600'
if (lowerStatus === 'in_progress' || lowerStatus.includes('progress')) return 'border-blue-500'
if (lowerStatus === 'on_hold' || lowerStatus.includes('hold') || lowerStatus.includes('waiting')) return 'border-yellow-500'
if (lowerStatus === 'done' || lowerStatus.includes('complete')) return 'border-green-500'
if (lowerStatus.includes('blocked')) return 'border-red-500'
return 'border-purple-500' // default for custom statuses
}
const FLAG_COLORS = {
red: 'bg-red-500',
@@ -60,9 +69,10 @@ function hasDescendantsInStatus(taskId, allTasks, status) {
return getDescendantsInStatus(taskId, allTasks, status).length > 0
}
function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatus, expandedCards, setExpandedCards }) {
function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatus, expandedCards, setExpandedCards, projectStatuses, projectId }) {
const [isEditing, setIsEditing] = useState(false)
const [editTitle, setEditTitle] = useState(task.title)
const [showAddSubtask, setShowAddSubtask] = useState(false)
// Use global expanded state
const isExpanded = expandedCards[task.id] || false
@@ -93,6 +103,26 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatu
}
}
const handleAddSubtask = async (taskData) => {
try {
await createTask({
project_id: parseInt(projectId),
parent_task_id: task.id,
title: taskData.title,
description: taskData.description,
status: taskData.status,
tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes,
flag_color: taskData.flag_color
})
setShowAddSubtask(false)
setExpandedCards(prev => ({ ...prev, [task.id]: true }))
onUpdate()
} catch (err) {
alert(`Error: ${err.message}`)
}
}
// 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
@@ -204,11 +234,19 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatu
</div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => setShowAddSubtask(true)}
className="text-cyber-orange hover:text-cyber-orange-bright p-1"
title="Add subtask"
>
<Plus size={14} />
</button>
<TaskMenu
task={task}
onUpdate={onUpdate}
onDelete={handleDelete}
onEdit={() => setIsEditing(true)}
projectStatuses={projectStatuses}
/>
</div>
</div>
@@ -216,6 +254,19 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatu
)}
</div>
{/* Add Subtask Form */}
{showAddSubtask && (
<div className="ml-6 mt-2">
<TaskForm
onSubmit={handleAddSubtask}
onCancel={() => setShowAddSubtask(false)}
submitLabel="Add Subtask"
projectStatuses={projectStatuses}
defaultStatus={columnStatus}
/>
</div>
)}
{/* Expanded children */}
{isParent && isExpanded && childrenInColumn.length > 0 && (
<div className="ml-6 mt-2 space-y-2">
@@ -230,6 +281,8 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatu
columnStatus={columnStatus}
expandedCards={expandedCards}
setExpandedCards={setExpandedCards}
projectStatuses={projectStatuses}
projectId={projectId}
/>
))}
</div>
@@ -238,7 +291,7 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart, isParent, columnStatu
)
}
function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOver, expandedCards, setExpandedCards }) {
function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOver, expandedCards, setExpandedCards, projectStatuses }) {
const [showAddTask, setShowAddTask] = useState(false)
const handleAddTask = async (taskData) => {
@@ -248,7 +301,7 @@ function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOve
parent_task_id: null,
title: taskData.title,
description: taskData.description,
status: status.key,
status: taskData.status,
tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes,
flag_color: taskData.flag_color
@@ -306,6 +359,8 @@ function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOve
onSubmit={handleAddTask}
onCancel={() => setShowAddTask(false)}
submitLabel="Add Task"
projectStatuses={projectStatuses}
defaultStatus={status.key}
/>
</div>
)}
@@ -327,6 +382,8 @@ function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOve
columnStatus={status.key}
expandedCards={expandedCards}
setExpandedCards={setExpandedCards}
projectStatuses={projectStatuses}
projectId={projectId}
/>
)
})}
@@ -335,12 +392,20 @@ function KanbanColumn({ status, allTasks, projectId, onUpdate, onDrop, onDragOve
)
}
function KanbanView({ projectId }) {
function KanbanView({ projectId, project }) {
const [allTasks, setAllTasks] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [expandedCards, setExpandedCards] = useState({})
// Get statuses from project, or use defaults
const statuses = project?.statuses || ['backlog', 'in_progress', 'on_hold', 'done']
const statusesWithMeta = statuses.map(status => ({
key: status,
label: formatStatusLabel(status),
color: getStatusColor(status)
}))
useEffect(() => {
loadTasks()
}, [projectId])
@@ -430,7 +495,7 @@ function KanbanView({ projectId }) {
</div>
<div className="flex gap-4 overflow-x-auto pb-4">
{STATUSES.map(status => (
{statusesWithMeta.map(status => (
<KanbanColumn
key={status.key}
status={status}
@@ -441,6 +506,7 @@ function KanbanView({ projectId }) {
onDragOver={handleDragOver}
expandedCards={expandedCards}
setExpandedCards={setExpandedCards}
projectStatuses={statuses}
/>
))}
</div>

View File

@@ -0,0 +1,298 @@
import { useState, useEffect } from 'react'
import { X, GripVertical, Plus, Trash2, Check, AlertTriangle } from 'lucide-react'
import { updateProject, getProjectTasks } from '../utils/api'
function ProjectSettings({ project, onClose, onUpdate }) {
const [statuses, setStatuses] = useState(project.statuses || [])
const [editingIndex, setEditingIndex] = useState(null)
const [editingValue, setEditingValue] = useState('')
const [draggedIndex, setDraggedIndex] = useState(null)
const [error, setError] = useState('')
const [taskCounts, setTaskCounts] = useState({})
const [deleteWarning, setDeleteWarning] = useState(null)
useEffect(() => {
loadTaskCounts()
}, [])
const loadTaskCounts = async () => {
try {
const tasks = await getProjectTasks(project.id)
const counts = {}
statuses.forEach(status => {
counts[status] = tasks.filter(t => t.status === status).length
})
setTaskCounts(counts)
} catch (err) {
console.error('Failed to load task counts:', err)
}
}
const handleDragStart = (index) => {
setDraggedIndex(index)
}
const handleDragOver = (e, index) => {
e.preventDefault()
if (draggedIndex === null || draggedIndex === index) return
const newStatuses = [...statuses]
const draggedItem = newStatuses[draggedIndex]
newStatuses.splice(draggedIndex, 1)
newStatuses.splice(index, 0, draggedItem)
setStatuses(newStatuses)
setDraggedIndex(index)
}
const handleDragEnd = () => {
setDraggedIndex(null)
}
const handleAddStatus = () => {
const newStatus = `new_status_${Date.now()}`
setStatuses([...statuses, newStatus])
setEditingIndex(statuses.length)
setEditingValue(newStatus)
}
const handleStartEdit = (index) => {
setEditingIndex(index)
setEditingValue(statuses[index])
}
const handleSaveEdit = () => {
if (!editingValue.trim()) {
setError('Status name cannot be empty')
return
}
const trimmedValue = editingValue.trim().toLowerCase().replace(/\s+/g, '_')
if (statuses.some((s, i) => i !== editingIndex && s === trimmedValue)) {
setError('Status name already exists')
return
}
const newStatuses = [...statuses]
newStatuses[editingIndex] = trimmedValue
setStatuses(newStatuses)
setEditingIndex(null)
setError('')
}
const handleCancelEdit = () => {
// If it's a new status that was never saved, remove it
if (statuses[editingIndex].startsWith('new_status_')) {
const newStatuses = statuses.filter((_, i) => i !== editingIndex)
setStatuses(newStatuses)
}
setEditingIndex(null)
setError('')
}
const handleDeleteStatus = (index) => {
const statusToDelete = statuses[index]
const taskCount = taskCounts[statusToDelete] || 0
if (taskCount > 0) {
setDeleteWarning({ index, status: statusToDelete, count: taskCount })
return
}
if (statuses.length === 1) {
setError('Cannot delete the last status')
return
}
const newStatuses = statuses.filter((_, i) => i !== index)
setStatuses(newStatuses)
}
const handleSave = async () => {
if (statuses.length === 0) {
setError('Project must have at least one status')
return
}
if (editingIndex !== null) {
setError('Please save or cancel the status you are editing')
return
}
try {
await updateProject(project.id, { statuses })
onUpdate()
onClose()
} catch (err) {
setError(err.message)
}
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-cyber-darkest border border-cyber-orange/30 rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex justify-between items-center p-6 border-b border-cyber-orange/20">
<h2 className="text-2xl font-bold text-gray-100">Project Settings</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-200"
>
<X size={24} />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-200 mb-2">Project Details</h3>
<div className="space-y-2">
<div>
<span className="text-sm text-gray-400">Name:</span>
<span className="ml-2 text-gray-200">{project.name}</span>
</div>
{project.description && (
<div>
<span className="text-sm text-gray-400">Description:</span>
<span className="ml-2 text-gray-200">{project.description}</span>
</div>
)}
</div>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-200 mb-2">Status Workflow</h3>
<p className="text-sm text-gray-400 mb-4">
Drag to reorder, click to rename. Tasks will appear in Kanban columns in this order.
</p>
{error && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded text-red-400 text-sm">
{error}
</div>
)}
<div className="space-y-2 mb-4">
{statuses.map((status, index) => (
<div
key={index}
draggable={editingIndex !== index}
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => handleDragOver(e, index)}
onDragEnd={handleDragEnd}
className={`flex items-center gap-3 p-3 bg-cyber-darker border border-cyber-orange/30 rounded ${
draggedIndex === index ? 'opacity-50' : ''
} ${editingIndex !== index ? 'cursor-move' : ''}`}
>
{editingIndex !== index && (
<GripVertical size={18} className="text-gray-500" />
)}
{editingIndex === index ? (
<>
<input
type="text"
value={editingValue}
onChange={(e) => setEditingValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveEdit()
if (e.key === 'Escape') handleCancelEdit()
}}
className="flex-1 px-2 py-1 bg-cyber-darkest border border-cyber-orange/50 rounded text-gray-100 text-sm focus:outline-none focus:border-cyber-orange"
autoFocus
/>
<button
onClick={handleSaveEdit}
className="text-green-400 hover:text-green-300"
>
<Check size={18} />
</button>
<button
onClick={handleCancelEdit}
className="text-gray-400 hover:text-gray-300"
>
<X size={18} />
</button>
</>
) : (
<>
<button
onClick={() => handleStartEdit(index)}
className="flex-1 text-left text-gray-200 hover:text-cyber-orange"
>
{status.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}
</button>
<span className="text-xs text-gray-500">
{taskCounts[status] || 0} task{taskCounts[status] === 1 ? '' : 's'}
</span>
<button
onClick={() => handleDeleteStatus(index)}
className="text-gray-400 hover:text-red-400"
disabled={statuses.length === 1}
>
<Trash2 size={18} />
</button>
</>
)}
</div>
))}
</div>
<button
onClick={handleAddStatus}
className="flex items-center gap-2 px-4 py-2 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"
>
<Plus size={16} />
Add Status
</button>
</div>
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t border-cyber-orange/20">
<button
onClick={onClose}
className="px-4 py-2 text-gray-400 hover:text-gray-200"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-4 py-2 bg-cyber-orange text-cyber-darkest rounded hover:bg-cyber-orange-bright font-semibold transition-colors"
>
Save Changes
</button>
</div>
{/* Delete Warning Dialog */}
{deleteWarning && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-cyber-darkest border border-red-500/50 rounded-lg p-6 max-w-md">
<div className="flex items-start gap-3 mb-4">
<AlertTriangle className="text-red-400 flex-shrink-0" size={24} />
<div>
<h3 className="text-lg font-semibold text-gray-100 mb-2">Cannot Delete Status</h3>
<p className="text-sm text-gray-300">
The status "{deleteWarning.status}" has {deleteWarning.count} task{deleteWarning.count === 1 ? '' : 's'}.
Please move or delete those tasks first.
</p>
</div>
</div>
<div className="flex justify-end">
<button
onClick={() => setDeleteWarning(null)}
className="px-4 py-2 bg-cyber-orange text-cyber-darkest rounded hover:bg-cyber-orange-bright font-semibold"
>
OK
</button>
</div>
</div>
</div>
)}
</div>
</div>
)
}
export default ProjectSettings

View File

@@ -12,13 +12,22 @@ const FLAG_COLORS = [
{ name: 'pink', label: 'Pink', color: 'bg-pink-500' }
]
function TaskForm({ onSubmit, onCancel, submitLabel = "Add" }) {
// Helper to format status label
const formatStatusLabel = (status) => {
return status.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
}
function TaskForm({ onSubmit, onCancel, submitLabel = "Add", projectStatuses = null, defaultStatus = "backlog" }) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [tags, setTags] = useState('')
const [hours, setHours] = useState('')
const [minutes, setMinutes] = useState('')
const [flagColor, setFlagColor] = useState(null)
const [status, setStatus] = useState(defaultStatus)
// Use provided statuses or fall back to defaults
const statuses = projectStatuses || ['backlog', 'in_progress', 'on_hold', 'done']
const handleSubmit = (e) => {
e.preventDefault()
@@ -37,7 +46,8 @@ function TaskForm({ onSubmit, onCancel, submitLabel = "Add" }) {
description: description.trim() || null,
tags: tagList && tagList.length > 0 ? tagList : null,
estimated_minutes: totalMinutes > 0 ? totalMinutes : null,
flag_color: flagColor
flag_color: flagColor,
status: status
}
onSubmit(taskData)
@@ -110,6 +120,22 @@ function TaskForm({ onSubmit, onCancel, submitLabel = "Add" }) {
</div>
</div>
{/* Status */}
<div>
<label className="block text-xs text-gray-400 mb-1">Status</label>
<select
value={status}
onChange={(e) => setStatus(e.target.value)}
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"
>
{statuses.map((s) => (
<option key={s} value={s}>
{formatStatusLabel(s)}
</option>
))}
</select>
</div>
{/* Flag Color */}
<div>
<label className="block text-xs text-gray-400 mb-1">Flag Color</label>

View File

@@ -12,14 +12,23 @@ const FLAG_COLORS = [
{ name: 'pink', color: 'bg-pink-500' }
]
const STATUSES = [
{ key: 'backlog', label: 'Backlog', color: 'text-gray-400' },
{ key: 'in_progress', label: 'In Progress', color: 'text-blue-400' },
{ key: 'blocked', label: 'Blocked', color: 'text-red-400' },
{ key: 'done', label: 'Done', color: 'text-green-400' }
]
// Helper to format status label
const formatStatusLabel = (status) => {
return status.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
}
function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
// Helper to get status color
const getStatusTextColor = (status) => {
const lowerStatus = status.toLowerCase()
if (lowerStatus === 'backlog') return 'text-gray-400'
if (lowerStatus === 'in_progress' || lowerStatus.includes('progress')) return 'text-blue-400'
if (lowerStatus === 'on_hold' || lowerStatus.includes('hold') || lowerStatus.includes('waiting')) return 'text-yellow-400'
if (lowerStatus === 'done' || lowerStatus.includes('complete')) return 'text-green-400'
if (lowerStatus.includes('blocked')) return 'text-red-400'
return 'text-purple-400' // default for custom statuses
}
function TaskMenu({ task, onUpdate, onDelete, onEdit, projectStatuses }) {
const [isOpen, setIsOpen] = useState(false)
const [showTimeEdit, setShowTimeEdit] = useState(false)
const [showDescriptionEdit, setShowDescriptionEdit] = useState(false)
@@ -334,17 +343,17 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
<span className="text-sm text-gray-300">Change Status</span>
</div>
<div className="space-y-1">
{STATUSES.map(({ key, label, color }) => (
{(projectStatuses || ['backlog', 'in_progress', 'on_hold', 'done']).map((status) => (
<button
key={key}
onClick={() => handleUpdateStatus(key)}
key={status}
onClick={() => handleUpdateStatus(status)}
className={`w-full text-left px-2 py-1.5 rounded text-sm ${
task.status === key
task.status === status
? 'bg-cyber-orange/20 border border-cyber-orange/40'
: 'hover:bg-cyber-darker border border-transparent'
} ${color} transition-all`}
} ${getStatusTextColor(status)} transition-all`}
>
{label} {task.status === key && '✓'}
{formatStatusLabel(status)} {task.status === status && '✓'}
</button>
))}
</div>

View File

@@ -18,18 +18,20 @@ import { formatTimeWithTotal } from '../utils/format'
import TaskMenu from './TaskMenu'
import TaskForm from './TaskForm'
const STATUS_COLORS = {
backlog: 'text-gray-400',
in_progress: 'text-blue-400',
blocked: 'text-red-400',
done: 'text-green-400'
// Helper to format status label
const formatStatusLabel = (status) => {
return status.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
}
const STATUS_LABELS = {
backlog: 'Backlog',
in_progress: 'In Progress',
blocked: 'Blocked',
done: 'Done'
// Helper to get status color
const getStatusColor = (status) => {
const lowerStatus = status.toLowerCase()
if (lowerStatus === 'backlog') return 'text-gray-400'
if (lowerStatus === 'in_progress' || lowerStatus.includes('progress')) return 'text-blue-400'
if (lowerStatus === 'on_hold' || lowerStatus.includes('hold') || lowerStatus.includes('waiting')) return 'text-yellow-400'
if (lowerStatus === 'done' || lowerStatus.includes('complete')) return 'text-green-400'
if (lowerStatus.includes('blocked')) return 'text-red-400'
return 'text-purple-400' // default for custom statuses
}
const FLAG_COLORS = {
@@ -42,7 +44,7 @@ const FLAG_COLORS = {
pink: 'bg-pink-500'
}
function TaskNode({ task, projectId, onUpdate, level = 0 }) {
function TaskNode({ task, projectId, onUpdate, level = 0, projectStatuses }) {
const [isExpanded, setIsExpanded] = useState(true)
const [isEditing, setIsEditing] = useState(false)
const [editTitle, setEditTitle] = useState(task.title)
@@ -81,7 +83,7 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
parent_task_id: task.id,
title: taskData.title,
description: taskData.description,
status: 'backlog',
status: taskData.status,
tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes,
flag_color: taskData.flag_color
@@ -126,10 +128,9 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
onChange={(e) => setEditStatus(e.target.value)}
className="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"
>
<option value="backlog">Backlog</option>
<option value="in_progress">In Progress</option>
<option value="blocked">Blocked</option>
<option value="done">Done</option>
{(projectStatuses || ['backlog', 'in_progress', 'on_hold', 'done']).map(status => (
<option key={status} value={status}>{formatStatusLabel(status)}</option>
))}
</select>
<button
onClick={handleSave}
@@ -157,8 +158,8 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
<Flag size={14} className={`${FLAG_COLORS[task.flag_color].replace('bg-', 'text-')}`} fill="currentColor" />
)}
<span className="text-gray-200">{task.title}</span>
<span className={`ml-2 text-xs ${STATUS_COLORS[task.status]}`}>
{STATUS_LABELS[task.status]}
<span className={`ml-2 text-xs ${getStatusColor(task.status)}`}>
{formatStatusLabel(task.status)}
</span>
</div>
@@ -211,6 +212,7 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
onUpdate={onUpdate}
onDelete={handleDelete}
onEdit={() => setIsEditing(true)}
projectStatuses={projectStatuses}
/>
</div>
</>
@@ -224,6 +226,7 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
onSubmit={handleAddSubtask}
onCancel={() => setShowAddSubtask(false)}
submitLabel="Add Subtask"
projectStatuses={projectStatuses}
/>
</div>
)}
@@ -238,6 +241,7 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
projectId={projectId}
onUpdate={onUpdate}
level={level + 1}
projectStatuses={projectStatuses}
/>
))}
</div>
@@ -246,7 +250,8 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
)
}
function TreeView({ projectId }) {
function TreeView({ projectId, project }) {
const projectStatuses = project?.statuses || ['backlog', 'in_progress', 'on_hold', 'done']
const [tasks, setTasks] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
@@ -275,7 +280,7 @@ function TreeView({ projectId }) {
parent_task_id: null,
title: taskData.title,
description: taskData.description,
status: 'backlog',
status: taskData.status,
tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes,
flag_color: taskData.flag_color
@@ -314,6 +319,7 @@ function TreeView({ projectId }) {
onSubmit={handleAddRootTask}
onCancel={() => setShowAddRoot(false)}
submitLabel="Add Task"
projectStatuses={projectStatuses}
/>
</div>
)}
@@ -331,6 +337,7 @@ function TreeView({ projectId }) {
task={task}
projectId={projectId}
onUpdate={loadTasks}
projectStatuses={projectStatuses}
/>
))}
</div>

View File

@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, LayoutList, LayoutGrid } from 'lucide-react'
import { ArrowLeft, LayoutList, LayoutGrid, Settings } from 'lucide-react'
import { getProject } from '../utils/api'
import TreeView from '../components/TreeView'
import KanbanView from '../components/KanbanView'
import ProjectSettings from '../components/ProjectSettings'
function ProjectView() {
const { projectId } = useParams()
@@ -12,6 +13,7 @@ function ProjectView() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [view, setView] = useState('tree') // 'tree' or 'kanban'
const [showSettings, setShowSettings] = useState(false)
useEffect(() => {
loadProject()
@@ -65,37 +67,55 @@ function ProjectView() {
)}
</div>
<div className="flex gap-2 bg-cyber-darkest rounded-lg p-1 border border-cyber-orange/30">
<div className="flex gap-3 items-center">
<div className="flex gap-2 bg-cyber-darkest rounded-lg p-1 border border-cyber-orange/30">
<button
onClick={() => setView('tree')}
className={`flex items-center gap-2 px-4 py-2 rounded transition-colors ${
view === 'tree'
? 'bg-cyber-orange text-cyber-darkest font-semibold'
: 'text-gray-400 hover:text-gray-200'
}`}
>
<LayoutList size={18} />
Tree View
</button>
<button
onClick={() => setView('kanban')}
className={`flex items-center gap-2 px-4 py-2 rounded transition-colors ${
view === 'kanban'
? 'bg-cyber-orange text-cyber-darkest font-semibold'
: 'text-gray-400 hover:text-gray-200'
}`}
>
<LayoutGrid size={18} />
Kanban
</button>
</div>
<button
onClick={() => setView('tree')}
className={`flex items-center gap-2 px-4 py-2 rounded transition-colors ${
view === 'tree'
? 'bg-cyber-orange text-cyber-darkest font-semibold'
: 'text-gray-400 hover:text-gray-200'
}`}
onClick={() => setShowSettings(true)}
className="p-2 text-gray-400 hover:text-cyber-orange transition-colors border border-cyber-orange/30 rounded-lg hover:border-cyber-orange/60"
title="Project Settings"
>
<LayoutList size={18} />
Tree View
</button>
<button
onClick={() => setView('kanban')}
className={`flex items-center gap-2 px-4 py-2 rounded transition-colors ${
view === 'kanban'
? 'bg-cyber-orange text-cyber-darkest font-semibold'
: 'text-gray-400 hover:text-gray-200'
}`}
>
<LayoutGrid size={18} />
Kanban
<Settings size={20} />
</button>
</div>
</div>
</div>
{view === 'tree' ? (
<TreeView projectId={projectId} />
<TreeView projectId={projectId} project={project} />
) : (
<KanbanView projectId={projectId} />
<KanbanView projectId={projectId} project={project} />
)}
{showSettings && (
<ProjectSettings
project={project}
onClose={() => setShowSettings(false)}
onUpdate={loadProject}
/>
)}
</div>
)