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,223 @@
---
story_id: sprint_4_story_1
sprint: sprint_4
priority: P0
status: not_started
story_points: 5
estimated_days: 3
created_date: 2025-11-05
assignee: Frontend Team
---
# Story 1: Story Detail Page Foundation
**Sprint**: Sprint 4
**Priority**: P0 (Critical)
**Estimated**: 3 days
**Owner**: Frontend Team
## Description
Create the Story detail page (`/stories/[id]`) to fix the critical 404 error when users click on Story cards. This page will display comprehensive Story information including title, description, status, priority, metadata, and parent Epic context using a two-column layout consistent with the existing Epic detail page.
## User Story
**As a** project manager or developer,
**I want** to view detailed Story information by clicking on a Story card,
**So that** I can see the full Story description, acceptance criteria, Tasks, and manage Story metadata.
## Acceptance Criteria
- [ ] Clicking Story card from Epic detail page navigates to `/stories/{id}` page (no 404 error)
- [ ] Page displays Story title, description, status, priority, and all metadata
- [ ] Two-column layout with main content area and metadata sidebar
- [ ] Breadcrumb navigation shows: Projects > Project > Epics > Epic > Stories > Story
- [ ] Metadata sidebar shows status, priority, assignee, time tracking, dates, and parent Epic card
- [ ] Edit button opens Story form dialog with current Story data
- [ ] Delete button shows confirmation dialog and removes Story
- [ ] Loading states display skeleton loaders
- [ ] Error states handle 404, network errors, and permission errors
- [ ] Responsive design works on mobile, tablet, and desktop
- [ ] Back button (or ESC key) navigates to parent Epic detail page
## Technical Requirements
**New Route**:
- `app/(dashboard)/stories/[id]/page.tsx` - Story detail page (400-500 lines)
**New Components**:
- `components/projects/story-header.tsx` - Story page header (100-150 lines)
- `components/projects/story-metadata-sidebar.tsx` - Story sidebar (150-200 lines)
**Reuse Pattern**: 85% similar to Epic detail page
- Reference: `app/(dashboard)/epics/[id]/page.tsx` (534 lines)
- Adapt Epic structure for Story context
- Replace Stories list with Tasks list (Story 2 dependency)
**API Integration**:
- Use `useStory(id)` hook (already exists)
- Use `useUpdateStory()` hook for Edit action
- Use `useDeleteStory()` hook for Delete action
**Layout Structure**:
```
┌────────────────────────────────────────────────────────┐
│ [Breadcrumb Navigation] [Actions] │
├────────────────────────────────────────────────────────┤
│ [←] Story Title [Edit][Delete] │
│ [Status Badge] [Priority Badge] │
├────────────────────────────────────────────────────────┤
│ ┌────────────────────────┐ ┌──────────────────────┐ │
│ │ Main Content │ │ Metadata Sidebar │ │
│ │ - Description │ │ - Status │ │
│ │ - Acceptance Criteria │ │ - Priority │ │
│ │ - Tasks (Story 2) │ │ - Assignee │ │
│ │ │ │ - Time Tracking │ │
│ │ │ │ - Dates │ │
│ │ │ │ - Parent Epic Card │ │
│ └────────────────────────┘ └──────────────────────┘ │
└────────────────────────────────────────────────────────┘
```
## Tasks
- [ ] [Task 1](sprint_4_story_1_task_1.md) - Create Story detail page route and layout
- [ ] [Task 2](sprint_4_story_1_task_2.md) - Implement Story header component
- [ ] [Task 3](sprint_4_story_1_task_3.md) - Implement Story metadata sidebar component
- [ ] [Task 4](sprint_4_story_1_task_4.md) - Integrate Story API data loading and error handling
- [ ] [Task 5](sprint_4_story_1_task_5.md) - Add Edit and Delete actions with dialogs
- [ ] [Task 6](sprint_4_story_1_task_6.md) - Implement responsive design and accessibility
**Progress**: 0/6 tasks completed
## Dependencies
**Prerequisites**:
- ✅ Story API ready (`GET /api/v1/stories/{id}`)
-`useStory(id)` hook implemented (`lib/hooks/use-stories.ts`)
- ✅ Story types defined (`types/project.ts`)
- ✅ Story form component exists (`components/projects/story-form.tsx`)
- ✅ Epic detail page as reference pattern
**Blocked By**: None (can start immediately)
**Blocks**:
- Story 2 (Task Management) - depends on Story detail page existing
## Definition of Done
- All 6 tasks completed
- Story detail page accessible at `/stories/{id}` route
- Page displays all Story information correctly
- Layout consistent with Epic detail page
- Edit and Delete actions working
- Loading and error states implemented
- Responsive design tested on all breakpoints
- Accessibility requirements met (keyboard navigation, ARIA labels)
- Code reviewed and approved
- Git commit created with descriptive message
## Test Plan
**Manual Testing**:
1. Navigate to Epic detail page
2. Click any Story card → Verify navigates to `/stories/{id}` (no 404)
3. Verify Story title, description, status, priority display correctly
4. Verify breadcrumb navigation shows full hierarchy
5. Verify metadata sidebar shows assignee, time, dates, parent Epic
6. Click Edit → Verify form opens with Story data
7. Edit Story → Verify changes saved and reflected
8. Click Delete → Verify confirmation dialog appears
9. Delete Story → Verify redirected to Epic detail page
10. Test invalid Story ID → Verify 404 error page
11. Test mobile/tablet/desktop responsive layouts
12. Test keyboard navigation (Tab, Enter, ESC)
**E2E Test** (Playwright):
```typescript
test('user can view story details', async ({ page }) => {
await page.goto('/epics/epic-123');
await page.click('[data-testid="story-card"]');
await expect(page).toHaveURL(/\/stories\/story-\w+/);
await expect(page.locator('h1')).toContainText('Story Title');
await expect(page.locator('[data-testid="story-status"]')).toBeVisible();
});
```
**Verification Commands**:
```bash
# Check route exists
ls app/(dashboard)/stories/[id]/page.tsx
# Run dev server and test
npm run dev
# Navigate to http://localhost:3000/stories/[valid-id]
# Build for production
npm run build
# Verify no build errors
```
## UX Design Reference
**Design Document**: `docs/designs/STORY_UX_UI_DESIGN.md`
- Section: "Story Detail Page Design" (lines 117-164)
- Layout: Two-column (main content + metadata sidebar)
- Breadcrumb: Projects > Project > Epics > Epic > Stories > Story
- Header: Title, Status, Priority, Edit, Delete actions
- Sidebar: Metadata fields and parent Epic card
**Design Tokens**:
```css
/* Status Colors */
Backlog: bg-slate-100 text-slate-700
Todo: bg-blue-100 text-blue-700
InProgress: bg-amber-100 text-amber-700
Done: bg-green-100 text-green-700
/* Priority Colors */
Low: bg-blue-100 text-blue-700
Medium: bg-yellow-100 text-yellow-700
High: bg-orange-100 text-orange-700
Critical: bg-red-100 text-red-700
/* Typography */
Story Title: 32px, Bold, Line-height 1.2
Story Description: 16px, Regular, Line-height 1.6
Metadata Label: 14px, Medium
Metadata Value: 14px, Regular
```
## Notes
**Why This Matters**:
- **Critical Bug**: Users currently see 404 error when clicking Story cards
- **User Experience**: Enables Epic → Story → Task navigation
- **Foundation**: Required for Story 2 (Task Management)
- **Consistency**: Maintains design patterns established by Epic detail page
**Code Reuse Strategy**:
- Copy `app/(dashboard)/epics/[id]/page.tsx` structure
- Replace `useEpic``useStory`
- Replace `useStories``useTasks` (Story 2)
- Update breadcrumb (add Story level)
- Adapt sidebar (Parent Project → Parent Epic)
- 85% code reuse, 50-60% faster development
**Performance Targets**:
- Page load time: < 1 second
- Time to interactive: < 2 seconds
- Lighthouse score: >= 90
- No layout shift (CLS < 0.1)
**Accessibility Requirements** (WCAG 2.1 Level AA):
- Keyboard navigation: All interactive elements
- Focus indicators: 2px solid outline
- ARIA labels: All buttons and links
- Screen reader: Proper heading hierarchy (h1 h2 h3)
- Color contrast: 4.5:1 minimum for text
---
**Created**: 2025-11-05 by Frontend Agent
**Updated**: 2025-11-05

View File

@@ -0,0 +1,152 @@
---
task_id: sprint_4_story_1_task_1
story_id: sprint_4_story_1
sprint_id: sprint_4
status: not_started
type: frontend
assignee: Frontend Developer 1
created_date: 2025-11-05
estimated_hours: 4
---
# Task 1: Create Story Detail Page Route and Layout
## Description
Create the Story detail page route at `app/(dashboard)/stories/[id]/page.tsx` with a two-column layout structure. This task establishes the foundational page structure that will be enhanced with components in subsequent tasks.
## What to Do
1. Create new directory: `app/(dashboard)/stories/[id]/`
2. Create page component: `page.tsx`
3. Copy layout structure from Epic detail page (`app/(dashboard)/epics/[id]/page.tsx`)
4. Implement two-column grid layout (main content + sidebar)
5. Add basic page wrapper and container
6. Set up TypeScript types and props
7. Test route accessibility
**Key Changes from Epic Page**:
- Route: `/stories/[id]` instead of `/epics/[id]`
- Page title: "Story Detail" instead of "Epic Detail"
- Breadcrumb: Add Story level in hierarchy
- Prepare sections: Description, Acceptance Criteria (placeholder), Tasks (placeholder for Story 2)
## Files to Modify/Create
- `app/(dashboard)/stories/[id]/page.tsx` (new, ~100 lines initial structure)
## Implementation Details
```typescript
// app/(dashboard)/stories/[id]/page.tsx
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import { Card } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
interface StoryDetailPageProps {
params: { id: string };
}
export default function StoryDetailPage({ params }: StoryDetailPageProps) {
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
{/* Breadcrumb Navigation - Task 1 */}
<div className="mb-4">
{/* Placeholder for breadcrumb */}
</div>
{/* Story Header - Task 2 */}
<div className="mb-6">
{/* Placeholder for story header */}
</div>
{/* Two-column layout */}
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6">
{/* Main Content Column */}
<div className="space-y-6">
{/* Story Description Card */}
<Card className="p-6">
{/* Placeholder for description - Task 4 */}
</Card>
{/* Acceptance Criteria Card */}
<Card className="p-6">
{/* Placeholder for acceptance criteria - Story 3 */}
</Card>
{/* Tasks Section - Story 2 dependency */}
<Card className="p-6">
{/* Placeholder for tasks list - Story 2 */}
</Card>
</div>
{/* Metadata Sidebar Column - Task 3 */}
<aside>
{/* Placeholder for metadata sidebar */}
</aside>
</div>
</div>
);
}
// Loading state
export function StoryDetailPageSkeleton() {
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
<Skeleton className="h-4 w-64 mb-4" />
<Skeleton className="h-12 w-96 mb-6" />
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6">
<div className="space-y-6">
<Skeleton className="h-48 w-full" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-64 w-full" />
</div>
<Skeleton className="h-96 w-full" />
</div>
</div>
);
}
```
## Acceptance Criteria
- [ ] Route `/stories/[id]` is accessible (no 404 error)
- [ ] Page renders with two-column layout on desktop
- [ ] Layout is responsive (single column on mobile)
- [ ] TypeScript types are properly defined
- [ ] No console errors or warnings
- [ ] Loading skeleton displays during data fetch
## Testing
```bash
# Start dev server
npm run dev
# Test route
# Navigate to: http://localhost:3000/stories/[any-valid-story-id]
# Should see basic page structure (not 404)
# Test responsive layout
# Resize browser window
# Desktop (> 1024px): Two columns
# Mobile (< 1024px): Single column
```
## Dependencies
**Prerequisites**:
- ✅ Next.js 15 app directory structure
- ✅ shadcn/ui Card and Skeleton components
**Blocks**:
- Task 2, 3, 4, 5, 6 (all depend on page structure existing)
## Estimated Time
4 hours
## Notes
This task creates the "skeleton" of the Story detail page. Subsequent tasks will fill in the components and functionality. Keep the structure simple and focus on layout correctness.

View File

@@ -0,0 +1,241 @@
---
task_id: sprint_4_story_1_task_2
story_id: sprint_4_story_1
sprint_id: sprint_4
status: not_started
type: frontend
assignee: Frontend Developer 1
created_date: 2025-11-05
estimated_hours: 3
---
# Task 2: Implement Story Header Component
## Description
Create the Story header component that displays the Story title, status badge, priority badge, and action buttons (Edit, Delete). This header sits at the top of the Story detail page and includes a back button for navigation.
## What to Do
1. Create `components/projects/story-header.tsx`
2. Display Story title (32px, bold)
3. Add back button (navigates to parent Epic)
4. Display status badge with color coding
5. Display priority badge with color coding
6. Add Edit button (opens Story form dialog)
7. Add Delete button (opens confirmation dialog)
8. Implement responsive layout for mobile
9. Add keyboard navigation (ESC key for back)
## Files to Create/Modify
- `components/projects/story-header.tsx` (new, ~100-150 lines)
- `app/(dashboard)/stories/[id]/page.tsx` (modify, integrate header)
## Implementation Details
```typescript
// components/projects/story-header.tsx
'use client';
import { useRouter } from 'next/navigation';
import { ArrowLeft, Edit, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import type { Story } from '@/types/project';
interface StoryHeaderProps {
story: Story;
onEdit: () => void;
onDelete: () => void;
}
export function StoryHeader({ story, onEdit, onDelete }: StoryHeaderProps) {
const router = useRouter();
const handleBack = () => {
router.push(`/epics/${story.epicId}`);
};
// Keyboard shortcut: ESC to go back
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
handleBack();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
return (
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex items-start gap-4">
{/* Back Button */}
<Button
variant="ghost"
size="icon"
onClick={handleBack}
aria-label="Back to Epic"
>
<ArrowLeft className="h-5 w-5" />
</Button>
{/* Title and Badges */}
<div className="flex-1">
<h1 className="text-3xl font-bold tracking-tight mb-2 line-clamp-2">
{story.title}
</h1>
<div className="flex items-center gap-2 flex-wrap">
{/* Status Badge */}
<Badge variant={getStatusVariant(story.status)}>
{story.status}
</Badge>
{/* Priority Badge */}
<Badge variant={getPriorityVariant(story.priority)}>
{story.priority}
</Badge>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={onEdit}
aria-label="Edit Story"
>
<Edit className="h-4 w-4 mr-2" />
Edit
</Button>
<Button
variant="destructive"
size="sm"
onClick={onDelete}
aria-label="Delete Story"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</div>
);
}
// Helper functions for badge variants
function getStatusVariant(status: string) {
const variants = {
Backlog: 'secondary',
Todo: 'default',
InProgress: 'warning',
Done: 'success',
};
return variants[status] || 'default';
}
function getPriorityVariant(priority: string) {
const variants = {
Low: 'default',
Medium: 'warning',
High: 'destructive',
Critical: 'destructive',
};
return variants[priority] || 'default';
}
```
**Integration in page.tsx**:
```typescript
// app/(dashboard)/stories/[id]/page.tsx
import { StoryHeader } from '@/components/projects/story-header';
// Inside component
<StoryHeader
story={story}
onEdit={() => setIsEditDialogOpen(true)}
onDelete={() => setIsDeleteDialogOpen(true)}
/>
```
## Acceptance Criteria
- [ ] Header displays Story title (max 2 lines, ellipsis overflow)
- [ ] Back button navigates to parent Epic detail page
- [ ] ESC key also navigates back
- [ ] Status badge shows correct color (Backlog/Todo/InProgress/Done)
- [ ] Priority badge shows correct color (Low/Medium/High/Critical)
- [ ] Edit button opens Story form dialog (handler passed from page)
- [ ] Delete button opens confirmation dialog (handler passed from page)
- [ ] Responsive layout: Stack vertically on mobile
- [ ] All buttons have proper ARIA labels
- [ ] Keyboard accessible (Tab navigation works)
## Testing
**Manual Testing**:
1. View Story detail page
2. Verify title displays correctly
3. Click back button → Navigates to Epic detail
4. Press ESC key → Navigates to Epic detail
5. Verify status badge color matches status value
6. Verify priority badge color matches priority value
7. Click Edit button → Triggers onEdit callback
8. Click Delete button → Triggers onDelete callback
9. Test on mobile → Buttons stack vertically
**Unit Test**:
```typescript
import { render, screen, fireEvent } from '@testing-library/react';
import { StoryHeader } from './story-header';
describe('StoryHeader', () => {
const mockStory = {
id: '123',
title: 'Test Story',
status: 'InProgress',
priority: 'High',
epicId: 'epic-123',
};
it('renders story title', () => {
render(<StoryHeader story={mockStory} onEdit={jest.fn()} onDelete={jest.fn()} />);
expect(screen.getByText('Test Story')).toBeInTheDocument();
});
it('calls onEdit when edit button clicked', () => {
const onEdit = jest.fn();
render(<StoryHeader story={mockStory} onEdit={onEdit} onDelete={jest.fn()} />);
fireEvent.click(screen.getByLabelText('Edit Story'));
expect(onEdit).toHaveBeenCalled();
});
it('calls onDelete when delete button clicked', () => {
const onDelete = jest.fn();
render(<StoryHeader story={mockStory} onEdit={jest.fn()} onDelete={onDelete} />);
fireEvent.click(screen.getByLabelText('Delete Story'));
expect(onDelete).toHaveBeenCalled();
});
});
```
## Dependencies
**Prerequisites**:
- Task 1 (page structure must exist)
- ✅ shadcn/ui Button, Badge components
**Blocks**:
- Task 5 (Edit/Delete dialogs need header buttons)
## Estimated Time
3 hours
## Notes
Reuse badge color logic from Epic header component if it exists. Keep the component simple and focused - dialogs are handled in Task 5.

View File

@@ -0,0 +1,298 @@
---
task_id: sprint_4_story_1_task_3
story_id: sprint_4_story_1
sprint_id: sprint_4
status: not_started
type: frontend
assignee: Frontend Developer 1
created_date: 2025-11-05
estimated_hours: 4
---
# Task 3: Implement Story Metadata Sidebar Component
## Description
Create the metadata sidebar component that displays Story status, priority, assignee, time tracking, dates, and a parent Epic card. This sidebar provides quick access to Story metadata and context.
## What to Do
1. Create `components/projects/story-metadata-sidebar.tsx`
2. Display Status dropdown (quick status change)
3. Display Priority dropdown (quick priority change)
4. Display Assignee with avatar and change button
5. Display Time Tracking (Estimated vs Actual hours, progress bar)
6. Display Dates (Created, Updated with relative time)
7. Display Parent Epic card (title, status, progress, link)
8. Add responsive design (sidebar moves to top on mobile)
9. Integrate with Story update hooks
## Files to Create/Modify
- `components/projects/story-metadata-sidebar.tsx` (new, ~150-200 lines)
- `components/projects/epic-card-compact.tsx` (optional, for parent Epic display)
- `app/(dashboard)/stories/[id]/page.tsx` (modify, integrate sidebar)
## Implementation Details
```typescript
// components/projects/story-metadata-sidebar.tsx
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { Calendar, Clock, User } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Progress } from '@/components/ui/progress';
import type { Story, Epic } from '@/types/project';
import { useChangeStoryStatus, useAssignStory } from '@/lib/hooks/use-stories';
import { formatDistanceToNow } from 'date-fns';
interface StoryMetadataSidebarProps {
story: Story;
parentEpic?: Epic;
}
export function StoryMetadataSidebar({ story, parentEpic }: StoryMetadataSidebarProps) {
const changeStatus = useChangeStoryStatus();
const assignStory = useAssignStory();
const handleStatusChange = async (newStatus: string) => {
await changeStatus.mutateAsync({ storyId: story.id, status: newStatus });
};
const completionPercentage = story.actualHours && story.estimatedHours
? Math.min(100, (story.actualHours / story.estimatedHours) * 100)
: 0;
return (
<div className="space-y-6">
{/* Status Section */}
<Card className="p-4">
<h3 className="text-sm font-medium mb-3">Status</h3>
<Select
value={story.status}
onValueChange={handleStatusChange}
disabled={changeStatus.isPending}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Backlog">Backlog</SelectItem>
<SelectItem value="Todo">Todo</SelectItem>
<SelectItem value="InProgress">In Progress</SelectItem>
<SelectItem value="Done">Done</SelectItem>
</SelectContent>
</Select>
</Card>
{/* Priority Section */}
<Card className="p-4">
<h3 className="text-sm font-medium mb-3">Priority</h3>
<Badge variant={getPriorityVariant(story.priority)} className="w-full justify-center">
{story.priority}
</Badge>
</Card>
{/* Assignee Section */}
<Card className="p-4">
<h3 className="text-sm font-medium mb-3">Assignee</h3>
{story.assigneeId ? (
<div className="flex items-center gap-3">
<Avatar className="h-8 w-8">
<AvatarImage src={`/avatars/${story.assigneeId}.png`} />
<AvatarFallback>
<User className="h-4 w-4" />
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="text-sm font-medium">Assignee Name</p>
<Button variant="link" size="sm" className="h-auto p-0 text-xs">
Change
</Button>
</div>
</div>
) : (
<Button variant="outline" size="sm" className="w-full">
<User className="h-4 w-4 mr-2" />
Assign
</Button>
)}
</Card>
{/* Time Tracking Section */}
<Card className="p-4">
<h3 className="text-sm font-medium mb-3">Time Tracking</h3>
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Estimated</span>
<span className="font-medium">{story.estimatedHours || 0}h</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Actual</span>
<span className="font-medium">{story.actualHours || 0}h</span>
</div>
<Progress value={completionPercentage} className="h-2" />
<p className="text-xs text-muted-foreground text-center">
{Math.round(completionPercentage)}% complete
</p>
</div>
</Card>
{/* Dates Section */}
<Card className="p-4">
<h3 className="text-sm font-medium mb-3">Dates</h3>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<Calendar className="h-4 w-4" />
<span>Created {formatDistanceToNow(new Date(story.createdAt), { addSuffix: true })}</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<Clock className="h-4 w-4" />
<span>Updated {formatDistanceToNow(new Date(story.updatedAt), { addSuffix: true })}</span>
</div>
</div>
</Card>
{/* Parent Epic Section */}
{parentEpic && (
<Card className="p-4">
<h3 className="text-sm font-medium mb-3">Parent Epic</h3>
<Link
href={`/epics/${parentEpic.id}`}
className="block p-3 rounded-lg border hover:bg-accent transition-colors"
>
<div className="flex items-start justify-between mb-2">
<h4 className="font-medium line-clamp-1">{parentEpic.title}</h4>
<Badge variant="outline" className="text-xs">
{parentEpic.status}
</Badge>
</div>
<p className="text-xs text-muted-foreground">
View Epic Details
</p>
</Link>
</Card>
)}
</div>
);
}
function getPriorityVariant(priority: string) {
const variants = {
Low: 'default',
Medium: 'warning',
High: 'destructive',
Critical: 'destructive',
};
return variants[priority] || 'default';
}
```
**Integration in page.tsx**:
```typescript
// app/(dashboard)/stories/[id]/page.tsx
import { StoryMetadataSidebar } from '@/components/projects/story-metadata-sidebar';
import { useEpic } from '@/lib/hooks/use-epics';
// Inside component
const { data: story } = useStory(params.id);
const { data: parentEpic } = useEpic(story?.epicId);
<StoryMetadataSidebar story={story} parentEpic={parentEpic} />
```
## Acceptance Criteria
- [ ] Sidebar displays all metadata sections (Status, Priority, Assignee, Time, Dates, Parent Epic)
- [ ] Status dropdown allows quick status changes
- [ ] Status changes update immediately (optimistic UI)
- [ ] Priority badge shows correct color
- [ ] Assignee section shows avatar and name (if assigned)
- [ ] Time tracking shows estimated/actual hours and progress bar
- [ ] Progress bar correctly calculates percentage
- [ ] Dates display in relative format ("2 hours ago", "3 days ago")
- [ ] Parent Epic card is clickable and navigates to Epic detail
- [ ] Parent Epic card shows Epic status and title
- [ ] Responsive: Sidebar moves above content on mobile
- [ ] Loading states show skeleton loaders
## Testing
**Manual Testing**:
1. View Story detail page
2. Verify all metadata sections display correctly
3. Change status via dropdown → Verify updates immediately
4. Verify priority badge color matches priority
5. Verify assignee displays correctly (if assigned)
6. Verify time tracking shows correct hours and percentage
7. Verify dates are in relative format
8. Click parent Epic card → Navigates to Epic detail page
9. Test on mobile → Sidebar moves above main content
**Unit Test**:
```typescript
import { render, screen } from '@testing-library/react';
import { StoryMetadataSidebar } from './story-metadata-sidebar';
describe('StoryMetadataSidebar', () => {
const mockStory = {
id: '123',
status: 'InProgress',
priority: 'High',
assigneeId: 'user-1',
estimatedHours: 16,
actualHours: 8,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const mockEpic = {
id: 'epic-1',
title: 'Parent Epic',
status: 'InProgress',
};
it('renders all metadata sections', () => {
render(<StoryMetadataSidebar story={mockStory} parentEpic={mockEpic} />);
expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByText('Priority')).toBeInTheDocument();
expect(screen.getByText('Assignee')).toBeInTheDocument();
expect(screen.getByText('Time Tracking')).toBeInTheDocument();
expect(screen.getByText('Dates')).toBeInTheDocument();
expect(screen.getByText('Parent Epic')).toBeInTheDocument();
});
it('calculates time tracking percentage correctly', () => {
render(<StoryMetadataSidebar story={mockStory} parentEpic={mockEpic} />);
expect(screen.getByText('50% complete')).toBeInTheDocument();
});
});
```
## Dependencies
**Prerequisites**:
- Task 1 (page structure must exist)
-`useChangeStoryStatus()` hook (already exists)
-`useAssignStory()` hook (already exists)
- ✅ shadcn/ui Card, Badge, Select, Avatar, Progress components
- ✅ date-fns for relative time formatting
**Blocks**:
- None (independent component)
## Estimated Time
4 hours
## Notes
**Code Reuse**: Copy sidebar structure from Epic detail page if similar metadata sidebar exists. The parent Epic card is similar to showing a parent Project card in Epic sidebar - reuse that pattern.
**Future Enhancement**: Assignee selector will be enhanced in Story 3 (Enhanced Story Form).

View File

@@ -0,0 +1,325 @@
---
task_id: sprint_4_story_1_task_4
story_id: sprint_4_story_1
sprint_id: sprint_4
status: not_started
type: frontend
assignee: Frontend Developer 1
created_date: 2025-11-05
estimated_hours: 3
---
# Task 4: Integrate Story API Data Loading and Error Handling
## Description
Connect the Story detail page to the backend API using React Query hooks. Implement data loading, error handling, 404 detection, and loading states with skeleton loaders.
## What to Do
1. Use `useStory(id)` hook to fetch Story data
2. Use `useEpic(epicId)` hook to fetch parent Epic data
3. Implement loading states with skeleton loaders
4. Handle 404 errors (Story not found)
5. Handle network errors
6. Handle permission errors (unauthorized)
7. Add breadcrumb navigation with data
8. Display Story description in main content area
9. Test with various Story IDs (valid, invalid, deleted)
## Files to Modify
- `app/(dashboard)/stories/[id]/page.tsx` (modify, ~50 lines added)
## Implementation Details
```typescript
// app/(dashboard)/stories/[id]/page.tsx
'use client';
import { use } from 'react';
import { notFound } from 'next/navigation';
import { useStory } from '@/lib/hooks/use-stories';
import { useEpic } from '@/lib/hooks/use-epics';
import { StoryHeader } from '@/components/projects/story-header';
import { StoryMetadataSidebar } from '@/components/projects/story-metadata-sidebar';
import { Card } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle } from 'lucide-react';
interface StoryDetailPageProps {
params: Promise<{ id: string }>;
}
export default function StoryDetailPage({ params }: StoryDetailPageProps) {
const { id } = use(params);
// Fetch Story data
const {
data: story,
isLoading: storyLoading,
error: storyError,
} = useStory(id);
// Fetch parent Epic data
const {
data: parentEpic,
isLoading: epicLoading,
} = useEpic(story?.epicId, {
enabled: !!story?.epicId, // Only fetch if Story loaded
});
// Handle loading state
if (storyLoading || epicLoading) {
return <StoryDetailPageSkeleton />;
}
// Handle 404 - Story not found
if (storyError?.response?.status === 404) {
notFound();
}
// Handle other errors (network, permission, etc.)
if (storyError || !story) {
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{storyError?.message || 'Failed to load Story. Please try again.'}
</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
{/* Breadcrumb Navigation */}
<nav className="mb-4 text-sm text-muted-foreground" aria-label="Breadcrumb">
<ol className="flex items-center gap-2">
<li><a href="/projects" className="hover:text-foreground">Projects</a></li>
<li>/</li>
<li><a href={`/projects/${story.projectId}`} className="hover:text-foreground">Project</a></li>
<li>/</li>
<li><a href="/epics" className="hover:text-foreground">Epics</a></li>
<li>/</li>
{parentEpic && (
<>
<li>
<a href={`/epics/${parentEpic.id}`} className="hover:text-foreground">
{parentEpic.title}
</a>
</li>
<li>/</li>
</>
)}
<li><a href={`/epics/${story.epicId}/stories`} className="hover:text-foreground">Stories</a></li>
<li>/</li>
<li className="font-medium text-foreground" aria-current="page">{story.title}</li>
</ol>
</nav>
{/* Story Header */}
<StoryHeader
story={story}
onEdit={() => setIsEditDialogOpen(true)}
onDelete={() => setIsDeleteDialogOpen(true)}
/>
{/* Two-column layout */}
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6 mt-6">
{/* Main Content Column */}
<div className="space-y-6">
{/* Story Description Card */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Description</h2>
{story.description ? (
<div className="prose prose-sm max-w-none">
<p className="whitespace-pre-wrap">{story.description}</p>
</div>
) : (
<p className="text-sm text-muted-foreground italic">No description provided.</p>
)}
</Card>
{/* Acceptance Criteria Card - Story 3 */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Acceptance Criteria</h2>
<p className="text-sm text-muted-foreground italic">
Acceptance criteria will be added in Story 3.
</p>
</Card>
{/* Tasks Section - Story 2 */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Tasks</h2>
<p className="text-sm text-muted-foreground italic">
Task list will be added in Story 2.
</p>
</Card>
</div>
{/* Metadata Sidebar Column */}
<aside>
<StoryMetadataSidebar story={story} parentEpic={parentEpic} />
</aside>
</div>
</div>
);
}
// Loading skeleton
function StoryDetailPageSkeleton() {
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
<Skeleton className="h-4 w-64 mb-4" />
<Skeleton className="h-12 w-96 mb-6" />
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6">
<div className="space-y-6">
<Card className="p-6">
<Skeleton className="h-6 w-32 mb-4" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-3/4" />
</Card>
<Card className="p-6">
<Skeleton className="h-6 w-48 mb-4" />
<Skeleton className="h-20 w-full" />
</Card>
<Card className="p-6">
<Skeleton className="h-6 w-24 mb-4" />
<Skeleton className="h-32 w-full" />
</Card>
</div>
<aside className="space-y-6">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-24 w-full" />
</aside>
</div>
</div>
);
}
```
**404 Page** (optional, create if doesn't exist):
```typescript
// app/(dashboard)/stories/[id]/not-found.tsx
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { FileQuestion } from 'lucide-react';
export default function StoryNotFound() {
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
<Card className="p-12 text-center">
<FileQuestion className="h-16 w-16 mx-auto mb-4 text-muted-foreground" />
<h1 className="text-2xl font-bold mb-2">Story Not Found</h1>
<p className="text-muted-foreground mb-6">
The Story you're looking for doesn't exist or you don't have permission to view it.
</p>
<div className="flex justify-center gap-4">
<Button variant="outline" asChild>
<Link href="/epics">View All Epics</Link>
</Button>
<Button asChild>
<Link href="/projects">Go to Projects</Link>
</Button>
</div>
</Card>
</div>
);
}
```
## Acceptance Criteria
- [ ] Page fetches Story data using `useStory(id)` hook
- [ ] Page fetches parent Epic data using `useEpic(epicId)` hook
- [ ] Loading state displays skeleton loaders
- [ ] 404 error handled (Story not found → shows custom 404 page)
- [ ] Network errors display error message with retry option
- [ ] Permission errors display appropriate message
- [ ] Breadcrumb navigation shows full hierarchy with clickable links
- [ ] Story description displays correctly (supports line breaks)
- [ ] Empty description shows placeholder message
- [ ] All data populates components (header, sidebar)
- [ ] No console errors during data fetching
## Testing
**Manual Testing**:
1. Navigate to valid Story ID → Verify data loads correctly
2. Navigate to invalid Story ID → Verify 404 page shows
3. Disconnect internet → Navigate to Story → Verify error message
4. Verify skeleton loaders show during initial load
5. Verify breadcrumb links are clickable and navigate correctly
6. Verify Story description displays with line breaks preserved
7. Test with Story that has no description → Verify placeholder shows
**Test Cases**:
```bash
# Valid Story
/stories/[valid-story-id] → Should load Story details
# Invalid Story
/stories/invalid-id → Should show 404 page
# Deleted Story
/stories/[deleted-story-id] → Should show 404 page
# No permission
/stories/[other-tenant-story-id] → Should show error message
```
**E2E Test**:
```typescript
test('story detail page loads data correctly', async ({ page }) => {
await page.goto('/stories/story-123');
// Wait for data to load
await expect(page.locator('h1')).toContainText('Story Title');
// Verify metadata displays
await expect(page.locator('[data-testid="story-status"]')).toBeVisible();
await expect(page.locator('[data-testid="story-priority"]')).toBeVisible();
// Verify description
await expect(page.getByText('Description')).toBeVisible();
});
test('handles 404 for invalid story', async ({ page }) => {
await page.goto('/stories/invalid-id');
await expect(page.getByText('Story Not Found')).toBeVisible();
});
```
## Dependencies
**Prerequisites**:
- Task 1 (page structure must exist)
- Task 2 (header component must exist)
- Task 3 (sidebar component must exist)
-`useStory(id)` hook (already exists)
-`useEpic(id)` hook (already exists)
**Blocks**:
- Task 5 (Edit/Delete actions need data loaded)
## Estimated Time
3 hours
## Notes
**Error Handling Strategy**:
- 404 (Not Found) → Custom 404 page with helpful navigation
- 403 (Forbidden) → Error message with permission context
- 500 (Server Error) → Error message with retry button
- Network Error → Error message with offline indicator
**Performance**: Use React Query's built-in caching. Subsequent visits to the same Story load instantly from cache.

View File

@@ -0,0 +1,309 @@
---
task_id: sprint_4_story_1_task_5
story_id: sprint_4_story_1
sprint_id: sprint_4
status: not_started
type: frontend
assignee: Frontend Developer 1
created_date: 2025-11-05
estimated_hours: 3
---
# Task 5: Add Edit and Delete Actions with Dialogs
## Description
Implement Edit and Delete functionality for Stories in the detail page. Reuse existing Story form dialog for editing and create a confirmation dialog for deletion with cascade warning.
## What to Do
1. Add state management for dialog visibility (Edit dialog, Delete dialog)
2. Integrate existing Story form component for Edit action
3. Create Delete confirmation dialog
4. Show cascade warning if Story has Tasks (list affected Tasks)
5. Implement delete mutation with error handling
6. Add success/error toast notifications
7. Handle post-delete navigation (redirect to Epic detail page)
8. Test Edit and Delete flows
## Files to Modify
- `app/(dashboard)/stories/[id]/page.tsx` (modify, ~80 lines added for dialogs)
## Implementation Details
```typescript
// app/(dashboard)/stories/[id]/page.tsx
'use client';
import { use, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useStory, useUpdateStory, useDeleteStory } from '@/lib/hooks/use-stories';
import { useTasks } from '@/lib/hooks/use-tasks';
import { StoryForm } from '@/components/projects/story-form';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { Loader2, AlertTriangle } from 'lucide-react';
export default function StoryDetailPage({ params }: StoryDetailPageProps) {
const { id } = use(params);
const router = useRouter();
// Fetch data
const { data: story } = useStory(id);
const { data: tasks = [] } = useTasks(id);
// Dialog state
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
// Mutations
const updateStory = useUpdateStory();
const deleteStory = useDeleteStory();
// Edit Story handler
const handleEditStory = async (data: UpdateStoryDto) => {
try {
await updateStory.mutateAsync({ id: story.id, data });
setIsEditDialogOpen(false);
toast.success('Story updated successfully');
} catch (error) {
toast.error('Failed to update Story');
console.error(error);
}
};
// Delete Story handler
const handleDeleteStory = async () => {
try {
await deleteStory.mutateAsync(story.id);
toast.success('Story deleted successfully');
// Redirect to parent Epic detail page
router.push(`/epics/${story.epicId}`);
} catch (error) {
toast.error('Failed to delete Story');
console.error(error);
setIsDeleteDialogOpen(false);
}
};
return (
<div className="container mx-auto px-4 py-6 max-w-7xl">
{/* ... existing content ... */}
<StoryHeader
story={story}
onEdit={() => setIsEditDialogOpen(true)}
onDelete={() => setIsDeleteDialogOpen(true)}
/>
{/* ... rest of content ... */}
{/* Edit Story Dialog */}
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Story</DialogTitle>
<DialogDescription>
Update Story details. Changes will be saved immediately.
</DialogDescription>
</DialogHeader>
<StoryForm
mode="edit"
initialData={{
epicId: story.epicId,
projectId: story.projectId,
title: story.title,
description: story.description,
priority: story.priority,
estimatedHours: story.estimatedHours,
}}
onSubmit={handleEditStory}
onCancel={() => setIsEditDialogOpen(false)}
isSubmitting={updateStory.isPending}
/>
</DialogContent>
</Dialog>
{/* Delete Story Confirmation Dialog */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-destructive" />
Delete Story?
</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete <strong>{story?.title}</strong> and all associated data.
This action cannot be undone.
{/* Cascade warning if Tasks exist */}
{tasks.length > 0 && (
<div className="mt-4 p-3 bg-destructive/10 border border-destructive/20 rounded-md">
<p className="font-medium text-destructive mb-2">
⚠️ This Story has {tasks.length} Task{tasks.length > 1 ? 's' : ''} that will also be deleted:
</p>
<ul className="list-disc list-inside text-sm space-y-1">
{tasks.slice(0, 5).map((task) => (
<li key={task.id}>{task.title}</li>
))}
{tasks.length > 5 && (
<li className="italic">... and {tasks.length - 5} more</li>
)}
</ul>
</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleteStory.isPending}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteStory}
disabled={deleteStory.isPending}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{deleteStory.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Delete Story
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
```
## Acceptance Criteria
- [ ] Edit button opens Story form dialog with current Story data
- [ ] Story form displays in "edit" mode with pre-filled values
- [ ] Editing Story updates data correctly
- [ ] Edit success shows toast notification
- [ ] Edit error shows error toast
- [ ] Dialog closes after successful edit
- [ ] Delete button opens confirmation dialog
- [ ] Confirmation dialog shows Story title
- [ ] If Story has Tasks, shows cascade warning with Task list
- [ ] Delete action removes Story and navigates to Epic detail page
- [ ] Delete success shows toast notification
- [ ] Delete error shows error toast and keeps user on page
- [ ] Dialog buttons disabled during mutation (loading state)
- [ ] ESC key closes dialogs
- [ ] Outside click closes dialogs (configurable)
## Testing
**Manual Testing**:
**Edit Story Flow**:
1. Click Edit button → Verify dialog opens
2. Verify form pre-filled with current Story data
3. Change title → Save → Verify title updates on page
4. Change description → Save → Verify description updates
5. Change priority → Save → Verify priority badge updates
6. Test validation → Submit empty title → Verify error message
7. Click Cancel → Verify dialog closes without saving
8. Press ESC → Verify dialog closes
**Delete Story Flow**:
1. Click Delete button → Verify confirmation dialog opens
2. Verify Story title displayed in confirmation message
3. If Story has Tasks → Verify cascade warning shows with Task list
4. Click Cancel → Verify dialog closes, Story not deleted
5. Click Delete Story → Verify Story deleted
6. Verify redirected to Epic detail page
7. Verify success toast shown
8. Go to Epic page → Verify Story no longer in list
**Error Handling**:
1. Disconnect internet
2. Try to edit Story → Verify error toast shown
3. Try to delete Story → Verify error toast shown
4. Verify user stays on page after error
**E2E Test**:
```typescript
test('user can edit story', async ({ page }) => {
await page.goto('/stories/story-123');
// Open edit dialog
await page.click('[aria-label="Edit Story"]');
await expect(page.getByRole('dialog')).toBeVisible();
// Edit title
const titleInput = page.locator('[name="title"]');
await titleInput.fill('Updated Story Title');
// Save
await page.click('button:has-text("Save")');
// Verify updated
await expect(page.locator('h1')).toContainText('Updated Story Title');
await expect(page.getByText('Story updated successfully')).toBeVisible();
});
test('user can delete story', async ({ page }) => {
await page.goto('/stories/story-123');
// Open delete dialog
await page.click('[aria-label="Delete Story"]');
await expect(page.getByRole('alertdialog')).toBeVisible();
// Confirm delete
await page.click('button:has-text("Delete Story")');
// Verify redirected
await expect(page).toHaveURL(/\/epics\//);
await expect(page.getByText('Story deleted successfully')).toBeVisible();
});
```
## Dependencies
**Prerequisites**:
- Task 1, 2, 3, 4 (page, header, sidebar, data loading must exist)
- ✅ StoryForm component (already exists)
-`useUpdateStory()` hook (already exists)
-`useDeleteStory()` hook (already exists)
-`useTasks(storyId)` hook (for cascade warning)
- ✅ shadcn/ui Dialog, AlertDialog components
- ✅ sonner for toast notifications
**Blocks**:
- None (final task for Story 1)
## Estimated Time
3 hours
## Notes
**Cascade Warning**: Show list of Tasks that will be deleted when Story is deleted. This helps users understand the impact of deletion and prevents accidental data loss.
**Navigation After Delete**: Always redirect to the parent Epic detail page after successful deletion. This prevents users from staying on a deleted Story page.
**Form Reuse**: The StoryForm component already exists and handles both "create" and "edit" modes. No changes needed to the form itself.
**Error Handling**: Use optimistic UI for updates - update immediately, revert on error. For deletes, wait for confirmation before navigating.

View File

@@ -0,0 +1,403 @@
---
task_id: sprint_4_story_1_task_6
story_id: sprint_4_story_1
sprint_id: sprint_4
status: not_started
type: frontend
assignee: Frontend Developer 1
created_date: 2025-11-05
estimated_hours: 3
---
# Task 6: Implement Responsive Design and Accessibility
## Description
Ensure the Story detail page works flawlessly on all screen sizes (mobile, tablet, desktop) and meets WCAG 2.1 Level AA accessibility standards. This includes responsive layout adjustments, keyboard navigation, ARIA labels, and focus management.
## What to Do
1. Test and fix responsive layout on all breakpoints
2. Implement mobile-specific UI adjustments (sidebar to tabs)
3. Add keyboard navigation support (Tab, Enter, ESC)
4. Add ARIA labels to all interactive elements
5. Implement focus management (dialog open/close)
6. Ensure proper heading hierarchy (h1 → h2 → h3)
7. Test color contrast ratios (WCAG AA: 4.5:1 minimum)
8. Test with screen reader (NVDA/JAWS)
9. Add skip links for keyboard users
10. Run Lighthouse accessibility audit
## Files to Modify
- `app/(dashboard)/stories/[id]/page.tsx` (modify, responsive tweaks)
- `components/projects/story-header.tsx` (modify, accessibility)
- `components/projects/story-metadata-sidebar.tsx` (modify, responsive)
- All related components (add ARIA labels)
## Implementation Details
### Responsive Layout Breakpoints
```typescript
// app/(dashboard)/stories/[id]/page.tsx
// Adjust grid layout for different screens
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6">
{/* Main content - full width on mobile, 70% on desktop */}
<div className="space-y-6 order-2 lg:order-1">
{/* Story description, acceptance criteria, tasks */}
</div>
{/* Sidebar - full width on mobile (at top), fixed width on desktop */}
<aside className="order-1 lg:order-2">
{/* Metadata sidebar */}
</aside>
</div>
```
### Mobile-Specific Adjustments
```typescript
// Convert sidebar sections to tabs on mobile
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
function MobileStoryMetadata({ story, parentEpic }: Props) {
return (
<Tabs defaultValue="details" className="lg:hidden">
<TabsList className="w-full">
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="time">Time</TabsTrigger>
<TabsTrigger value="epic">Epic</TabsTrigger>
</TabsList>
<TabsContent value="details">
{/* Status, Priority, Assignee */}
</TabsContent>
<TabsContent value="time">
{/* Time tracking, Dates */}
</TabsContent>
<TabsContent value="epic">
{/* Parent Epic card */}
</TabsContent>
</Tabs>
);
}
```
### ARIA Labels and Roles
```typescript
// Story header with accessibility
<header role="banner" aria-labelledby="story-title">
<h1 id="story-title" className="text-3xl font-bold">
{story.title}
</h1>
<div role="group" aria-label="Story metadata">
<Badge aria-label={`Status: ${story.status}`}>
{story.status}
</Badge>
<Badge aria-label={`Priority: ${story.priority}`}>
{story.priority}
</Badge>
</div>
<div role="group" aria-label="Story actions">
<Button
onClick={onEdit}
aria-label="Edit story details"
aria-describedby="edit-help"
>
<Edit className="h-4 w-4" aria-hidden="true" />
Edit
</Button>
<span id="edit-help" className="sr-only">
Opens a dialog to edit story title, description, and metadata
</span>
<Button
onClick={onDelete}
aria-label="Delete story permanently"
aria-describedby="delete-help"
>
<Trash2 className="h-4 w-4" aria-hidden="true" />
Delete
</Button>
<span id="delete-help" className="sr-only">
Permanently deletes this story and all associated tasks
</span>
</div>
</header>
// Main content sections
<main role="main" aria-labelledby="story-title">
<section aria-labelledby="description-heading">
<h2 id="description-heading">Description</h2>
<div role="document" aria-label="Story description">
{story.description}
</div>
</section>
<section aria-labelledby="criteria-heading">
<h2 id="criteria-heading">Acceptance Criteria</h2>
{/* Criteria list */}
</section>
<section aria-labelledby="tasks-heading">
<h2 id="tasks-heading">Tasks</h2>
{/* Tasks list */}
</section>
</main>
// Sidebar
<aside role="complementary" aria-label="Story metadata">
<h2 className="sr-only">Story Details</h2>
{/* Metadata cards */}
</aside>
```
### Keyboard Navigation
```typescript
// Add keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// ESC - Go back to Epic
if (e.key === 'Escape' && !isEditDialogOpen && !isDeleteDialogOpen) {
router.push(`/epics/${story.epicId}`);
}
// Cmd/Ctrl + E - Edit Story
if ((e.metaKey || e.ctrlKey) && e.key === 'e') {
e.preventDefault();
setIsEditDialogOpen(true);
}
// Cmd/Ctrl + Backspace - Delete Story
if ((e.metaKey || e.ctrlKey) && e.key === 'Backspace') {
e.preventDefault();
setIsDeleteDialogOpen(true);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isEditDialogOpen, isDeleteDialogOpen]);
// Add keyboard shortcut hints
<footer className="mt-8 text-xs text-muted-foreground">
<p>Keyboard shortcuts:</p>
<ul className="list-disc list-inside">
<li>ESC - Back to Epic</li>
<li>Cmd/Ctrl + E - Edit Story</li>
<li>Cmd/Ctrl + Backspace - Delete Story</li>
</ul>
</footer>
```
### Focus Management
```typescript
// Trap focus in dialogs
import { useFocusTrap } from '@/lib/hooks/use-focus-trap';
function EditStoryDialog({ isOpen, onClose }: Props) {
const dialogRef = useRef<HTMLDivElement>(null);
useFocusTrap(dialogRef, isOpen);
// Auto-focus first field when dialog opens
useEffect(() => {
if (isOpen) {
const firstInput = dialogRef.current?.querySelector('input');
firstInput?.focus();
}
}, [isOpen]);
// Return focus to trigger element when dialog closes
const triggerRef = useRef<HTMLButtonElement | null>(null);
const handleOpen = () => {
triggerRef.current = document.activeElement as HTMLButtonElement;
setIsOpen(true);
};
const handleClose = () => {
setIsOpen(false);
triggerRef.current?.focus();
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent ref={dialogRef}>
{/* Dialog content */}
</DialogContent>
</Dialog>
);
}
```
### Skip Links
```typescript
// Add skip link for keyboard users
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-md"
>
Skip to main content
</a>
<main id="main-content" tabIndex={-1}>
{/* Main content */}
</main>
```
## Acceptance Criteria
### Responsive Design
- [ ] Desktop (> 1024px): Two-column layout (content + sidebar)
- [ ] Tablet (640px - 1024px): Two-column layout with narrower sidebar
- [ ] Mobile (< 640px): Single-column layout, sidebar moves to top or tabs
- [ ] All buttons and controls accessible on touch devices
- [ ] No horizontal scrolling on any screen size
- [ ] Text remains readable on all screen sizes (minimum 14px)
### Accessibility (WCAG 2.1 Level AA)
- [ ] All interactive elements keyboard accessible (Tab navigation)
- [ ] Focus indicators visible (2px outline, high contrast)
- [ ] ARIA labels on all buttons, links, and form controls
- [ ] Proper heading hierarchy (h1 h2 h3)
- [ ] Color contrast ratio >= 4.5:1 for all text
- [ ] Large text (18px+) contrast ratio >= 3:1
- [ ] Screen reader announces all content correctly
- [ ] Focus trapped in dialogs (cannot Tab outside)
- [ ] Focus returns to trigger element when dialog closes
- [ ] Skip links available for keyboard users
- [ ] Icons have `aria-hidden="true"` (text labels present)
- [ ] Loading states announced to screen readers (`role="status"`)
- [ ] Error messages announced (`role="alert"`)
### Keyboard Navigation
- [ ] Tab - Navigate through interactive elements
- [ ] Enter - Activate buttons and links
- [ ] Space - Toggle checkboxes and buttons
- [ ] ESC - Close dialogs and navigate back
- [ ] Cmd/Ctrl + E - Edit Story (custom shortcut)
- [ ] Cmd/Ctrl + Backspace - Delete Story (custom shortcut)
## Testing
### Manual Testing
**Responsive Design**:
1. Open Story detail page on desktop → Verify two-column layout
2. Resize to tablet width (768px) → Verify layout adjusts
3. Resize to mobile width (375px) → Verify single-column layout
4. Verify sidebar moves to top on mobile
5. Test all buttons clickable on touch device
6. Verify no horizontal scrolling on any screen
7. Test landscape and portrait orientations
**Keyboard Navigation**:
1. Navigate to Story page using only keyboard
2. Press Tab → Verify focus moves through all interactive elements
3. Verify focus indicator visible (blue outline)
4. Press ESC → Verify navigates back to Epic
5. Press Cmd/Ctrl + E → Verify Edit dialog opens
6. Press Tab in dialog → Verify focus trapped
7. Press ESC in dialog → Verify dialog closes and focus returns
**Screen Reader** (NVDA/JAWS):
1. Enable screen reader
2. Navigate to Story page
3. Verify page structure announced (heading hierarchy)
4. Verify all buttons have labels
5. Verify badges announced with context ("Status: In Progress")
6. Verify loading states announced
7. Verify error messages announced
8. Open Edit dialog → Verify form fields labeled correctly
### Automated Testing
**Lighthouse Audit**:
```bash
# Run Lighthouse accessibility audit
npm run lighthouse
# Expected results:
# Accessibility: >= 90 (ideally 100)
# Best Practices: >= 90
# SEO: >= 90
# Performance: >= 80 (acceptable for dynamic content)
```
**axe DevTools**:
```bash
# Install axe DevTools browser extension
# Open Story page
# Run axe scan
# Expected: 0 violations
```
### Color Contrast Testing
```bash
# Test all text colors
# Use browser DevTools > Accessibility > Color contrast
# Required ratios (WCAG AA):
# Normal text (< 18px): 4.5:1
# Large text (>= 18px): 3:1
# UI components: 3:1
# Test combinations:
Story Title (32px): Should pass (large text)
Description (16px): Should pass 4.5:1
Metadata labels (14px): Should pass 4.5:1
Badges: Should pass 3:1 (UI components)
Buttons: Should pass 3:1 (UI components)
```
## Dependencies
**Prerequisites**:
- Task 1, 2, 3, 4, 5 (all components must exist)
- ✅ shadcn/ui components with built-in accessibility
- ✅ Tailwind CSS responsive utilities
- ✅ ARIA attributes support
**Blocks**:
- None (final polish task)
## Estimated Time
3 hours
## Notes
**Testing Tools**:
- Chrome DevTools > Lighthouse (accessibility audit)
- axe DevTools browser extension (automated WCAG testing)
- NVDA/JAWS screen reader (manual testing)
- WAVE browser extension (visual accessibility check)
- Contrast Checker (color contrast ratios)
**Common Accessibility Issues to Avoid**:
- Missing alt text on images
- Insufficient color contrast
- Missing ARIA labels on icon-only buttons
- Broken keyboard navigation (focus trap)
- Missing heading hierarchy (h1 → h3, skip h2)
- Form inputs without labels
- Loading states not announced to screen readers
**Responsive Design Checklist**:
- Test on real devices (iPhone, Android, tablet)
- Test different orientations (portrait, landscape)
- Test different zoom levels (100%, 150%, 200%)
- Verify touch targets >= 44x44px
- No horizontal scrolling on any screen

View File

@@ -0,0 +1,278 @@
---
story_id: sprint_4_story_2
sprint: sprint_4
priority: P0
status: not_started
story_points: 5
estimated_days: 2
created_date: 2025-11-05
assignee: Frontend Team
---
# Story 2: Task Management in Story Detail
**Sprint**: Sprint 4
**Priority**: P0 (Critical)
**Estimated**: 2 days
**Owner**: Frontend Team
## Description
Implement comprehensive Task management within the Story detail page, including Task list display, inline Task creation, Task status updates via checkbox, Task filtering, and Task sorting. This enables users to break down Stories into granular implementation tasks and track their progress.
## User Story
**As a** developer or project manager,
**I want** to create and manage Tasks within a Story,
**So that** I can break down Stories into smaller work items and track implementation progress.
## Acceptance Criteria
- [ ] Task list displays all Tasks associated with the Story
- [ ] Empty state shows when Story has no Tasks
- [ ] "Add Task" button opens inline Task creation form
- [ ] Inline form allows creating Tasks without leaving the page
- [ ] Task checkbox toggles Task status (Todo ↔ Done)
- [ ] Task cards display title, priority, assignee, estimated hours
- [ ] Task count badge shows in Story header (e.g., "Tasks (8)")
- [ ] Task filters allow filtering by status, priority, assignee
- [ ] Task sorting allows sorting by priority, status, created date, assignee
- [ ] Task form validates inputs (title required, hours numeric)
- [ ] Success/error toast notifications for Task operations
- [ ] Loading states during Task creation/update
- [ ] Optimistic UI updates for instant feedback
## Technical Requirements
**New Components**:
- `components/projects/task-list.tsx` - Task list container (300-400 lines)
- `components/projects/task-card.tsx` - Individual Task card (150-200 lines)
- `components/projects/task-form.tsx` - Inline Task creation form (200-250 lines)
**New Hooks**:
- `lib/hooks/use-tasks.ts` - Task CRUD hooks (150-200 lines)
- `useTasks(storyId)` - List Tasks by Story
- `useCreateTask()` - Create Task
- `useUpdateTask()` - Update Task
- `useDeleteTask()` - Delete Task
- `useChangeTaskStatus()` - Change Task status (checkbox)
**API Integration**:
- `GET /api/v1/stories/{storyId}/tasks` - List Tasks
- `POST /api/v1/tasks` - Create Task
- `PUT /api/v1/tasks/{id}` - Update Task
- `DELETE /api/v1/tasks/{id}` - Delete Task
- `PUT /api/v1/tasks/{id}/status` - Change status
**Task Types** (add to `types/project.ts`):
```typescript
export interface Task {
id: string;
title: string;
description?: string;
storyId: string;
projectId: string;
status: WorkItemStatus;
priority: WorkItemPriority;
estimatedHours?: number;
actualHours?: number;
assigneeId?: string;
tenantId: string;
createdAt: string;
updatedAt: string;
}
export interface CreateTaskDto {
storyId: string;
title: string;
description?: string;
priority: WorkItemPriority;
estimatedHours?: number;
}
export interface UpdateTaskDto {
title?: string;
description?: string;
priority?: WorkItemPriority;
estimatedHours?: number;
actualHours?: number;
}
```
## Tasks
- [ ] [Task 1](sprint_4_story_2_task_1.md) - Verify Task API endpoints and create Task types
- [ ] [Task 2](sprint_4_story_2_task_2.md) - Create Task API client and React Query hooks
- [ ] [Task 3](sprint_4_story_2_task_3.md) - Implement TaskList component with filters and sorting
- [ ] [Task 4](sprint_4_story_2_task_4.md) - Implement TaskCard component with checkbox status toggle
- [ ] [Task 5](sprint_4_story_2_task_5.md) - Implement inline TaskForm for Task creation
- [ ] [Task 6](sprint_4_story_2_task_6.md) - Integrate Task management into Story detail page
**Progress**: 0/6 tasks completed
## Dependencies
**Prerequisites**:
- ✅ Task API endpoints ready (TasksController.cs)
- Story 1 completed (Story detail page must exist)
**Blocks**:
- None (independent feature after Story 1)
## Definition of Done
- All 6 tasks completed
- Task list displays Tasks correctly
- Task creation form working (inline)
- Task checkbox status toggle working
- Task filters and sorting functional
- Task count badge updates in real-time
- Empty state displays when no Tasks
- Loading and error states implemented
- Optimistic UI updates for status changes
- Code reviewed and approved
- E2E tests passing
- Git commit created with descriptive message
## Test Plan
**Manual Testing**:
1. Navigate to Story detail page
2. Verify "Add Task" button visible
3. Click "Add Task" → Verify inline form appears
4. Create Task with title "Implement login API" → Verify Task appears in list
5. Create Task without title → Verify validation error
6. Click Task checkbox → Verify status changes to Done
7. Uncheck Task checkbox → Verify status changes back to Todo
8. Verify Task count badge updates (e.g., "Tasks (3)")
9. Filter by status: "Done" → Verify only Done Tasks show
10. Sort by priority → Verify Tasks reorder correctly
11. Create 10+ Tasks → Verify list scrolls properly
12. Test on mobile → Verify responsive layout
**E2E Test** (Playwright):
```typescript
test('user can create and manage tasks', async ({ page }) => {
await page.goto('/stories/story-123');
// Create Task
await page.click('[data-testid="add-task-button"]');
await page.fill('[name="title"]', 'Implement login form');
await page.selectOption('[name="priority"]', 'High');
await page.fill('[name="estimatedHours"]', '8');
await page.click('[data-testid="submit-task"]');
// Verify Task appears
await expect(page.locator('[data-testid="task-item"]')).toContainText('Implement login form');
// Toggle status
await page.click('[data-testid="task-checkbox"]');
await expect(page.locator('[data-testid="task-status"]')).toContainText('Done');
// Verify count updates
await expect(page.locator('[data-testid="task-count"]')).toContainText('1');
});
test('task filters work correctly', async ({ page }) => {
await page.goto('/stories/story-123');
// Create Done Task
await page.click('[data-testid="add-task-button"]');
await page.fill('[name="title"]', 'Task 1');
await page.click('[data-testid="submit-task"]');
await page.click('[data-testid="task-checkbox"]');
// Create Todo Task
await page.click('[data-testid="add-task-button"]');
await page.fill('[name="title"]', 'Task 2');
await page.click('[data-testid="submit-task"]');
// Filter by Done
await page.selectOption('[data-testid="task-filter"]', 'Done');
await expect(page.locator('[data-testid="task-item"]')).toHaveCount(1);
await expect(page.locator('[data-testid="task-item"]')).toContainText('Task 1');
});
```
**Verification Commands**:
```bash
# Start dev server
npm run dev
# Navigate to Story detail page
# Create Tasks and test functionality
# Run E2E tests
npm run test:e2e -- tasks
```
## UX Design Reference
**Design Document**: `docs/designs/STORY_UX_UI_DESIGN.md`
- Section: "Tasks Section" (lines 305-348)
- Layout: Task cards with checkbox, metadata, actions
- Interaction: Checkbox toggles status, click card to expand details
- Filters: All, Todo, InProgress, Done
- Sorting: Priority, Status, Created date, Assignee
**Design Tokens**:
```css
/* Task Card */
.task-card {
padding: 12px 16px;
border: 1px solid var(--border);
border-radius: 8px;
}
.task-card:hover {
border-color: var(--primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Task Checkbox */
.task-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
}
.task-checkbox:hover {
transform: scale(1.1);
}
/* Task Count Badge */
.task-count-badge {
background: var(--muted);
color: var(--muted-foreground);
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
}
```
## Notes
**Why This Matters**:
- **Core Feature**: Tasks are essential for breaking down Stories into implementation steps
- **User Experience**: Inline Task creation enables fast workflow without navigation
- **Real-time Feedback**: Optimistic UI updates make the app feel instant
- **Epic → Story → Task**: Completes the full project management hierarchy
**Performance Considerations**:
- Use React.memo for TaskCard to prevent unnecessary re-renders
- Debounce filter/sort operations (300ms delay)
- Virtual scrolling for lists > 50 Tasks (Phase 3)
- Optimistic updates for status changes (instant UI feedback)
**Future Enhancements** (Post-Sprint 4):
- Task drag-and-drop reordering
- Task bulk operations (multi-select)
- Task due dates and reminders
- Task comments and activity log
- Task templates for common patterns
- Task time tracking (start/stop timer)
---
**Created**: 2025-11-05 by Frontend Agent
**Updated**: 2025-11-05

View File

@@ -0,0 +1,234 @@
---
task_id: sprint_4_story_2_task_1
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: 2
---
# Task 1: Verify Task API Endpoints and Create Task Types
## Description
Verify that all Task API endpoints are working correctly and create TypeScript types for Task entities. This task ensures the backend is ready and establishes the type-safe foundation for Task management.
## What to Do
1. Test Task API endpoints using Postman or Swagger
2. Verify GET /api/v1/stories/{storyId}/tasks returns Task list
3. Verify POST /api/v1/tasks creates Task correctly
4. Verify PUT /api/v1/tasks/{id} updates Task
5. Verify DELETE /api/v1/tasks/{id} deletes Task
6. Verify PUT /api/v1/tasks/{id}/status changes status
7. Check multi-tenant isolation (cannot access other tenant Tasks)
8. Add Task types to `types/project.ts`
9. Document any API quirks or issues
## Files to Create/Modify
- `types/project.ts` (modify, add Task types ~50 lines)
## Implementation Details
```typescript
// types/project.ts
// Add these Task types
export interface Task {
id: string;
title: string;
description?: string;
storyId: string;
projectId: string;
status: WorkItemStatus;
priority: WorkItemPriority;
estimatedHours?: number;
actualHours?: number;
assigneeId?: string;
tenantId: string;
createdAt: string;
updatedAt: string;
createdBy?: string;
updatedBy?: string;
}
export interface CreateTaskDto {
storyId: string;
projectId?: string; // May be auto-filled from Story
title: string;
description?: string;
priority: WorkItemPriority;
estimatedHours?: number;
createdBy: string;
}
export interface UpdateTaskDto {
title?: string;
description?: string;
priority?: WorkItemPriority;
estimatedHours?: number;
actualHours?: number;
}
export interface ChangeTaskStatusDto {
status: WorkItemStatus;
}
export interface AssignTaskDto {
assigneeId: string;
}
// Add Task to existing types if needed
export interface Story {
// ... existing fields
taskCount?: number; // Optional: count of Tasks
tasks?: Task[]; // Optional: nested Tasks
}
```
## API Testing Checklist
**GET /api/v1/stories/{storyId}/tasks** - List Tasks:
```bash
# Expected: 200 OK, array of Tasks
curl -H "Authorization: Bearer {token}" \
GET https://api.colaflow.com/api/v1/stories/{storyId}/tasks
# Test cases:
# - Valid storyId → Returns tasks array
# - Empty story → Returns []
# - Invalid storyId → Returns 404
# - Other tenant story → Returns 403 or empty array
```
**POST /api/v1/tasks** - Create Task:
```bash
# Expected: 201 Created, Task object
curl -H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"storyId": "story-123",
"title": "Implement login API",
"description": "Create POST /auth/login endpoint",
"priority": "High",
"estimatedHours": 8,
"createdBy": "user-123"
}' \
POST https://api.colaflow.com/api/v1/tasks
# Test cases:
# - Valid data → Creates task
# - Missing title → Returns 400 validation error
# - Invalid storyId → Returns 404 or 400
# - Missing storyId → Returns 400
```
**PUT /api/v1/tasks/{id}** - Update Task:
```bash
# Expected: 200 OK, updated Task object
curl -H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"title": "Updated title",
"priority": "Critical"
}' \
PUT https://api.colaflow.com/api/v1/tasks/{id}
# Test cases:
# - Valid update → Returns updated task
# - Invalid taskId → Returns 404
# - Other tenant task → Returns 403
```
**DELETE /api/v1/tasks/{id}** - Delete Task:
```bash
# Expected: 204 No Content or 200 OK
curl -H "Authorization: Bearer {token}" \
DELETE https://api.colaflow.com/api/v1/tasks/{id}
# Test cases:
# - Valid taskId → Deletes task
# - Invalid taskId → Returns 404
# - Other tenant task → Returns 403
```
**PUT /api/v1/tasks/{id}/status** - Change Status:
```bash
# Expected: 200 OK, updated Task object
curl -H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"status": "Done"
}' \
PUT https://api.colaflow.com/api/v1/tasks/{id}/status
# Test cases:
# - Valid status → Updates status
# - Invalid status → Returns 400
# - Other tenant task → Returns 403
```
## Acceptance Criteria
- [ ] All Task API endpoints tested and working
- [ ] GET endpoint returns Task list for Story
- [ ] POST endpoint creates Task successfully
- [ ] PUT endpoint updates Task correctly
- [ ] DELETE endpoint removes Task
- [ ] Status change endpoint works
- [ ] Multi-tenant isolation verified (cannot access other tenant Tasks)
- [ ] Task types added to `types/project.ts`
- [ ] All Task fields properly typed
- [ ] CreateTaskDto, UpdateTaskDto, ChangeTaskStatusDto defined
- [ ] API quirks documented (if any)
## Testing
**Postman/Swagger Testing**:
1. Import Task API collection (if available)
2. Test each endpoint with valid data
3. Test error cases (invalid IDs, missing fields, validation)
4. Verify responses match TypeScript types
5. Document any discrepancies
**Expected API Behavior**:
- All endpoints require authentication (JWT token)
- All endpoints respect multi-tenant isolation (TenantId filter)
- Validation errors return 400 with error details
- Not found errors return 404
- Forbidden errors return 403
- Successful creates return 201 Created
- Successful updates/deletes return 200 OK or 204 No Content
## Dependencies
**Prerequisites**:
- ✅ Task API ready (TasksController.cs)
- ✅ JWT authentication working
- ✅ Postman or Swagger access
**Blocks**:
- Task 2 (API client depends on verified endpoints)
## Estimated Time
2 hours
## Notes
**Document API Issues**:
If you find any API issues, document them clearly:
- Missing fields in response
- Unexpected validation rules
- Incorrect HTTP status codes
- Multi-tenant isolation not working
- Performance issues (slow responses)
**Communicate with Backend**:
If API endpoints are not ready or have issues, immediately notify Backend team and Product Manager. This is a blocker for Story 2.
**Fallback Plan**:
If Task API is not ready, frontend can proceed with mock data for development, but API must be ready before Story 2 completion.

View File

@@ -0,0 +1,449 @@
---
task_id: sprint_4_story_2_task_2
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: 3
---
# Task 2: Create Task API Client and React Query Hooks
## Description
Create the Task API client module and React Query hooks for Task CRUD operations. This establishes the data layer for Task management with optimistic updates, caching, and error handling.
## What to Do
1. Add Task API client methods to `lib/api/pm.ts`
2. Create `lib/hooks/use-tasks.ts` with React Query hooks
3. Implement optimistic updates for instant UI feedback
4. Add cache invalidation strategies
5. Add error handling with toast notifications
6. Add logger integration for debugging
7. Test all hooks with real API data
8. Document hook usage and examples
## Files to Create/Modify
- `lib/api/pm.ts` (modify, add Task methods ~100 lines)
- `lib/hooks/use-tasks.ts` (new, ~150-200 lines)
## Implementation Details
### Task API Client (`lib/api/pm.ts`)
```typescript
// lib/api/pm.ts
// Add these Task API methods
import { apiClient } from './client';
import type {
Task,
CreateTaskDto,
UpdateTaskDto,
ChangeTaskStatusDto,
AssignTaskDto,
} from '@/types/project';
export const tasksApi = {
/**
* Get all Tasks for a Story
*/
list: async (storyId: string): Promise<Task[]> => {
const response = await apiClient.get(`/api/v1/stories/${storyId}/tasks`);
return response.data;
},
/**
* Get single Task by ID
*/
get: async (id: string): Promise<Task> => {
const response = await apiClient.get(`/api/v1/tasks/${id}`);
return response.data;
},
/**
* Create new Task
*/
create: async (data: CreateTaskDto): Promise<Task> => {
const response = await apiClient.post('/api/v1/tasks', data);
return response.data;
},
/**
* Update Task
*/
update: async (id: string, data: UpdateTaskDto): Promise<Task> => {
const response = await apiClient.put(`/api/v1/tasks/${id}`, data);
return response.data;
},
/**
* Delete Task
*/
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/api/v1/tasks/${id}`);
},
/**
* Change Task status
*/
changeStatus: async (id: string, status: string): Promise<Task> => {
const response = await apiClient.put(`/api/v1/tasks/${id}/status`, { status });
return response.data;
},
/**
* Assign Task to user
*/
assign: async (id: string, assigneeId: string): Promise<Task> => {
const response = await apiClient.put(`/api/v1/tasks/${id}/assign`, { assigneeId });
return response.data;
},
};
```
### React Query Hooks (`lib/hooks/use-tasks.ts`)
```typescript
// lib/hooks/use-tasks.ts
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { tasksApi } from '@/lib/api/pm';
import { toast } from 'sonner';
import { logger } from '@/lib/utils/logger';
import type { Task, CreateTaskDto, UpdateTaskDto } from '@/types/project';
/**
* Get all Tasks for a Story
*/
export function useTasks(storyId: string | undefined) {
return useQuery({
queryKey: ['tasks', storyId],
queryFn: () => {
if (!storyId) throw new Error('Story ID required');
return tasksApi.list(storyId);
},
enabled: !!storyId,
staleTime: 60_000, // Consider fresh for 1 minute
});
}
/**
* Get single Task by ID
*/
export function useTask(id: string | undefined) {
return useQuery({
queryKey: ['tasks', id],
queryFn: () => {
if (!id) throw new Error('Task ID required');
return tasksApi.get(id);
},
enabled: !!id,
});
}
/**
* Create new Task with optimistic update
*/
export function useCreateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: CreateTaskDto) => {
logger.info('Creating task:', data);
return tasksApi.create(data);
},
onMutate: async (newTask) => {
// Cancel outgoing queries
await queryClient.cancelQueries({ queryKey: ['tasks', newTask.storyId] });
// Snapshot previous value
const previousTasks = queryClient.getQueryData<Task[]>(['tasks', newTask.storyId]);
// Optimistically update (optional for creates)
// Usually we just wait for server response
return { previousTasks };
},
onSuccess: (newTask, variables) => {
logger.info('Task created successfully:', newTask);
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['tasks', variables.storyId] });
queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] }); // Update Story task count
toast.success('Task created successfully');
},
onError: (error, variables, context) => {
logger.error('Failed to create task:', error);
// Restore previous state if optimistic update was used
if (context?.previousTasks) {
queryClient.setQueryData(['tasks', variables.storyId], context.previousTasks);
}
toast.error('Failed to create Task');
},
});
}
/**
* Update Task with optimistic update
*/
export function useUpdateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, data }: { id: string; data: UpdateTaskDto }) => {
logger.info('Updating task:', id, data);
return tasksApi.update(id, data);
},
onMutate: async ({ id, data }) => {
// Cancel queries
await queryClient.cancelQueries({ queryKey: ['tasks', id] });
// Snapshot previous
const previousTask = queryClient.getQueryData<Task>(['tasks', id]);
// Optimistically update
queryClient.setQueryData<Task>(['tasks', id], (old) => {
if (!old) return old;
return { ...old, ...data };
});
return { previousTask };
},
onSuccess: (updatedTask) => {
logger.info('Task updated successfully:', updatedTask);
// Invalidate queries
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.id] });
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.storyId] });
toast.success('Task updated successfully');
},
onError: (error, variables, context) => {
logger.error('Failed to update task:', error);
// Revert optimistic update
if (context?.previousTask) {
queryClient.setQueryData(['tasks', variables.id], context.previousTask);
}
toast.error('Failed to update Task');
},
});
}
/**
* Delete Task
*/
export function useDeleteTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
logger.info('Deleting task:', id);
return tasksApi.delete(id);
},
onSuccess: (_, deletedId) => {
logger.info('Task deleted successfully:', deletedId);
// Remove from cache
queryClient.removeQueries({ queryKey: ['tasks', deletedId] });
// Invalidate lists
queryClient.invalidateQueries({ queryKey: ['tasks'] });
toast.success('Task deleted successfully');
},
onError: (error) => {
logger.error('Failed to delete task:', error);
toast.error('Failed to delete Task');
},
});
}
/**
* Change Task status (for checkbox toggle)
*/
export function useChangeTaskStatus() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ taskId, status }: { taskId: string; status: string }) => {
logger.info('Changing task status:', taskId, status);
return tasksApi.changeStatus(taskId, status);
},
onMutate: async ({ taskId, status }) => {
// Cancel queries
await queryClient.cancelQueries({ queryKey: ['tasks', taskId] });
// Snapshot previous
const previousTask = queryClient.getQueryData<Task>(['tasks', taskId]);
// Optimistically update status
queryClient.setQueryData<Task>(['tasks', taskId], (old) => {
if (!old) return old;
return { ...old, status };
});
// Also update in list
const storyId = previousTask?.storyId;
if (storyId) {
queryClient.setQueryData<Task[]>(['tasks', storyId], (old) => {
if (!old) return old;
return old.map((task) =>
task.id === taskId ? { ...task, status } : task
);
});
}
return { previousTask };
},
onSuccess: (updatedTask) => {
logger.info('Task status changed successfully:', updatedTask);
// Invalidate queries
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.id] });
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.storyId] });
queryClient.invalidateQueries({ queryKey: ['stories', updatedTask.storyId] }); // Update Story progress
toast.success(`Task marked as ${updatedTask.status}`);
},
onError: (error, variables, context) => {
logger.error('Failed to change task status:', error);
// Revert optimistic update
if (context?.previousTask) {
queryClient.setQueryData(['tasks', variables.taskId], context.previousTask);
}
toast.error('Failed to update Task status');
},
});
}
/**
* Assign Task to user
*/
export function useAssignTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ taskId, assigneeId }: { taskId: string; assigneeId: string }) => {
logger.info('Assigning task:', taskId, assigneeId);
return tasksApi.assign(taskId, assigneeId);
},
onSuccess: (updatedTask) => {
logger.info('Task assigned successfully:', updatedTask);
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.id] });
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.storyId] });
toast.success('Task assigned successfully');
},
onError: (error) => {
logger.error('Failed to assign task:', error);
toast.error('Failed to assign Task');
},
});
}
```
## Acceptance Criteria
- [ ] Task API client methods added to `lib/api/pm.ts`
- [ ] All CRUD operations implemented (list, get, create, update, delete)
- [ ] `use-tasks.ts` hooks file created
- [ ] `useTasks(storyId)` hook returns Task list for Story
- [ ] `useCreateTask()` hook creates Task with optimistic update
- [ ] `useUpdateTask()` hook updates Task with optimistic update
- [ ] `useDeleteTask()` hook deletes Task
- [ ] `useChangeTaskStatus()` hook changes status with optimistic update
- [ ] All hooks include error handling with toast notifications
- [ ] All hooks include logger integration
- [ ] Cache invalidation strategies implemented correctly
- [ ] Optimistic updates provide instant UI feedback
- [ ] Hooks tested with real API data
## Testing
**Manual Testing**:
```typescript
// Test in a React component
function TestTaskHooks() {
const { data: tasks, isLoading } = useTasks('story-123');
const createTask = useCreateTask();
const handleCreate = () => {
createTask.mutate({
storyId: 'story-123',
title: 'Test Task',
priority: 'High',
estimatedHours: 8,
createdBy: 'user-123',
});
};
return (
<div>
<button onClick={handleCreate}>Create Task</button>
{isLoading && <p>Loading...</p>}
{tasks?.map((task) => (
<div key={task.id}>{task.title}</div>
))}
</div>
);
}
```
**Test Cases**:
1. Create Task → Verify appears in list immediately (optimistic update)
2. Update Task → Verify updates instantly
3. Delete Task → Verify removes from list
4. Change status → Verify checkbox updates instantly
5. Error handling → Disconnect internet → Verify error toast shows
6. Cache invalidation → Create Task → Verify Story task count updates
## Dependencies
**Prerequisites**:
- Task 1 (API endpoints verified, types created)
- ✅ React Query configured
- ✅ apiClient ready (`lib/api/client.ts`)
- ✅ logger utility (`lib/utils/logger.ts`)
- ✅ sonner toast library
**Blocks**:
- Task 3, 4, 5 (components depend on hooks)
## Estimated Time
3 hours
## Notes
**Optimistic Updates**: Provide instant UI feedback by updating cache immediately, then reverting on error. This makes the app feel fast and responsive.
**Cache Invalidation**: When creating/updating/deleting Tasks, also invalidate the parent Story query to update task counts and progress indicators.
**Code Reuse**: Copy patterns from `use-stories.ts` hook - Task hooks are very similar to Story hooks.

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.

View File

@@ -0,0 +1,28 @@
---
task_id: sprint_4_story_2_task_4
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: 3
---
# Task 4: Implement TaskCard Component with Checkbox Status Toggle
## Description
Create the TaskCard component that displays individual Task information with a checkbox for quick status toggling.
## Acceptance Criteria
- [ ] TaskCard displays Task title, priority, estimated hours
- [ ] Checkbox shows correct state (checked = Done, unchecked = Todo/InProgress)
- [ ] Clicking checkbox toggles Task status
- [ ] Optimistic UI update for instant feedback
- [ ] Hover effect highlights card
## Estimated Time
3 hours

View File

@@ -0,0 +1,29 @@
---
task_id: sprint_4_story_2_task_5
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: 3
---
# Task 5: Implement Inline TaskForm for Task Creation
## Description
Create the inline TaskForm component for creating new Tasks without leaving the Story detail page.
## Acceptance Criteria
- [ ] Form displays title, description, priority, estimated hours fields
- [ ] Title field is required with validation
- [ ] Submitting form creates Task successfully
- [ ] Form resets after successful creation
- [ ] Cancel button closes form
- [ ] Success and error toast notifications
## Estimated Time
3 hours

View File

@@ -0,0 +1,30 @@
---
task_id: sprint_4_story_2_task_6
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: 2
---
# Task 6: Integrate Task Management into Story Detail Page
## Description
Integrate the complete Task management system into the Story detail page and test full functionality.
## Acceptance Criteria
- [ ] TaskList component integrated into Story detail page
- [ ] Task count badge shows in Story header
- [ ] Full Task CRUD operations working
- [ ] Filters and sorting functional
- [ ] Checkbox status toggle working
- [ ] E2E tests passing
- [ ] No console errors
## Estimated Time
2 hours

View File

@@ -0,0 +1,58 @@
---
story_id: sprint_4_story_3
sprint: sprint_4
priority: P1
status: not_started
story_points: 3
estimated_days: 2
created_date: 2025-11-05
assignee: Frontend Team
---
# Story 3: Enhanced Story Form
**Sprint**: Sprint 4
**Priority**: P1 (High)
**Estimated**: 2 days
**Owner**: Frontend Team
## Description
Enhance the existing Story form component to include UX-designed fields: Acceptance Criteria (checkbox list), Assignee selector, Tags/Labels (multi-select), and Story Points. This improves Story planning capabilities and aligns with the comprehensive UX design.
## User Story
**As a** product manager,
**I want** to add acceptance criteria, assignee, tags, and story points when creating Stories,
**So that** I can better plan and communicate Story requirements.
## Acceptance Criteria
- [ ] Form includes Acceptance Criteria field (dynamic checkbox list)
- [ ] Form includes Assignee selector (searchable dropdown showing users)
- [ ] Form includes Tags field (multi-select labels)
- [ ] Form includes Story Points field (optional numeric)
- [ ] Acceptance criteria can be added/removed dynamically
- [ ] Tags support multi-select
- [ ] Form validation works for all fields
- [ ] Backward compatible with existing Stories (missing fields are optional)
- [ ] Form saves correctly with all new fields
## Tasks
- [ ] Task 1: Add Acceptance Criteria editor component
- [ ] Task 2: Implement Assignee selector component
- [ ] Task 3: Add Tags/Labels multi-select component
- [ ] Task 4: Add Story Points field and update schema
- [ ] Task 5: Integrate all new fields into Story form
- [ ] Task 6: Update Story types and API client
**Progress**: 0/6 tasks completed
## Estimated Time
2 days (16 hours)
---
**Created**: 2025-11-05 by Frontend Agent

View File

@@ -0,0 +1,57 @@
---
story_id: sprint_4_story_4
sprint: sprint_4
priority: P1
status: not_started
story_points: 3
estimated_days: 2
created_date: 2025-11-05
assignee: Frontend Team
---
# Story 4: Quick Add Story Workflow
**Sprint**: Sprint 4
**Priority**: P1 (High)
**Estimated**: 2 days
**Owner**: Frontend Team
## Description
Implement a Quick Add Story workflow with an inline form that requires only title and priority. This enables rapid Story creation for batch planning sessions without the overhead of the full form dialog.
## User Story
**As a** product manager,
**I want** to quickly create multiple Stories with just title and priority,
**So that** I can rapidly plan Epics during brainstorming sessions.
## Acceptance Criteria
- [ ] Quick Add button appears at top of Stories list in Epic detail page
- [ ] Clicking button shows inline form (not dialog)
- [ ] Form requires only title and priority (minimal fields)
- [ ] Pressing Enter key submits form
- [ ] Form resets and stays open after successful creation (batch creation)
- [ ] Keyboard shortcut (Cmd/Ctrl + N) opens Quick Add form
- [ ] "Add & Create Tasks" button variant navigates to Story detail page
- [ ] Form animations smooth and performant
- [ ] Success toast notifications shown
## Tasks
- [ ] Task 1: Create QuickAddStory component with inline form
- [ ] Task 2: Add keyboard shortcut handler (Cmd/Ctrl + N)
- [ ] Task 3: Implement auto-reset and batch creation flow
- [ ] Task 4: Add "Add & Create Tasks" button and navigation
- [ ] Task 5: Integrate into Epic detail page
**Progress**: 0/5 tasks completed
## Estimated Time
2 days (16 hours)
---
**Created**: 2025-11-05 by Frontend Agent

View File

@@ -0,0 +1,55 @@
---
story_id: sprint_4_story_5
sprint: sprint_4
priority: P2
status: not_started
story_points: 2
estimated_days: 1
created_date: 2025-11-05
assignee: Frontend Team
---
# Story 5: Story Card Component
**Sprint**: Sprint 4
**Priority**: P2 (Medium)
**Estimated**: 1 day
**Owner**: Frontend Team
## Description
Create a reusable StoryCard component with three variants (list, kanban, compact) to replace inline Story display code and improve code maintainability. This component will be used across Epic detail page, Kanban board, and Story lists.
## User Story
**As a** developer,
**I want** a reusable Story card component,
**So that** Story display is consistent across the app and easier to maintain.
## Acceptance Criteria
- [ ] StoryCard component works in three variants (list, kanban, compact)
- [ ] Visual states implemented (default, hover, selected, dragging)
- [ ] Quick actions menu appears on hover (Edit, Delete, Duplicate)
- [ ] Task count indicator shows (e.g., "5 tasks")
- [ ] Component shows all relevant metadata (status, priority, assignee, time)
- [ ] Component is reusable across different views
- [ ] Performance optimized with React.memo
- [ ] TypeScript types fully defined
## Tasks
- [ ] Task 1: Create StoryCard component with three variants
- [ ] Task 2: Implement visual states and hover effects
- [ ] Task 3: Add Task count indicator and quick actions
- [ ] Task 4: Optimize with React.memo and replace existing Story cards
**Progress**: 0/4 tasks completed
## Estimated Time
1 day (8 hours)
---
**Created**: 2025-11-05 by Frontend Agent

View File

@@ -0,0 +1,58 @@
---
story_id: sprint_4_story_6
sprint: sprint_4
priority: P2
status: not_started
story_points: 3
estimated_days: 2
created_date: 2025-11-05
assignee: Frontend Team
---
# Story 6: Kanban Story Creation Enhancement (Optional)
**Sprint**: Sprint 4
**Priority**: P2 (Optional - Stretch Goal)
**Estimated**: 2 days
**Owner**: Frontend Team
## Description
Add contextual Story creation directly from Epic cards in the Kanban board. This allows users to create Stories without leaving the Kanban view, maintaining context and improving workflow efficiency.
## User Story
**As a** product manager,
**I want** to create Stories directly from Epic cards in Kanban,
**So that** I can quickly add Stories while planning without leaving the board view.
## Acceptance Criteria
- [ ] Hovering over Epic card shows "+ Add Story" button
- [ ] Clicking button opens inline Story form below Epic card
- [ ] Form is context-bound (Epic pre-selected, read-only)
- [ ] Created Story appears in correct Kanban column by status
- [ ] Epic Story count updates in real-time
- [ ] Form slide-in animation smooth and intuitive
- [ ] Can cancel or close form easily (Cancel button, outside click, ESC key)
## Tasks
- [ ] Task 1: Enhance Epic card with "+ Add Story" action on hover
- [ ] Task 2: Create inline Story form for Kanban context
- [ ] Task 3: Implement form slide-in animation
- [ ] Task 4: Integrate real-time Epic Story count updates
**Progress**: 0/4 tasks completed
## Estimated Time
2 days (16 hours)
## Notes
**Status**: Optional stretch goal. Implement only if Stories 1-5 complete ahead of schedule. If time is tight, defer to future sprint.
---
**Created**: 2025-11-05 by Frontend Agent