Created comprehensive Story and Task files for Sprint 4 frontend implementation: Story 1: Story Detail Page Foundation (P0 Critical - 3 days) - 6 tasks: route creation, header, sidebar, data loading, Edit/Delete, responsive design - Fixes critical 404 error when clicking Story cards - Two-column layout consistent with Epic detail page Story 2: Task Management in Story Detail (P0 Critical - 2 days) - 6 tasks: API verification, hooks, TaskList, TaskCard, TaskForm, integration - Complete Task CRUD with checkbox status toggle - Filters, sorting, and optimistic UI updates Story 3: Enhanced Story Form (P1 High - 2 days) - 6 tasks: acceptance criteria, assignee selector, tags, story points, integration - Aligns with UX design specification - Backward compatible with existing Stories Story 4: Quick Add Story Workflow (P1 High - 2 days) - 5 tasks: inline form, keyboard shortcuts, batch creation, navigation - Rapid Story creation with minimal fields - Keyboard shortcut (Cmd/Ctrl + N) Story 5: Story Card Component (P2 Medium - 1 day) - 4 tasks: component variants, visual states, Task count, optimization - Reusable component with list/kanban/compact variants - React.memo optimization Story 6: Kanban Story Creation Enhancement (P2 Optional - 2 days) - 4 tasks: Epic card enhancement, inline form, animation, real-time updates - Contextual Story creation from Kanban - Stretch goal - implement only if ahead of schedule Total: 6 Stories, 31 Tasks, 12 days estimated Priority breakdown: P0 (2), P1 (2), P2 (2 optional) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
13 KiB
13 KiB
task_id, story_id, sprint_id, status, type, assignee, created_date, estimated_hours
| task_id | story_id | sprint_id | status | type | assignee | created_date | estimated_hours |
|---|---|---|---|---|---|---|---|
| sprint_4_story_2_task_3 | sprint_4_story_2 | sprint_4 | not_started | frontend | Frontend Developer 2 | 2025-11-05 | 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
- Create
components/projects/task-list.tsx - Display Task count badge (e.g., "Tasks (8)")
- Add "Add Task" button to open inline form
- Implement Task filters (All, Todo, InProgress, Done)
- Implement Task sorting (Priority, Status, Created date, Assignee)
- Show empty state when no Tasks exist
- Render TaskCard components for each Task
- Add loading skeleton during data fetch
- Integrate with useTasks hook
- Test with various filter/sort combinations
Files to Create
components/projects/task-list.tsx(new, ~300-400 lines)
Implementation Details
// 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<FilterStatus>('All');
const [filterPriority, setFilterPriority] = useState<FilterPriority>('All');
const [sortBy, setSortBy] = useState<SortOption>('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 <TaskListSkeleton />;
}
// Error state
if (error) {
return (
<Card className="p-6">
<p className="text-sm text-destructive">Failed to load Tasks. Please try again.</p>
</Card>
);
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">Tasks</h2>
<Badge variant="secondary" className="text-xs">
{taskCount} total
</Badge>
{completedCount > 0 && (
<Badge variant="success" className="text-xs">
{completedCount} done
</Badge>
)}
</div>
{!readonly && (
<Button
onClick={() => setShowAddForm(true)}
size="sm"
data-testid="add-task-button"
>
<Plus className="h-4 w-4 mr-2" />
Add Task
</Button>
)}
</div>
{/* Filters and Sorting */}
{taskCount > 0 && (
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Filter:</span>
</div>
<Select value={filterStatus} onValueChange={(v) => setFilterStatus(v as FilterStatus)}>
<SelectTrigger className="w-[140px] h-8">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Status</SelectItem>
<SelectItem value="Backlog">Backlog</SelectItem>
<SelectItem value="Todo">Todo</SelectItem>
<SelectItem value="InProgress">In Progress</SelectItem>
<SelectItem value="Done">Done</SelectItem>
</SelectContent>
</Select>
<Select value={filterPriority} onValueChange={(v) => setFilterPriority(v as FilterPriority)}>
<SelectTrigger className="w-[140px] h-8">
<SelectValue placeholder="Priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Priority</SelectItem>
<SelectItem value="Low">Low</SelectItem>
<SelectItem value="Medium">Medium</SelectItem>
<SelectItem value="High">High</SelectItem>
<SelectItem value="Critical">Critical</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center gap-2 ml-auto">
<ArrowUpDown className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Sort:</span>
<Select value={sortBy} onValueChange={(v) => setSortBy(v as SortOption)}>
<SelectTrigger className="w-[140px] h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="priority">Priority</SelectItem>
<SelectItem value="status">Status</SelectItem>
<SelectItem value="createdAt">Created Date</SelectItem>
<SelectItem value="assignee">Assignee</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
{/* Add Task Form */}
{showAddForm && (
<Card className="p-4 border-primary">
<TaskForm
storyId={storyId}
onSuccess={() => setShowAddForm(false)}
onCancel={() => setShowAddForm(false)}
/>
</Card>
)}
{/* Task List */}
{filteredAndSortedTasks.length > 0 ? (
<div className="space-y-2">
{filteredAndSortedTasks.map((task) => (
<TaskCard key={task.id} task={task} readonly={readonly} />
))}
</div>
) : (
<EmptyState
hasFilters={filterStatus !== 'All' || filterPriority !== 'All'}
onReset={() => {
setFilterStatus('All');
setFilterPriority('All');
}}
onAddTask={() => setShowAddForm(true)}
readonly={readonly}
/>
)}
</div>
);
}
// 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 (
<Card className="p-8 text-center">
<p className="text-sm text-muted-foreground mb-4">
No Tasks match the selected filters.
</p>
<Button onClick={onReset} variant="outline" size="sm">
Reset Filters
</Button>
</Card>
);
}
return (
<Card className="p-8 text-center">
<div className="text-4xl mb-4">☑️</div>
<h3 className="text-lg font-semibold mb-2">No Tasks Yet</h3>
<p className="text-sm text-muted-foreground mb-4">
Break down this Story into technical Tasks to track implementation progress.
</p>
{!readonly && (
<Button onClick={onAddTask} size="sm">
<Plus className="h-4 w-4 mr-2" />
Add Your First Task
</Button>
)}
</Card>
);
}
// Loading skeleton
function TaskListSkeleton() {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-9 w-24" />
</div>
<Skeleton className="h-8 w-full" />
<div className="space-y-2">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
</div>
</div>
);
}
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:
- Navigate to Story detail page
- Verify Task count badge shows correctly (e.g., "8 total, 3 done")
- Click "Add Task" → Verify inline form appears
- Create 5 Tasks with different statuses and priorities
- Test status filter → Select "Done" → Verify only Done Tasks show
- Test priority filter → Select "High" → Verify only High priority Tasks show
- Test combined filters → Status: InProgress + Priority: High
- Test sorting → Sort by Priority → Verify High/Critical at top
- Test sorting → Sort by Status → Verify Backlog → Todo → InProgress → Done
- Reset filters → Verify all Tasks show again
- Test empty state (Story with no Tasks) → Verify helpful message
Unit Test:
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(<TaskList storyId="story-123" />);
expect(screen.getByText('3 total')).toBeInTheDocument();
expect(screen.getByText('1 done')).toBeInTheDocument();
});
it('filters tasks by status', () => {
render(<TaskList storyId="story-123" />);
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(<TaskList storyId="story-123" />);
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.