--- task_id: sprint_4_story_2_task_3 story_id: sprint_4_story_2 sprint_id: sprint_4 status: not_started type: frontend assignee: Frontend Developer 2 created_date: 2025-11-05 estimated_hours: 4 --- # Task 3: Implement TaskList Component with Filters and Sorting ## Description Create the TaskList component that displays all Tasks for a Story, with filtering by status/priority/assignee and sorting capabilities. This component is the container for Task management UI. ## What to Do 1. Create `components/projects/task-list.tsx` 2. Display Task count badge (e.g., "Tasks (8)") 3. Add "Add Task" button to open inline form 4. Implement Task filters (All, Todo, InProgress, Done) 5. Implement Task sorting (Priority, Status, Created date, Assignee) 6. Show empty state when no Tasks exist 7. Render TaskCard components for each Task 8. Add loading skeleton during data fetch 9. Integrate with useTasks hook 10. Test with various filter/sort combinations ## Files to Create - `components/projects/task-list.tsx` (new, ~300-400 lines) ## Implementation Details ```typescript // components/projects/task-list.tsx 'use client'; import { useState, useMemo } from 'react'; import { Plus, Filter, ArrowUpDown } from 'lucide-react'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Skeleton } from '@/components/ui/skeleton'; import { TaskCard } from './task-card'; import { TaskForm } from './task-form'; import { useTasks } from '@/lib/hooks/use-tasks'; import type { Task, WorkItemStatus, WorkItemPriority } from '@/types/project'; interface TaskListProps { storyId: string; readonly?: boolean; } type FilterStatus = 'All' | WorkItemStatus; type FilterPriority = 'All' | WorkItemPriority; type SortOption = 'priority' | 'status' | 'createdAt' | 'assignee'; export function TaskList({ storyId, readonly = false }: TaskListProps) { // State const [showAddForm, setShowAddForm] = useState(false); const [filterStatus, setFilterStatus] = useState('All'); const [filterPriority, setFilterPriority] = useState('All'); const [sortBy, setSortBy] = useState('priority'); // Fetch Tasks const { data: tasks = [], isLoading, error } = useTasks(storyId); // Filter and sort Tasks const filteredAndSortedTasks = useMemo(() => { let result = [...tasks]; // Apply status filter if (filterStatus !== 'All') { result = result.filter((task) => task.status === filterStatus); } // Apply priority filter if (filterPriority !== 'All') { result = result.filter((task) => task.priority === filterPriority); } // Apply sorting result.sort((a, b) => { switch (sortBy) { case 'priority': return getPriorityValue(b.priority) - getPriorityValue(a.priority); case 'status': return getStatusValue(a.status) - getStatusValue(b.status); case 'createdAt': return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); case 'assignee': return (a.assigneeId || '').localeCompare(b.assigneeId || ''); default: return 0; } }); return result; }, [tasks, filterStatus, filterPriority, sortBy]); // Task count const taskCount = tasks.length; const completedCount = tasks.filter((t) => t.status === 'Done').length; // Loading state if (isLoading) { return ; } // Error state if (error) { return (

Failed to load Tasks. Please try again.

); } return (
{/* Header */}

Tasks

{taskCount} total {completedCount > 0 && ( {completedCount} done )}
{!readonly && ( )}
{/* Filters and Sorting */} {taskCount > 0 && (
Filter:
Sort:
)} {/* Add Task Form */} {showAddForm && ( setShowAddForm(false)} onCancel={() => setShowAddForm(false)} /> )} {/* Task List */} {filteredAndSortedTasks.length > 0 ? (
{filteredAndSortedTasks.map((task) => ( ))}
) : ( { setFilterStatus('All'); setFilterPriority('All'); }} onAddTask={() => setShowAddForm(true)} readonly={readonly} /> )}
); } // Helper functions for sorting function getPriorityValue(priority: WorkItemPriority): number { const values = { Low: 1, Medium: 2, High: 3, Critical: 4 }; return values[priority] || 0; } function getStatusValue(status: WorkItemStatus): number { const values = { Backlog: 1, Todo: 2, InProgress: 3, Done: 4 }; return values[status] || 0; } // Empty state component function EmptyState({ hasFilters, onReset, onAddTask, readonly, }: { hasFilters: boolean; onReset: () => void; onAddTask: () => void; readonly: boolean; }) { if (hasFilters) { return (

No Tasks match the selected filters.

); } return (
☑️

No Tasks Yet

Break down this Story into technical Tasks to track implementation progress.

{!readonly && ( )}
); } // Loading skeleton function TaskListSkeleton() { return (
); } ``` ## Acceptance Criteria - [ ] TaskList component displays all Tasks for Story - [ ] Task count badge shows total and completed count - [ ] "Add Task" button opens inline Task form - [ ] Status filter works (All, Backlog, Todo, InProgress, Done) - [ ] Priority filter works (All, Low, Medium, High, Critical) - [ ] Sorting works (Priority, Status, Created date, Assignee) - [ ] Filters and sorting can be combined - [ ] Empty state displays when no Tasks exist - [ ] Empty state with filters shows "Reset Filters" button - [ ] Loading skeleton displays during data fetch - [ ] Error state displays on fetch failure - [ ] Component integrates with useTasks hook - [ ] Readonly mode hides "Add Task" button - [ ] Performance: Filters/sorts without lag (<100ms) ## Testing **Manual Testing**: 1. Navigate to Story detail page 2. Verify Task count badge shows correctly (e.g., "8 total, 3 done") 3. Click "Add Task" → Verify inline form appears 4. Create 5 Tasks with different statuses and priorities 5. Test status filter → Select "Done" → Verify only Done Tasks show 6. Test priority filter → Select "High" → Verify only High priority Tasks show 7. Test combined filters → Status: InProgress + Priority: High 8. Test sorting → Sort by Priority → Verify High/Critical at top 9. Test sorting → Sort by Status → Verify Backlog → Todo → InProgress → Done 10. Reset filters → Verify all Tasks show again 11. Test empty state (Story with no Tasks) → Verify helpful message **Unit Test**: ```typescript import { render, screen, fireEvent } from '@testing-library/react'; import { TaskList } from './task-list'; describe('TaskList', () => { const mockTasks = [ { id: '1', title: 'Task 1', status: 'Todo', priority: 'High' }, { id: '2', title: 'Task 2', status: 'Done', priority: 'Low' }, { id: '3', title: 'Task 3', status: 'InProgress', priority: 'Critical' }, ]; it('renders task count badge', () => { render(); expect(screen.getByText('3 total')).toBeInTheDocument(); expect(screen.getByText('1 done')).toBeInTheDocument(); }); it('filters tasks by status', () => { render(); fireEvent.click(screen.getByText('All Status')); fireEvent.click(screen.getByText('Done')); expect(screen.getAllByTestId('task-card')).toHaveLength(1); expect(screen.getByText('Task 2')).toBeInTheDocument(); }); it('sorts tasks by priority', () => { render(); const tasks = screen.getAllByTestId('task-card'); expect(tasks[0]).toHaveTextContent('Task 3'); // Critical first expect(tasks[1]).toHaveTextContent('Task 1'); // High second }); }); ``` ## Dependencies **Prerequisites**: - Task 2 (useTasks hook must exist) - TaskCard component (Task 4 - can use placeholder initially) - TaskForm component (Task 5 - can use placeholder initially) - ✅ shadcn/ui Card, Button, Badge, Select components **Blocks**: - Task 6 (Story detail page integration) ## Estimated Time 4 hours ## Notes **Performance**: Use `useMemo` for filtering/sorting to avoid recalculating on every render. With 100+ Tasks, this prevents performance issues. **Empty State**: Different empty states for "no Tasks" vs "no Tasks matching filters" improve UX. **Progressive Enhancement**: Component works with TaskCard and TaskForm placeholders initially. Replace with real components in Tasks 4 and 5.