feat(frontend): Create Sprint 4 Stories and Tasks for Story Management

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>
This commit is contained in:
Yaojia Wang
2025-11-05 21:49:57 +01:00
parent b3c92042ed
commit 88d6413f81
18 changed files with 3629 additions and 0 deletions

View File

@@ -0,0 +1,402 @@
---
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<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**:
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(<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.