Files
ColaFlow-Web/components/epics/epic-form.tsx
Yaojia Wang 04ba00d108 fix(frontend): Align Epic field names with backend API
Fix frontend-backend API field mismatches for Epic entity by:
1. Changed Epic.title to Epic.name in type definitions
2. Added Epic.createdBy field (required by backend)
3. Updated all Epic references from epic.title to epic.name
4. Fixed Epic form to use name field and include createdBy

Files modified:
- types/project.ts: Updated Epic, CreateEpicDto, UpdateEpicDto interfaces
- components/epics/epic-form.tsx: Fixed defaultValues to use epic.name
- components/projects/hierarchy-tree.tsx: Replaced epic.title with epic.name
- components/projects/story-form.tsx: Fixed epic dropdown to show epic.name
- app/(dashboard)/projects/[id]/epics/page.tsx: Display epic.name in list
- app/(dashboard)/projects/[id]/page.tsx: Display epic.name in preview
- app/(dashboard)/api-test/page.tsx: Display epic.name in test page

This resolves the 400 Bad Request error when creating Epics caused by
missing 'Name' field (was sending 'title' instead) and missing 'CreatedBy' field.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 13:30:48 +01:00

230 lines
6.8 KiB
TypeScript

'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useCreateEpic, useUpdateEpic } from '@/lib/hooks/use-epics';
import type { Epic, WorkItemPriority } from '@/types/project';
import { toast } from 'sonner';
import { Loader2 } from 'lucide-react';
import { useAuthStore } from '@/stores/authStore';
const epicSchema = z.object({
name: z
.string()
.min(1, 'Title is required')
.max(200, 'Title must be less than 200 characters'),
description: z
.string()
.max(2000, 'Description must be less than 2000 characters')
.optional(),
priority: z.enum(['Low', 'Medium', 'High', 'Critical']),
estimatedHours: z
.number()
.min(0, 'Estimated hours must be positive')
.optional()
.or(z.literal('')),
});
type EpicFormValues = z.infer<typeof epicSchema>;
interface EpicFormProps {
projectId: string;
epic?: Epic;
onSuccess?: () => void;
onCancel?: () => void;
}
export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps) {
const isEditing = !!epic;
const createEpic = useCreateEpic();
const updateEpic = useUpdateEpic();
const user = useAuthStore((state) => state.user);
const form = useForm<EpicFormValues>({
resolver: zodResolver(epicSchema),
defaultValues: {
name: epic?.name || '', // Fixed: use 'name' instead of 'title'
description: epic?.description || '',
priority: epic?.priority || 'Medium',
estimatedHours: epic?.estimatedHours || ('' as any),
},
});
async function onSubmit(data: EpicFormValues) {
try {
if (!user?.id) {
toast.error('User not authenticated');
return;
}
const payload = {
...data,
estimatedHours: data.estimatedHours || undefined,
};
if (isEditing) {
await updateEpic.mutateAsync({
id: epic.id,
data: payload,
});
} else {
await createEpic.mutateAsync({
projectId,
createdBy: user.id,
...payload,
});
}
onSuccess?.();
} catch (error) {
const message = error instanceof Error ? error.message : 'Operation failed';
toast.error(message);
}
}
const isLoading = createEpic.isPending || updateEpic.isPending;
const priorityOptions: Array<{ value: WorkItemPriority; label: string; color: string }> = [
{ value: 'Low', label: 'Low', color: 'text-blue-600' },
{ value: 'Medium', label: 'Medium', color: 'text-yellow-600' },
{ value: 'High', label: 'High', color: 'text-orange-600' },
{ value: 'Critical', label: 'Critical', color: 'text-red-600' },
];
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Epic Title *</FormLabel>
<FormControl>
<Input placeholder="e.g., User Authentication System" {...field} />
</FormControl>
<FormDescription>
A concise title describing this epic
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Detailed description of the epic, including goals and acceptance criteria..."
className="resize-none"
rows={6}
{...field}
/>
</FormControl>
<FormDescription>
Optional detailed description (max 2000 characters)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>Priority *</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select priority" />
</SelectTrigger>
</FormControl>
<SelectContent>
{priorityOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span className={option.color}>{option.label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Set the priority level for this epic
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="estimatedHours"
render={({ field }) => (
<FormItem>
<FormLabel>Estimated Hours</FormLabel>
<FormControl>
<Input
type="number"
placeholder="e.g., 40"
{...field}
onChange={(e) => {
const value = e.target.value;
field.onChange(value === '' ? '' : Number(value));
}}
/>
</FormControl>
<FormDescription>
Optional time estimate in hours
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end gap-3 pt-4">
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isLoading}
>
Cancel
</Button>
)}
<Button type="submit" disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isEditing ? 'Update Epic' : 'Create Epic'}
</Button>
</div>
</form>
</Form>
);
}