Add comprehensive Epic detail page at /epics/[id] with full CRUD operations. Changes: - Created Epic detail page with breadcrumb navigation - Display Epic details: name, description, status, priority, time estimates - Show list of Stories belonging to the Epic with card view - Add Edit Epic functionality (opens dialog with form) - Add Create/Edit/Delete Story functionality under Epic - Fix Epic type inconsistency (name vs title) across components - Update Kanban page to map Epic.name to title for unified interface - Update epic-form to use 'name' field and add createdBy support - Update work-item-breadcrumb to use Epic.name instead of title Technical improvements: - Use Shadcn UI components for consistent design - Implement optimistic updates with React Query - Add loading and error states with skeletons - Follow Next.js App Router patterns with async params - Add delete confirmation dialogs for Epic and Stories 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
111 lines
3.3 KiB
TypeScript
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.name}</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>
|
|
);
|
|
}
|