1
0
mirror of https://github.com/coleam00/Archon.git synced 2026-01-11 17:16:57 -05:00

The New Archon (Beta) - The Operating System for AI Coding Assistants!

This commit is contained in:
Cole Medin
2025-08-13 07:58:24 -05:00
parent 13e1fc6a0e
commit 59084036f6
603 changed files with 131376 additions and 417 deletions

View File

@@ -0,0 +1,880 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import { Check, Trash2, Edit, Tag, User, Bot, Clipboard, Save, Plus } from 'lucide-react';
import { useToast } from '../../contexts/ToastContext';
import { DeleteConfirmModal } from '../../pages/ProjectPage';
import { projectService } from '../../services/projectService';
import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils';
import { DraggableTaskCard } from './DraggableTaskCard';
export interface Task {
id: string;
title: string;
description: string;
status: 'backlog' | 'in-progress' | 'review' | 'complete';
assignee: {
name: 'User' | 'Archon' | 'AI IDE Agent';
avatar: string;
};
feature: string;
featureColor: string;
task_order: number;
}
interface TaskTableViewProps {
tasks: Task[];
onTaskView: (task: Task) => void;
onTaskComplete: (taskId: string) => void;
onTaskDelete: (task: Task) => void;
onTaskReorder: (taskId: string, newOrder: number, status: Task['status']) => void;
onTaskCreate?: (task: Omit<Task, 'id'>) => Promise<void>;
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => Promise<void>;
}
const getAssigneeGlassStyle = (assigneeName: 'User' | 'Archon' | 'AI IDE Agent') => {
switch (assigneeName) {
case 'User':
return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-blue-400 dark:border-blue-500'; // blue glass
case 'AI IDE Agent':
return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-emerald-400 dark:border-emerald-500'; // emerald green glass (like toggle)
case 'Archon':
return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-pink-400 dark:border-pink-500'; // pink glass
default:
return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-blue-400 dark:border-blue-500';
}
};
// Get glass morphism style based on task order (lower = higher priority = warmer color)
const getOrderGlassStyle = (order: number) => {
if (order <= 3) return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-rose-400 dark:border-rose-500'; // red glass
if (order <= 6) return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-orange-400 dark:border-orange-500'; // orange glass
if (order <= 10) return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-blue-400 dark:border-blue-500'; // blue glass
return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-emerald-400 dark:border-emerald-500'; // green glass
};
const getOrderTextColor = (order: number) => {
if (order <= 3) return 'text-rose-500 dark:text-rose-400'; // red text
if (order <= 6) return 'text-orange-500 dark:text-orange-400'; // orange text
if (order <= 10) return 'text-blue-500 dark:text-blue-400'; // blue text
return 'text-emerald-500 dark:text-emerald-400'; // green text
};
// Helper function to reorder tasks properly
const reorderTasks = (tasks: Task[], fromIndex: number, toIndex: number): Task[] => {
const result = [...tasks];
const [movedTask] = result.splice(fromIndex, 1);
result.splice(toIndex, 0, movedTask);
// Update task_order to be sequential (1, 2, 3, ...)
return result.map((task, index) => ({
...task,
task_order: index + 1
}));
};
// Inline editable cell component
interface EditableCellProps {
value: string;
onSave: (value: string) => void;
type?: 'text' | 'textarea' | 'select';
options?: string[];
placeholder?: string;
isEditing: boolean;
onEdit: () => void;
onCancel: () => void;
}
const EditableCell = ({
value,
onSave,
type = 'text',
options = [],
placeholder = '',
isEditing,
onEdit,
onCancel
}: EditableCellProps) => {
const [editValue, setEditValue] = useState(value);
const handleSave = () => {
onSave(editValue);
};
const handleCancel = () => {
setEditValue(value);
onCancel();
};
// Handle keyboard events for Tab/Enter to save, Escape to cancel
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault();
handleSave();
} else if (e.key === 'Escape') {
e.preventDefault();
handleCancel();
}
};
// Handle blur to save (when clicking outside)
const handleBlur = () => {
handleSave();
};
if (!isEditing) {
return (
<div
onClick={onEdit}
className="cursor-pointer hover:bg-gray-100/50 dark:hover:bg-gray-800/30 p-1 rounded transition-colors min-h-[24px] flex items-center truncate"
title={value || placeholder}
>
<span className="truncate">
{value || <span className="text-gray-400 italic">Click to edit</span>}
</span>
</div>
);
}
return (
<div className="flex items-center w-full">
{type === 'select' ? (
<select
value={editValue}
onChange={(e) => {
setEditValue(e.target.value);
// Auto-save on select change
setTimeout(() => handleSave(), 0);
}}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)]"
autoFocus
>
{options.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
) : type === 'textarea' ? (
<textarea
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
placeholder={placeholder}
className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)] resize-none"
rows={2}
autoFocus
/>
) : (
<input
type="text"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
placeholder={placeholder}
className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)]"
autoFocus
/>
)}
</div>
);
};
interface DraggableTaskRowProps {
task: Task;
index: number;
onTaskView: (task: Task) => void;
onTaskComplete: (taskId: string) => void;
onTaskDelete: (task: Task) => void;
onTaskReorder: (taskId: string, newOrder: number, status: Task['status']) => void;
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => Promise<void>;
tasksInStatus: Task[];
style?: React.CSSProperties;
}
const DraggableTaskRow = ({
task,
index,
onTaskView,
onTaskComplete,
onTaskDelete,
onTaskReorder,
onTaskUpdate,
tasksInStatus,
style
}: DraggableTaskRowProps) => {
const [editingField, setEditingField] = useState<string | null>(null);
const [isHovering, setIsHovering] = useState(false);
const [{ isDragging }, drag] = useDrag({
type: ItemTypes.TASK,
item: { id: task.id, index, status: task.status },
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
});
const [{ isOver, canDrop }, drop] = useDrop({
accept: ItemTypes.TASK,
hover: (draggedItem: { id: string; index: number; status: Task['status'] }, monitor) => {
if (!monitor.isOver({ shallow: true })) return;
if (draggedItem.id === task.id) return;
if (draggedItem.status !== task.status) return;
const draggedIndex = draggedItem.index;
const hoveredIndex = index;
if (draggedIndex === hoveredIndex) return;
console.log('HOVER: Moving task', draggedItem.id, 'to index', draggedIndex, 'to', hoveredIndex);
// Move the task immediately for visual feedback
onTaskReorder(draggedItem.id, hoveredIndex, task.status);
// Update the dragged item's index to prevent re-triggering
draggedItem.index = hoveredIndex;
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
}),
});
const handleUpdateField = async (field: string, value: string) => {
if (onTaskUpdate) {
const updates: Partial<Task> = {};
if (field === 'title') {
updates.title = value;
} else if (field === 'status') {
updates.status = value as Task['status'];
} else if (field === 'assignee') {
updates.assignee = { name: value as 'User' | 'Archon' | 'AI IDE Agent', avatar: '' };
} else if (field === 'feature') {
updates.feature = value;
}
try {
await onTaskUpdate(task.id, updates);
setEditingField(null);
} catch (error) {
console.error('Failed to update task:', error);
}
}
};
return (
<tr
ref={(node) => drag(drop(node))}
className={`
group transition-all duration-200 cursor-move
${index % 2 === 0 ? 'bg-white/50 dark:bg-black/50' : 'bg-gray-50/80 dark:bg-gray-900/30'}
hover:bg-gradient-to-r hover:from-cyan-50/70 hover:to-purple-50/70 dark:hover:from-cyan-900/20 dark:hover:to-purple-900/20
border-b border-gray-200 dark:border-gray-800 last:border-b-0
${isDragging ? 'opacity-50 scale-105 shadow-lg z-50' : ''}
${isOver && canDrop ? 'bg-cyan-100/50 dark:bg-cyan-900/20 border-cyan-400' : ''}
${isHovering ? 'transform translate-y-1 shadow-md' : ''}
`}
onMouseLeave={() => setIsHovering(false)}
style={style}
>
<td className="p-3">
<div className="flex items-center justify-center">
<div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-bold transition-all duration-300 ${getOrderGlassStyle(task.task_order)} ${getOrderTextColor(task.task_order)} ${getOrderGlow(task.task_order)}`}>
{task.task_order}
</div>
</div>
</td>
<td className="p-3 text-gray-800 dark:text-gray-200 group-hover:text-gray-900 dark:group-hover:text-white transition-colors relative">
<div className="min-w-0 flex items-center">
<div className="truncate flex-1">
<EditableCell
value={task.title}
onSave={(value) => handleUpdateField('title', value)}
isEditing={editingField === 'title'}
onEdit={() => setEditingField('title')}
onCancel={() => setEditingField(null)}
placeholder="Task title..."
/>
</div>
</div>
</td>
<td className="p-3">
<EditableCell
value={task.status === 'backlog' ? 'Backlog' :
task.status === 'in-progress' ? 'In Progress' :
task.status === 'review' ? 'Review' : 'Complete'}
onSave={(value) => {
const statusMap: Record<string, Task['status']> = {
'Backlog': 'backlog',
'In Progress': 'in-progress',
'Review': 'review',
'Complete': 'complete'
};
handleUpdateField('status', statusMap[value] || 'backlog');
}}
type="select"
options={['Backlog', 'In Progress', 'Review', 'Complete']}
isEditing={editingField === 'status'}
onEdit={() => setEditingField('status')}
onCancel={() => setEditingField(null)}
/>
</td>
<td className="p-3">
<div className="truncate">
<EditableCell
value={task.feature}
onSave={(value) => handleUpdateField('feature', value)}
isEditing={editingField === 'feature'}
onEdit={() => setEditingField('feature')}
onCancel={() => setEditingField(null)}
placeholder="Feature name..."
/>
</div>
</td>
<td className="p-3">
<div className="flex items-center justify-center">
<div
className={`flex items-center justify-center w-8 h-8 rounded-full border-2 transition-all duration-300 cursor-pointer hover:scale-110 ${getAssigneeGlassStyle(task.assignee?.name || 'User')} ${getAssigneeGlow(task.assignee?.name || 'User')}`}
onClick={() => setEditingField('assignee')}
title={`Assignee: ${task.assignee?.name || 'User'}`}
>
{getAssigneeIcon(task.assignee?.name || 'User')}
</div>
{editingField === 'assignee' && (
<div className="absolute z-50 mt-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg p-2">
<select
value={task.assignee?.name || 'User'}
onChange={(e) => {
handleUpdateField('assignee', e.target.value);
setEditingField(null);
}}
className="bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1 text-sm focus:outline-none focus:border-cyan-500"
autoFocus
>
<option value="User">User</option>
<option value="Archon">Archon</option>
<option value="AI IDE Agent">AI IDE Agent</option>
</select>
</div>
)}
</div>
</td>
<td className="p-3">
<div className="flex justify-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => onTaskDelete(task)}
className="p-1.5 rounded-full bg-red-500/20 text-red-500 hover:bg-red-500/30 hover:shadow-[0_0_10px_rgba(239,68,68,0.3)] transition-all duration-300"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
<button
onClick={() => onTaskComplete(task.id)}
className="p-1.5 rounded-full bg-green-500/20 text-green-500 hover:bg-green-500/30 hover:shadow-[0_0_10px_rgba(34,197,94,0.3)] transition-all duration-300"
>
<Check className="w-3.5 h-3.5" />
</button>
<button
onClick={() => onTaskView(task)}
className="p-1.5 rounded-full bg-cyan-500/20 text-cyan-500 hover:bg-cyan-500/30 hover:shadow-[0_0_10px_rgba(34,211,238,0.3)] transition-all duration-300"
>
<Edit className="w-3.5 h-3.5" />
</button>
{/* Copy Task ID Button - Matching Board View */}
<button
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(task.id);
// Visual feedback like in board view
const button = e.currentTarget;
const originalHTML = button.innerHTML;
button.innerHTML = '<div class="flex items-center gap-1"><span class="w-3 h-3 text-green-500">✓</span><span class="text-green-500 text-xs">Copied</span></div>';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}}
className="p-1.5 rounded-full bg-gray-500/20 text-gray-500 hover:bg-gray-500/30 hover:shadow-[0_0_10px_rgba(107,114,128,0.3)] transition-all duration-300"
title="Copy Task ID to clipboard"
>
<Clipboard className="w-3.5 h-3.5" />
</button>
</div>
</td>
</tr>
);
};
// Add Task Row Component - Always visible empty input row
interface AddTaskRowProps {
onTaskCreate?: (task: Omit<Task, 'id'>) => Promise<void>;
tasks: Task[];
statusFilter: Task['status'] | 'all';
}
const AddTaskRow = ({ onTaskCreate, tasks, statusFilter }: AddTaskRowProps) => {
const [newTask, setNewTask] = useState<Omit<Task, 'id'>>({
title: '',
description: '',
status: statusFilter === 'all' ? 'backlog' : statusFilter,
assignee: { name: 'AI IDE Agent', avatar: '' },
feature: '',
featureColor: '#3b82f6',
task_order: 1
});
const handleCreateTask = async () => {
if (!newTask.title.trim() || !onTaskCreate) return;
// Calculate the next order number for the target status
const targetStatus = newTask.status;
const tasksInStatus = tasks.filter(t => t.status === targetStatus);
const nextOrder = tasksInStatus.length > 0 ? Math.max(...tasksInStatus.map(t => t.task_order)) + 1 : 1;
try {
await onTaskCreate({
...newTask,
task_order: nextOrder
});
// Reset only the title to allow quick adding
setNewTask(prev => ({
...prev,
title: '',
description: ''
}));
} catch (error) {
console.error('Failed to create task:', error);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleCreateTask();
}
};
// Update status when filter changes
React.useEffect(() => {
if (statusFilter !== 'all') {
setNewTask(prev => ({ ...prev, status: statusFilter }));
}
}, [statusFilter]);
return (
<>
<tr className="border-t border-cyan-400 dark:border-cyan-500 bg-cyan-50/30 dark:bg-cyan-900/10 relative">
{/* Toned down neon blue line separator */}
<td colSpan={6} className="p-0 relative">
<div className="absolute inset-x-0 top-0 h-[1px] bg-gradient-to-r from-transparent via-cyan-400 to-transparent shadow-[0_0_4px_1px_rgba(34,211,238,0.4)] dark:shadow-[0_0_6px_2px_rgba(34,211,238,0.5)]"></div>
</td>
</tr>
<tr className="bg-cyan-50/20 dark:bg-cyan-900/5">
<td className="p-3">
<div className="flex items-center justify-center">
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold text-white bg-cyan-500 shadow-[0_0_10px_rgba(34,211,238,0.5)]">
+
</div>
</div>
</td>
<td className="p-3">
<input
type="text"
value={newTask.title}
onChange={(e) => setNewTask(prev => ({ ...prev, title: e.target.value }))}
onKeyPress={handleKeyPress}
placeholder="Type task title and press Enter..."
className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)] transition-all duration-200"
autoFocus
/>
</td>
<td className="p-3">
<select
value={newTask.status === 'backlog' ? 'Backlog' :
newTask.status === 'in-progress' ? 'In Progress' :
newTask.status === 'review' ? 'Review' : 'Complete'}
onChange={(e) => {
const statusMap: Record<string, Task['status']> = {
'Backlog': 'backlog',
'In Progress': 'in-progress',
'Review': 'review',
'Complete': 'complete'
};
setNewTask(prev => ({ ...prev, status: statusMap[e.target.value] || 'backlog' }));
}}
className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)]"
>
<option value="Backlog">Backlog</option>
<option value="In Progress">In Progress</option>
<option value="Review">Review</option>
<option value="Complete">Complete</option>
</select>
</td>
<td className="p-3">
<input
type="text"
value={newTask.feature}
onChange={(e) => setNewTask(prev => ({ ...prev, feature: e.target.value }))}
onKeyPress={handleKeyPress}
placeholder="Feature..."
className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)]"
/>
</td>
<td className="p-3">
<select
value={newTask.assignee.name}
onChange={(e) => setNewTask(prev => ({
...prev,
assignee: { name: e.target.value as 'User' | 'Archon' | 'AI IDE Agent', avatar: '' }
}))}
className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)]"
>
<option value="AI IDE Agent">AI IDE Agent</option>
<option value="User">User</option>
<option value="Archon">Archon</option>
</select>
</td>
<td className="p-3">
<div className="flex justify-center">
<span className="text-xs text-cyan-600 dark:text-cyan-400 font-medium">Press Enter</span>
</div>
</td>
</tr>
</>
);
};
export const TaskTableView = ({
tasks,
onTaskView,
onTaskComplete,
onTaskDelete,
onTaskReorder,
onTaskCreate,
onTaskUpdate
}: TaskTableViewProps) => {
const [statusFilter, setStatusFilter] = useState<Task['status'] | 'all'>('backlog');
// State for delete confirmation modal
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [taskToDelete, setTaskToDelete] = useState<Task | null>(null);
const { showToast } = useToast();
// Refs for scroll fade effect
const tableContainerRef = useRef<HTMLDivElement>(null);
const tableRef = useRef<HTMLTableElement>(null);
const [scrollOpacities, setScrollOpacities] = useState<Map<string, number>>(new Map());
// Calculate opacity based on row position
const calculateOpacity = (rowElement: HTMLElement, containerElement: HTMLElement) => {
const containerRect = containerElement.getBoundingClientRect();
const rowRect = rowElement.getBoundingClientRect();
// Calculate the row's position relative to the container
const rowCenter = rowRect.top + rowRect.height / 2;
const containerCenter = containerRect.top + containerRect.height / 2;
const containerHeight = containerRect.height;
// Distance from center (0 at center, 1 at edges)
const distanceFromCenter = Math.abs(rowCenter - containerCenter) / (containerHeight / 2);
// Create a smooth fade effect
// Rows at the top 40% of viewport get full opacity
// Rows fade out smoothly towards the bottom
const relativePosition = (rowRect.top - containerRect.top) / containerHeight;
if (relativePosition < 0) {
return 1; // Full opacity for rows above viewport
} else if (relativePosition < 0.4) {
return 1; // Full opacity for top 40%
} else if (relativePosition > 0.9) {
return 0.15; // Very faded at bottom (slightly more visible)
} else {
// Smooth transition from 1 to 0.15
const fadeRange = 0.9 - 0.4; // 0.5
const fadePosition = (relativePosition - 0.4) / fadeRange;
return 1 - (fadePosition * 0.85); // Fade from 1 to 0.15
}
};
// Update opacities on scroll
const updateOpacities = useCallback(() => {
if (!tableContainerRef.current || !tableRef.current) return;
const container = tableContainerRef.current;
const rows = tableRef.current.querySelectorAll('tbody tr');
const newOpacities = new Map<string, number>();
rows.forEach((row, index) => {
const opacity = calculateOpacity(row as HTMLElement, container);
newOpacities.set(`row-${index}`, opacity);
});
setScrollOpacities(newOpacities);
}, []);
// Set up scroll listener
useEffect(() => {
const container = tableContainerRef.current;
if (!container) return;
// Initial opacity calculation
updateOpacities();
// Update on scroll
container.addEventListener('scroll', updateOpacities);
// Also update on window resize
window.addEventListener('resize', updateOpacities);
return () => {
container.removeEventListener('scroll', updateOpacities);
window.removeEventListener('resize', updateOpacities);
};
}, [updateOpacities, tasks]); // Re-calculate when tasks change
// Handle task deletion (opens confirmation modal)
const handleDeleteTask = useCallback((task: Task) => {
setTaskToDelete(task);
setShowDeleteConfirm(true);
}, [setTaskToDelete, setShowDeleteConfirm]);
// Confirm deletion and execute
const confirmDeleteTask = useCallback(async () => {
if (!taskToDelete) return;
try {
await projectService.deleteTask(taskToDelete.id); // Call backend service
onTaskDelete(taskToDelete); // Notify parent component
showToast(`Task "${taskToDelete.title}" deleted successfully`, 'success');
} catch (error) {
console.error('Failed to delete task:', error);
showToast(error instanceof Error ? error.message : 'Failed to delete task', 'error');
} finally {
setShowDeleteConfirm(false);
setTaskToDelete(null);
}
}, [taskToDelete, onTaskDelete, showToast, setShowDeleteConfirm, setTaskToDelete, projectService]);
// Cancel deletion
const cancelDeleteTask = useCallback(() => {
setShowDeleteConfirm(false);
setTaskToDelete(null);
}, [setShowDeleteConfirm, setTaskToDelete]);
// Group tasks by status and sort by task_order
const getTasksByStatus = (status: Task['status']) => {
return tasks
.filter(task => task.status === status)
.sort((a, b) => a.task_order - b.task_order);
};
// Simply return tasks as-is (no hierarchy)
const organizeTasksHierarchically = (taskList: Task[]) => {
return taskList;
};
// Apply status filtering
let statusFilteredTasks: Task[];
if (statusFilter === 'all') {
statusFilteredTasks = tasks;
} else {
statusFilteredTasks = tasks.filter(task => task.status === statusFilter);
}
const filteredTasks = organizeTasksHierarchically(statusFilteredTasks);
const statuses: Task['status'][] = ['backlog', 'in-progress', 'review', 'complete'];
// Get column header color and glow based on header type (matching board view style)
const getHeaderColor = (type: 'primary' | 'secondary') => {
return type === 'primary' ? 'text-cyan-600 dark:text-cyan-400' : 'text-purple-600 dark:text-purple-400';
};
const getHeaderGlow = (type: 'primary' | 'secondary') => {
return type === 'primary' ? 'bg-cyan-500 shadow-[0_0_8px_rgba(34,211,238,0.6)]' : 'bg-purple-500 shadow-[0_0_8px_rgba(168,85,247,0.6)]';
};
return (
<div className="relative">
{/* Status Filter */}
<div className="mb-4 flex gap-2 flex-wrap py-2 items-center justify-between">
<div className="flex gap-2 flex-wrap">
<button
onClick={() => setStatusFilter('all')}
className={`
px-3 py-1.5 rounded-full text-xs transition-all duration-200
${statusFilter === 'all'
? 'bg-cyan-100 dark:bg-cyan-900/20 text-cyan-600 dark:text-cyan-400 ring-1 ring-cyan-500/50 shadow-[0_0_8px_rgba(34,211,238,0.3)]'
: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-gray-200/70 dark:hover:bg-gray-700/50'
}
`}
>
All Tasks
</button>
{statuses.map((status) => {
// Define colors for each status
const getStatusColors = (status: Task['status']) => {
switch (status) {
case 'backlog':
return {
selected: 'bg-gray-100 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400 ring-1 ring-gray-500/50 shadow-[0_0_8px_rgba(107,114,128,0.3)]',
unselected: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-gray-200/70 dark:hover:bg-gray-700/50'
};
case 'in-progress':
return {
selected: 'bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 ring-1 ring-blue-500/50 shadow-[0_0_8px_rgba(59,130,246,0.3)]',
unselected: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-blue-200/30 dark:hover:bg-blue-900/20'
};
case 'review':
return {
selected: 'bg-purple-100 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400 ring-1 ring-purple-500/50 shadow-[0_0_8px_rgba(168,85,247,0.3)]',
unselected: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-purple-200/30 dark:hover:bg-purple-900/20'
};
case 'complete':
return {
selected: 'bg-green-100 dark:bg-green-900/20 text-green-600 dark:text-green-400 ring-1 ring-green-500/50 shadow-[0_0_8px_rgba(34,197,94,0.3)]',
unselected: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-green-200/30 dark:hover:bg-green-900/20'
};
default:
return {
selected: 'bg-gray-100 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400 ring-1 ring-gray-500/50 shadow-[0_0_8px_rgba(107,114,128,0.3)]',
unselected: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-gray-200/70 dark:hover:bg-gray-700/50'
};
}
};
const colors = getStatusColors(status);
return (
<button
key={status}
onClick={() => setStatusFilter(status)}
className={`
px-3 py-1.5 rounded-full text-xs transition-all duration-200
${statusFilter === status ? colors.selected : colors.unselected}
`}
>
{status === 'backlog' ? 'Backlog' :
status === 'in-progress' ? 'In Progress' :
status === 'review' ? 'Review' : 'Complete'}
</button>
);
})}
</div>
</div>
{/* Scrollable table container */}
<div
ref={tableContainerRef}
className="overflow-x-auto overflow-y-auto max-h-[600px] relative"
style={{
maskImage: 'linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 40%, rgba(0,0,0,0.5) 70%, rgba(0,0,0,0.1) 90%, rgba(0,0,0,0) 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 40%, rgba(0,0,0,0.5) 70%, rgba(0,0,0,0.1) 90%, rgba(0,0,0,0) 100%)'
}}
>
<table ref={tableRef} className="w-full border-collapse table-fixed">
<colgroup>
<col className="w-16" />
<col className="w-auto" />
<col className="w-24" />
<col className="w-28" />
<col className="w-32" />
<col className="w-40" />
</colgroup>
<thead>
<tr className="bg-white/80 dark:bg-black/80 backdrop-blur-sm sticky top-0 z-10">
<th className="text-left p-3 font-mono border-b border-gray-300 dark:border-gray-800 relative">
<div className="flex items-center gap-2">
<span className={getHeaderColor('secondary')}>Order</span>
<span className={`w-1 h-1 rounded-full ${getHeaderGlow('secondary')}`}></span>
</div>
{/* Header divider with glow matching board view */}
<div className={`absolute bottom-0 left-[15%] right-[15%] w-[70%] mx-auto h-[1px] bg-purple-500/30 shadow-[0_0_10px_2px_rgba(168,85,247,0.2)]`}></div>
</th>
<th className="text-left p-3 font-mono border-b border-gray-300 dark:border-gray-800 relative">
<div className="flex items-center gap-2">
<span className={getHeaderColor('primary')}>Task</span>
<span className={`w-1 h-1 rounded-full ${getHeaderGlow('primary')}`}></span>
</div>
<div className={`absolute bottom-0 left-[15%] right-[15%] w-[70%] mx-auto h-[1px] bg-cyan-500/30 shadow-[0_0_10px_2px_rgba(34,211,238,0.2)]`}></div>
</th>
<th className="text-left p-3 font-mono border-b border-gray-300 dark:border-gray-800 relative">
<div className="flex items-center gap-2">
<span className={getHeaderColor('secondary')}>Status</span>
<span className={`w-1 h-1 rounded-full ${getHeaderGlow('secondary')}`}></span>
</div>
<div className={`absolute bottom-0 left-[15%] right-[15%] w-[70%] mx-auto h-[1px] bg-purple-500/30 shadow-[0_0_10px_2px_rgba(168,85,247,0.2)]`}></div>
</th>
<th className="text-left p-3 font-mono border-b border-gray-300 dark:border-gray-800 relative">
<div className="flex items-center gap-2">
<span className={getHeaderColor('secondary')}>Feature</span>
<span className={`w-1 h-1 rounded-full ${getHeaderGlow('secondary')}`}></span>
</div>
<div className={`absolute bottom-0 left-[15%] right-[15%] w-[70%] mx-auto h-[1px] bg-purple-500/30 shadow-[0_0_10px_2px_rgba(168,85,247,0.2)]`}></div>
</th>
<th className="text-left p-3 font-mono border-b border-gray-300 dark:border-gray-800 relative">
<div className="flex items-center gap-2">
<span className={getHeaderColor('primary')}>Assignee</span>
<span className={`w-1 h-1 rounded-full ${getHeaderGlow('primary')}`}></span>
</div>
<div className={`absolute bottom-0 left-[15%] right-[15%] w-[70%] mx-auto h-[1px] bg-cyan-500/30 shadow-[0_0_10px_2px_rgba(34,211,238,0.2)]`}></div>
</th>
<th className="text-center p-3 font-mono border-b border-gray-300 dark:border-gray-800 relative">
<div className="flex items-center justify-center gap-2">
<span className={getHeaderColor('primary')}>Actions</span>
<span className={`w-1 h-1 rounded-full ${getHeaderGlow('primary')}`}></span>
</div>
<div className={`absolute bottom-0 left-[15%] right-[15%] w-[70%] mx-auto h-[1px] bg-cyan-500/30 shadow-[0_0_10px_2px_rgba(34,211,238,0.2)]`}></div>
</th>
</tr>
</thead>
<tbody>
{filteredTasks.map((task, index) => (
<DraggableTaskRow
key={task.id}
task={task}
index={index}
onTaskView={onTaskView}
onTaskComplete={onTaskComplete}
onTaskDelete={handleDeleteTask}
onTaskReorder={onTaskReorder}
onTaskUpdate={onTaskUpdate}
tasksInStatus={getTasksByStatus(task.status)}
style={{
opacity: scrollOpacities.get(`row-${index}`) || 1,
transition: 'opacity 0.2s ease-out'
}}
/>
))}
{/* Add Task Row - only show if create permission exists */}
{onTaskCreate && (
<AddTaskRow
onTaskCreate={onTaskCreate}
tasks={tasks}
statusFilter={statusFilter}
/>
)}
</tbody>
</table>
{/* Spacer to allow scrolling last rows to top */}
<div style={{ height: '70vh' }} aria-hidden="true" />
</div>
{/* Delete Confirmation Modal for Tasks */}
{showDeleteConfirm && taskToDelete && (
<DeleteConfirmModal
itemName={taskToDelete.title}
onConfirm={confirmDeleteTask}
onCancel={cancelDeleteTask}
type="task"
/>
)}
</div>
);
};