Add complete Task CRUD UI for Story detail page with inline creation, status toggling, filtering, and sorting capabilities. Changes: - Created TaskList component with filters, sorting, and progress bar - Created TaskCard component with checkbox status toggle and metadata - Created TaskQuickAdd component for inline Task creation - Added shadcn/ui checkbox and alert components - All components use existing Task hooks (useTasks, useCreateTask, etc.) Components: - components/tasks/task-list.tsx (150 lines) - components/tasks/task-card.tsx (160 lines) - components/tasks/task-quick-add.tsx (180 lines) - components/ui/checkbox.tsx (shadcn/ui) - components/ui/alert.tsx (shadcn/ui) Features: - Task list with real-time count and progress bar - Filter by: All, Active, Completed - Sort by: Recent, Alphabetical, Status - Checkbox toggle for instant status change (optimistic UI) - Inline Quick Add form for fast Task creation - Priority badges and metadata display - Loading states and error handling - Empty state messaging Sprint 4 Story 2: Task Management in Story Detail Task 3: Implement TaskList, TaskCard, TaskQuickAdd components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
191 lines
5.5 KiB
TypeScript
191 lines
5.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { z } from 'zod';
|
|
import { useCreateTask } from '@/lib/hooks/use-tasks';
|
|
import { CreateTaskDto, WorkItemPriority } from '@/types/project';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@/components/ui/form';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Plus, X } from 'lucide-react';
|
|
|
|
interface TaskQuickAddProps {
|
|
storyId: string;
|
|
}
|
|
|
|
const taskSchema = z.object({
|
|
title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
|
|
priority: z.enum(['Critical', 'High', 'Medium', 'Low']),
|
|
estimatedHours: z.coerce.number().min(0).optional(),
|
|
});
|
|
|
|
type TaskFormData = z.infer<typeof taskSchema>;
|
|
|
|
export function TaskQuickAdd({ storyId }: TaskQuickAddProps) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const createTask = useCreateTask();
|
|
|
|
const form = useForm<TaskFormData>({
|
|
resolver: zodResolver(taskSchema),
|
|
defaultValues: {
|
|
title: '',
|
|
priority: 'Medium',
|
|
estimatedHours: undefined,
|
|
},
|
|
});
|
|
|
|
const onSubmit = async (data: TaskFormData) => {
|
|
const taskData: CreateTaskDto = {
|
|
storyId,
|
|
title: data.title,
|
|
priority: data.priority as WorkItemPriority,
|
|
estimatedHours: data.estimatedHours,
|
|
};
|
|
|
|
createTask.mutate(taskData, {
|
|
onSuccess: () => {
|
|
form.reset();
|
|
// Keep form open for batch creation
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
form.reset();
|
|
setIsOpen(false);
|
|
};
|
|
|
|
if (!isOpen) {
|
|
return (
|
|
<Button
|
|
onClick={() => setIsOpen(true)}
|
|
variant="outline"
|
|
className="w-full"
|
|
size="sm"
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Add Task
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card className="border-dashed">
|
|
<CardContent className="pt-4">
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h4 className="text-sm font-medium">Quick Add Task</h4>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleCancel}
|
|
className="h-6 w-6 p-0"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="title"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Title *</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="e.g., Implement login API"
|
|
{...field}
|
|
autoFocus
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<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>
|
|
<SelectItem value="Critical">Critical</SelectItem>
|
|
<SelectItem value="High">High</SelectItem>
|
|
<SelectItem value="Medium">Medium</SelectItem>
|
|
<SelectItem value="Low">Low</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="estimatedHours"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Est. Hours</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="number"
|
|
placeholder="8"
|
|
{...field}
|
|
value={field.value ?? ''}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="submit"
|
|
size="sm"
|
|
disabled={createTask.isPending}
|
|
className="flex-1"
|
|
>
|
|
{createTask.isPending ? 'Creating...' : 'Add Task'}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleCancel}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|