Files
ColaFlow-Web/components/projects/work-item-breadcrumb.tsx
Yaojia Wang bfcbf6e350 feat(frontend): Implement Epic/Story/Task Management UI (Story 2)
Complete implementation of Sprint 1 Story 2 with full CRUD operations
for Epic/Story/Task entities including forms, hierarchy visualization,
and breadcrumb navigation.

Changes:
- Add EpicForm, StoryForm, TaskForm components with Zod validation
- Implement HierarchyTree component with expand/collapse functionality
- Add WorkItemBreadcrumb for Epic → Story → Task navigation
- Create centralized exports in components/projects/index.ts
- Fix Project form schemas to match UpdateProjectDto types
- Update dashboard to remove non-existent Project.status field

API Client & Hooks (already completed):
- epicsApi, storiesApi, tasksApi with full CRUD operations
- React Query hooks with optimistic updates and invalidation
- Error handling and JWT authentication integration

Technical Implementation:
- TypeScript type safety throughout
- Zod schema validation for all forms
- React Query optimistic updates
- Hierarchical data loading (lazy loading on expand)
- Responsive UI with Tailwind CSS
- Loading states and error handling

Story Points: 8 SP
Estimated Hours: 16h
Status: Completed

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 22:58:44 +01:00

111 lines
3.3 KiB
TypeScript

'use client';
import { ChevronRight, Folder, FileText, CheckSquare, Home } from 'lucide-react';
import { useEpic } from '@/lib/hooks/use-epics';
import { useStory } from '@/lib/hooks/use-stories';
import { Skeleton } from '@/components/ui/skeleton';
import Link from 'next/link';
import type { Epic, Story, Task } from '@/types/project';
interface WorkItemBreadcrumbProps {
projectId: string;
projectName?: string;
epic?: Epic;
story?: Story;
task?: Task;
epicId?: string;
storyId?: string;
}
export function WorkItemBreadcrumb({
projectId,
projectName,
epic,
story,
task,
epicId,
storyId,
}: WorkItemBreadcrumbProps) {
// Fetch epic if only epicId provided
const { data: fetchedEpic, isLoading: epicLoading } = useEpic(
epicId && !epic ? epicId : ''
);
const effectiveEpic = epic || fetchedEpic;
// Fetch story if only storyId provided
const { data: fetchedStory, isLoading: storyLoading } = useStory(
storyId && !story ? storyId : ''
);
const effectiveStory = story || fetchedStory;
// If we need to fetch parent epic from story
const { data: parentEpic, isLoading: parentEpicLoading } = useEpic(
effectiveStory && !effectiveEpic ? effectiveStory.epicId : ''
);
const finalEpic = effectiveEpic || parentEpic;
const isLoading = epicLoading || storyLoading || parentEpicLoading;
if (isLoading) {
return (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Skeleton className="h-4 w-24" />
<ChevronRight className="h-4 w-4" />
<Skeleton className="h-4 w-32" />
</div>
);
}
return (
<nav className="flex items-center gap-2 text-sm" aria-label="Breadcrumb">
{/* Project */}
<Link
href={`/projects/${projectId}`}
className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
>
<Home className="h-4 w-4" />
{projectName && <span>{projectName}</span>}
</Link>
{/* Epic */}
{finalEpic && (
<>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
<Link
href={`/projects/${projectId}/epics/${finalEpic.id}`}
className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
>
<Folder className="h-4 w-4 text-blue-500" />
<span className="max-w-[200px] truncate">{finalEpic.title}</span>
</Link>
</>
)}
{/* Story */}
{effectiveStory && (
<>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
<Link
href={`/projects/${projectId}/stories/${effectiveStory.id}`}
className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
>
<FileText className="h-4 w-4 text-green-500" />
<span className="max-w-[200px] truncate">{effectiveStory.title}</span>
</Link>
</>
)}
{/* Task */}
{task && (
<>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
<div className="flex items-center gap-1 font-medium">
<CheckSquare className="h-4 w-4 text-purple-500" />
<span className="max-w-[200px] truncate">{task.title}</span>
</div>
</>
)}
</nav>
);
}