Files
ColaFlow/docs/plans/sprint_4_story_2_task_3.md
Yaojia Wang 88d6413f81 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>
2025-11-05 21:49:57 +01:00

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

  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

// 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:

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.