feat(frontend): Implement Phase 1 - ProjectManagement API Client & Hooks
Add complete API integration for ProjectManagement module: - Epics, Stories, Tasks API clients - React Query hooks for all entities - Updated type definitions to match backend API - API test page for connection verification Changes: - Update lib/api/config.ts: Add all ProjectManagement endpoints - Update types/project.ts: Match backend API models (Epic, Story, Task) - Create lib/api/pm.ts: API clients for Epics, Stories, Tasks - Create lib/hooks/use-epics.ts: React Query hooks for Epic CRUD - Create lib/hooks/use-stories.ts: React Query hooks for Story CRUD - Create lib/hooks/use-tasks.ts: React Query hooks for Task CRUD - Create app/(dashboard)/api-test/page.tsx: API connection test page Features: - Full CRUD operations for Epics, Stories, Tasks - Status change and assignment operations - Optimistic updates for better UX - Error handling with toast notifications - Query invalidation for cache consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
167
lib/hooks/use-epics.ts
Normal file
167
lib/hooks/use-epics.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { epicsApi } from '@/lib/api/pm';
|
||||
import type { Epic, CreateEpicDto, UpdateEpicDto, WorkItemStatus } from '@/types/project';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// ==================== Query Hooks ====================
|
||||
export function useEpics(projectId?: string) {
|
||||
return useQuery<Epic[]>({
|
||||
queryKey: ['epics', projectId],
|
||||
queryFn: async () => {
|
||||
console.log('[useEpics] Fetching epics...', { projectId });
|
||||
try {
|
||||
const result = await epicsApi.list(projectId);
|
||||
console.log('[useEpics] Fetch successful:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[useEpics] Fetch failed:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: 1,
|
||||
});
|
||||
}
|
||||
|
||||
export function useEpic(id: string) {
|
||||
return useQuery<Epic>({
|
||||
queryKey: ['epics', id],
|
||||
queryFn: () => epicsApi.get(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Mutation Hooks ====================
|
||||
export function useCreateEpic() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateEpicDto) => epicsApi.create(data),
|
||||
onSuccess: (newEpic) => {
|
||||
// Invalidate all epic queries (including filtered by projectId)
|
||||
queryClient.invalidateQueries({ queryKey: ['epics'] });
|
||||
|
||||
// Also invalidate project details if exists
|
||||
queryClient.invalidateQueries({ queryKey: ['projects', newEpic.projectId] });
|
||||
|
||||
toast.success('Epic created successfully!');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('[useCreateEpic] Error:', error);
|
||||
toast.error(error.response?.data?.detail || 'Failed to create epic');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateEpic() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: UpdateEpicDto }) =>
|
||||
epicsApi.update(id, data),
|
||||
onMutate: async ({ id, data }) => {
|
||||
// Cancel outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: ['epics', id] });
|
||||
|
||||
// Snapshot previous value
|
||||
const previousEpic = queryClient.getQueryData<Epic>(['epics', id]);
|
||||
|
||||
// Optimistically update
|
||||
queryClient.setQueryData<Epic>(['epics', id], (old) => ({
|
||||
...old!,
|
||||
...data,
|
||||
}));
|
||||
|
||||
return { previousEpic };
|
||||
},
|
||||
onError: (error: any, variables, context) => {
|
||||
console.error('[useUpdateEpic] Error:', error);
|
||||
|
||||
// Rollback
|
||||
if (context?.previousEpic) {
|
||||
queryClient.setQueryData(['epics', variables.id], context.previousEpic);
|
||||
}
|
||||
|
||||
toast.error(error.response?.data?.detail || 'Failed to update epic');
|
||||
},
|
||||
onSuccess: (updatedEpic) => {
|
||||
toast.success('Epic updated successfully!');
|
||||
},
|
||||
onSettled: (_, __, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['epics', variables.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['epics'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteEpic() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => epicsApi.delete(id),
|
||||
onSuccess: (_, id) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['epics'] });
|
||||
queryClient.removeQueries({ queryKey: ['epics', id] });
|
||||
toast.success('Epic deleted successfully!');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('[useDeleteEpic] Error:', error);
|
||||
toast.error(error.response?.data?.detail || 'Failed to delete epic');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useChangeEpicStatus() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, status }: { id: string; status: WorkItemStatus }) =>
|
||||
epicsApi.changeStatus(id, status),
|
||||
onMutate: async ({ id, status }) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['epics', id] });
|
||||
|
||||
const previousEpic = queryClient.getQueryData<Epic>(['epics', id]);
|
||||
|
||||
queryClient.setQueryData<Epic>(['epics', id], (old) => ({
|
||||
...old!,
|
||||
status,
|
||||
}));
|
||||
|
||||
return { previousEpic };
|
||||
},
|
||||
onError: (error: any, variables, context) => {
|
||||
console.error('[useChangeEpicStatus] Error:', error);
|
||||
|
||||
if (context?.previousEpic) {
|
||||
queryClient.setQueryData(['epics', variables.id], context.previousEpic);
|
||||
}
|
||||
|
||||
toast.error(error.response?.data?.detail || 'Failed to change epic status');
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Epic status changed successfully!');
|
||||
},
|
||||
onSettled: (_, __, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['epics', variables.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['epics'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssignEpic() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, assigneeId }: { id: string; assigneeId: string }) =>
|
||||
epicsApi.assign(id, assigneeId),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['epics', variables.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['epics'] });
|
||||
toast.success('Epic assigned successfully!');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('[useAssignEpic] Error:', error);
|
||||
toast.error(error.response?.data?.detail || 'Failed to assign epic');
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user