feat(frontend): Create Sprint 4 Stories and Tasks for Story Management

Created comprehensive Story and Task files for Sprint 4 frontend implementation:

Story 1: Story Detail Page Foundation (P0 Critical - 3 days)
- 6 tasks: route creation, header, sidebar, data loading, Edit/Delete, responsive design
- Fixes critical 404 error when clicking Story cards
- Two-column layout consistent with Epic detail page

Story 2: Task Management in Story Detail (P0 Critical - 2 days)
- 6 tasks: API verification, hooks, TaskList, TaskCard, TaskForm, integration
- Complete Task CRUD with checkbox status toggle
- Filters, sorting, and optimistic UI updates

Story 3: Enhanced Story Form (P1 High - 2 days)
- 6 tasks: acceptance criteria, assignee selector, tags, story points, integration
- Aligns with UX design specification
- Backward compatible with existing Stories

Story 4: Quick Add Story Workflow (P1 High - 2 days)
- 5 tasks: inline form, keyboard shortcuts, batch creation, navigation
- Rapid Story creation with minimal fields
- Keyboard shortcut (Cmd/Ctrl + N)

Story 5: Story Card Component (P2 Medium - 1 day)
- 4 tasks: component variants, visual states, Task count, optimization
- Reusable component with list/kanban/compact variants
- React.memo optimization

Story 6: Kanban Story Creation Enhancement (P2 Optional - 2 days)
- 4 tasks: Epic card enhancement, inline form, animation, real-time updates
- Contextual Story creation from Kanban
- Stretch goal - implement only if ahead of schedule

Total: 6 Stories, 31 Tasks, 12 days estimated
Priority breakdown: P0 (2), P1 (2), P2 (2 optional)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Yaojia Wang
2025-11-05 21:49:57 +01:00
parent b3c92042ed
commit 88d6413f81
18 changed files with 3629 additions and 0 deletions

View File

@@ -0,0 +1,449 @@
---
task_id: sprint_4_story_2_task_2
story_id: sprint_4_story_2
sprint_id: sprint_4
status: not_started
type: frontend
assignee: Frontend Developer 2
created_date: 2025-11-05
estimated_hours: 3
---
# Task 2: Create Task API Client and React Query Hooks
## Description
Create the Task API client module and React Query hooks for Task CRUD operations. This establishes the data layer for Task management with optimistic updates, caching, and error handling.
## What to Do
1. Add Task API client methods to `lib/api/pm.ts`
2. Create `lib/hooks/use-tasks.ts` with React Query hooks
3. Implement optimistic updates for instant UI feedback
4. Add cache invalidation strategies
5. Add error handling with toast notifications
6. Add logger integration for debugging
7. Test all hooks with real API data
8. Document hook usage and examples
## Files to Create/Modify
- `lib/api/pm.ts` (modify, add Task methods ~100 lines)
- `lib/hooks/use-tasks.ts` (new, ~150-200 lines)
## Implementation Details
### Task API Client (`lib/api/pm.ts`)
```typescript
// lib/api/pm.ts
// Add these Task API methods
import { apiClient } from './client';
import type {
Task,
CreateTaskDto,
UpdateTaskDto,
ChangeTaskStatusDto,
AssignTaskDto,
} from '@/types/project';
export const tasksApi = {
/**
* Get all Tasks for a Story
*/
list: async (storyId: string): Promise<Task[]> => {
const response = await apiClient.get(`/api/v1/stories/${storyId}/tasks`);
return response.data;
},
/**
* Get single Task by ID
*/
get: async (id: string): Promise<Task> => {
const response = await apiClient.get(`/api/v1/tasks/${id}`);
return response.data;
},
/**
* Create new Task
*/
create: async (data: CreateTaskDto): Promise<Task> => {
const response = await apiClient.post('/api/v1/tasks', data);
return response.data;
},
/**
* Update Task
*/
update: async (id: string, data: UpdateTaskDto): Promise<Task> => {
const response = await apiClient.put(`/api/v1/tasks/${id}`, data);
return response.data;
},
/**
* Delete Task
*/
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/api/v1/tasks/${id}`);
},
/**
* Change Task status
*/
changeStatus: async (id: string, status: string): Promise<Task> => {
const response = await apiClient.put(`/api/v1/tasks/${id}/status`, { status });
return response.data;
},
/**
* Assign Task to user
*/
assign: async (id: string, assigneeId: string): Promise<Task> => {
const response = await apiClient.put(`/api/v1/tasks/${id}/assign`, { assigneeId });
return response.data;
},
};
```
### React Query Hooks (`lib/hooks/use-tasks.ts`)
```typescript
// lib/hooks/use-tasks.ts
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { tasksApi } from '@/lib/api/pm';
import { toast } from 'sonner';
import { logger } from '@/lib/utils/logger';
import type { Task, CreateTaskDto, UpdateTaskDto } from '@/types/project';
/**
* Get all Tasks for a Story
*/
export function useTasks(storyId: string | undefined) {
return useQuery({
queryKey: ['tasks', storyId],
queryFn: () => {
if (!storyId) throw new Error('Story ID required');
return tasksApi.list(storyId);
},
enabled: !!storyId,
staleTime: 60_000, // Consider fresh for 1 minute
});
}
/**
* Get single Task by ID
*/
export function useTask(id: string | undefined) {
return useQuery({
queryKey: ['tasks', id],
queryFn: () => {
if (!id) throw new Error('Task ID required');
return tasksApi.get(id);
},
enabled: !!id,
});
}
/**
* Create new Task with optimistic update
*/
export function useCreateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: CreateTaskDto) => {
logger.info('Creating task:', data);
return tasksApi.create(data);
},
onMutate: async (newTask) => {
// Cancel outgoing queries
await queryClient.cancelQueries({ queryKey: ['tasks', newTask.storyId] });
// Snapshot previous value
const previousTasks = queryClient.getQueryData<Task[]>(['tasks', newTask.storyId]);
// Optimistically update (optional for creates)
// Usually we just wait for server response
return { previousTasks };
},
onSuccess: (newTask, variables) => {
logger.info('Task created successfully:', newTask);
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['tasks', variables.storyId] });
queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] }); // Update Story task count
toast.success('Task created successfully');
},
onError: (error, variables, context) => {
logger.error('Failed to create task:', error);
// Restore previous state if optimistic update was used
if (context?.previousTasks) {
queryClient.setQueryData(['tasks', variables.storyId], context.previousTasks);
}
toast.error('Failed to create Task');
},
});
}
/**
* Update Task with optimistic update
*/
export function useUpdateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, data }: { id: string; data: UpdateTaskDto }) => {
logger.info('Updating task:', id, data);
return tasksApi.update(id, data);
},
onMutate: async ({ id, data }) => {
// Cancel queries
await queryClient.cancelQueries({ queryKey: ['tasks', id] });
// Snapshot previous
const previousTask = queryClient.getQueryData<Task>(['tasks', id]);
// Optimistically update
queryClient.setQueryData<Task>(['tasks', id], (old) => {
if (!old) return old;
return { ...old, ...data };
});
return { previousTask };
},
onSuccess: (updatedTask) => {
logger.info('Task updated successfully:', updatedTask);
// Invalidate queries
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.id] });
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.storyId] });
toast.success('Task updated successfully');
},
onError: (error, variables, context) => {
logger.error('Failed to update task:', error);
// Revert optimistic update
if (context?.previousTask) {
queryClient.setQueryData(['tasks', variables.id], context.previousTask);
}
toast.error('Failed to update Task');
},
});
}
/**
* Delete Task
*/
export function useDeleteTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
logger.info('Deleting task:', id);
return tasksApi.delete(id);
},
onSuccess: (_, deletedId) => {
logger.info('Task deleted successfully:', deletedId);
// Remove from cache
queryClient.removeQueries({ queryKey: ['tasks', deletedId] });
// Invalidate lists
queryClient.invalidateQueries({ queryKey: ['tasks'] });
toast.success('Task deleted successfully');
},
onError: (error) => {
logger.error('Failed to delete task:', error);
toast.error('Failed to delete Task');
},
});
}
/**
* Change Task status (for checkbox toggle)
*/
export function useChangeTaskStatus() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ taskId, status }: { taskId: string; status: string }) => {
logger.info('Changing task status:', taskId, status);
return tasksApi.changeStatus(taskId, status);
},
onMutate: async ({ taskId, status }) => {
// Cancel queries
await queryClient.cancelQueries({ queryKey: ['tasks', taskId] });
// Snapshot previous
const previousTask = queryClient.getQueryData<Task>(['tasks', taskId]);
// Optimistically update status
queryClient.setQueryData<Task>(['tasks', taskId], (old) => {
if (!old) return old;
return { ...old, status };
});
// Also update in list
const storyId = previousTask?.storyId;
if (storyId) {
queryClient.setQueryData<Task[]>(['tasks', storyId], (old) => {
if (!old) return old;
return old.map((task) =>
task.id === taskId ? { ...task, status } : task
);
});
}
return { previousTask };
},
onSuccess: (updatedTask) => {
logger.info('Task status changed successfully:', updatedTask);
// Invalidate queries
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.id] });
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.storyId] });
queryClient.invalidateQueries({ queryKey: ['stories', updatedTask.storyId] }); // Update Story progress
toast.success(`Task marked as ${updatedTask.status}`);
},
onError: (error, variables, context) => {
logger.error('Failed to change task status:', error);
// Revert optimistic update
if (context?.previousTask) {
queryClient.setQueryData(['tasks', variables.taskId], context.previousTask);
}
toast.error('Failed to update Task status');
},
});
}
/**
* Assign Task to user
*/
export function useAssignTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ taskId, assigneeId }: { taskId: string; assigneeId: string }) => {
logger.info('Assigning task:', taskId, assigneeId);
return tasksApi.assign(taskId, assigneeId);
},
onSuccess: (updatedTask) => {
logger.info('Task assigned successfully:', updatedTask);
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.id] });
queryClient.invalidateQueries({ queryKey: ['tasks', updatedTask.storyId] });
toast.success('Task assigned successfully');
},
onError: (error) => {
logger.error('Failed to assign task:', error);
toast.error('Failed to assign Task');
},
});
}
```
## Acceptance Criteria
- [ ] Task API client methods added to `lib/api/pm.ts`
- [ ] All CRUD operations implemented (list, get, create, update, delete)
- [ ] `use-tasks.ts` hooks file created
- [ ] `useTasks(storyId)` hook returns Task list for Story
- [ ] `useCreateTask()` hook creates Task with optimistic update
- [ ] `useUpdateTask()` hook updates Task with optimistic update
- [ ] `useDeleteTask()` hook deletes Task
- [ ] `useChangeTaskStatus()` hook changes status with optimistic update
- [ ] All hooks include error handling with toast notifications
- [ ] All hooks include logger integration
- [ ] Cache invalidation strategies implemented correctly
- [ ] Optimistic updates provide instant UI feedback
- [ ] Hooks tested with real API data
## Testing
**Manual Testing**:
```typescript
// Test in a React component
function TestTaskHooks() {
const { data: tasks, isLoading } = useTasks('story-123');
const createTask = useCreateTask();
const handleCreate = () => {
createTask.mutate({
storyId: 'story-123',
title: 'Test Task',
priority: 'High',
estimatedHours: 8,
createdBy: 'user-123',
});
};
return (
<div>
<button onClick={handleCreate}>Create Task</button>
{isLoading && <p>Loading...</p>}
{tasks?.map((task) => (
<div key={task.id}>{task.title}</div>
))}
</div>
);
}
```
**Test Cases**:
1. Create Task → Verify appears in list immediately (optimistic update)
2. Update Task → Verify updates instantly
3. Delete Task → Verify removes from list
4. Change status → Verify checkbox updates instantly
5. Error handling → Disconnect internet → Verify error toast shows
6. Cache invalidation → Create Task → Verify Story task count updates
## Dependencies
**Prerequisites**:
- Task 1 (API endpoints verified, types created)
- ✅ React Query configured
- ✅ apiClient ready (`lib/api/client.ts`)
- ✅ logger utility (`lib/utils/logger.ts`)
- ✅ sonner toast library
**Blocks**:
- Task 3, 4, 5 (components depend on hooks)
## Estimated Time
3 hours
## Notes
**Optimistic Updates**: Provide instant UI feedback by updating cache immediately, then reverting on error. This makes the app feel fast and responsive.
**Cache Invalidation**: When creating/updating/deleting Tasks, also invalidate the parent Story query to update task counts and progress indicators.
**Code Reuse**: Copy patterns from `use-stories.ts` hook - Task hooks are very similar to Story hooks.