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>
This commit is contained in:
110
components/projects/work-item-breadcrumb.tsx
Normal file
110
components/projects/work-item-breadcrumb.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user