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:
325
docs/plans/sprint_4_story_1_task_4.md
Normal file
325
docs/plans/sprint_4_story_1_task_4.md
Normal 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.
|
||||
Reference in New Issue
Block a user