Files
ColaFlow/docs/plans/sprint_4_story_1_task_4.md
Yaojia Wang 88d6413f81 feat(frontend): Create Sprint 4 Stories and Tasks for Story Management
Created comprehensive Story and Task files for Sprint 4 frontend implementation:

Story 1: Story Detail Page Foundation (P0 Critical - 3 days)
- 6 tasks: route creation, header, sidebar, data loading, Edit/Delete, responsive design
- Fixes critical 404 error when clicking Story cards
- Two-column layout consistent with Epic detail page

Story 2: Task Management in Story Detail (P0 Critical - 2 days)
- 6 tasks: API verification, hooks, TaskList, TaskCard, TaskForm, integration
- Complete Task CRUD with checkbox status toggle
- Filters, sorting, and optimistic UI updates

Story 3: Enhanced Story Form (P1 High - 2 days)
- 6 tasks: acceptance criteria, assignee selector, tags, story points, integration
- Aligns with UX design specification
- Backward compatible with existing Stories

Story 4: Quick Add Story Workflow (P1 High - 2 days)
- 5 tasks: inline form, keyboard shortcuts, batch creation, navigation
- Rapid Story creation with minimal fields
- Keyboard shortcut (Cmd/Ctrl + N)

Story 5: Story Card Component (P2 Medium - 1 day)
- 4 tasks: component variants, visual states, Task count, optimization
- Reusable component with list/kanban/compact variants
- React.memo optimization

Story 6: Kanban Story Creation Enhancement (P2 Optional - 2 days)
- 4 tasks: Epic card enhancement, inline form, animation, real-time updates
- Contextual Story creation from Kanban
- Stretch goal - implement only if ahead of schedule

Total: 6 Stories, 31 Tasks, 12 days estimated
Priority breakdown: P0 (2), P1 (2), P2 (2 optional)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 21:49:57 +01:00

10 KiB

task_id, story_id, sprint_id, status, type, assignee, created_date, estimated_hours
task_id story_id sprint_id status type assignee created_date estimated_hours
sprint_4_story_1_task_4 sprint_4_story_1 sprint_4 not_started frontend Frontend Developer 1 2025-11-05 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

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

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

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

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.