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>
164 lines
5.1 KiB
TypeScript
164 lines
5.1 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { storiesApi } from '@/lib/api/pm';
|
|
import type { Story, CreateStoryDto, UpdateStoryDto, WorkItemStatus } from '@/types/project';
|
|
import { toast } from 'sonner';
|
|
|
|
// ==================== Query Hooks ====================
|
|
export function useStories(epicId?: string) {
|
|
return useQuery<Story[]>({
|
|
queryKey: ['stories', epicId],
|
|
queryFn: async () => {
|
|
console.log('[useStories] Fetching stories...', { epicId });
|
|
try {
|
|
const result = await storiesApi.list(epicId);
|
|
console.log('[useStories] Fetch successful:', result);
|
|
return result;
|
|
} catch (error) {
|
|
console.error('[useStories] Fetch failed:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
retry: 1,
|
|
});
|
|
}
|
|
|
|
export function useStory(id: string) {
|
|
return useQuery<Story>({
|
|
queryKey: ['stories', id],
|
|
queryFn: () => storiesApi.get(id),
|
|
enabled: !!id,
|
|
});
|
|
}
|
|
|
|
// ==================== Mutation Hooks ====================
|
|
export function useCreateStory() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: CreateStoryDto) => storiesApi.create(data),
|
|
onSuccess: (newStory) => {
|
|
// Invalidate all story queries
|
|
queryClient.invalidateQueries({ queryKey: ['stories'] });
|
|
|
|
// Also invalidate epic details
|
|
queryClient.invalidateQueries({ queryKey: ['epics', newStory.epicId] });
|
|
|
|
toast.success('Story created successfully!');
|
|
},
|
|
onError: (error: any) => {
|
|
console.error('[useCreateStory] Error:', error);
|
|
toast.error(error.response?.data?.detail || 'Failed to create story');
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateStory() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: UpdateStoryDto }) =>
|
|
storiesApi.update(id, data),
|
|
onMutate: async ({ id, data }) => {
|
|
await queryClient.cancelQueries({ queryKey: ['stories', id] });
|
|
|
|
const previousStory = queryClient.getQueryData<Story>(['stories', id]);
|
|
|
|
queryClient.setQueryData<Story>(['stories', id], (old) => ({
|
|
...old!,
|
|
...data,
|
|
}));
|
|
|
|
return { previousStory };
|
|
},
|
|
onError: (error: any, variables, context) => {
|
|
console.error('[useUpdateStory] Error:', error);
|
|
|
|
if (context?.previousStory) {
|
|
queryClient.setQueryData(['stories', variables.id], context.previousStory);
|
|
}
|
|
|
|
toast.error(error.response?.data?.detail || 'Failed to update story');
|
|
},
|
|
onSuccess: () => {
|
|
toast.success('Story updated successfully!');
|
|
},
|
|
onSettled: (_, __, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['stories', variables.id] });
|
|
queryClient.invalidateQueries({ queryKey: ['stories'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteStory() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (id: string) => storiesApi.delete(id),
|
|
onSuccess: (_, id) => {
|
|
queryClient.invalidateQueries({ queryKey: ['stories'] });
|
|
queryClient.removeQueries({ queryKey: ['stories', id] });
|
|
toast.success('Story deleted successfully!');
|
|
},
|
|
onError: (error: any) => {
|
|
console.error('[useDeleteStory] Error:', error);
|
|
toast.error(error.response?.data?.detail || 'Failed to delete story');
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useChangeStoryStatus() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ id, status }: { id: string; status: WorkItemStatus }) =>
|
|
storiesApi.changeStatus(id, status),
|
|
onMutate: async ({ id, status }) => {
|
|
await queryClient.cancelQueries({ queryKey: ['stories', id] });
|
|
|
|
const previousStory = queryClient.getQueryData<Story>(['stories', id]);
|
|
|
|
queryClient.setQueryData<Story>(['stories', id], (old) => ({
|
|
...old!,
|
|
status,
|
|
}));
|
|
|
|
return { previousStory };
|
|
},
|
|
onError: (error: any, variables, context) => {
|
|
console.error('[useChangeStoryStatus] Error:', error);
|
|
|
|
if (context?.previousStory) {
|
|
queryClient.setQueryData(['stories', variables.id], context.previousStory);
|
|
}
|
|
|
|
toast.error(error.response?.data?.detail || 'Failed to change story status');
|
|
},
|
|
onSuccess: () => {
|
|
toast.success('Story status changed successfully!');
|
|
},
|
|
onSettled: (_, __, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['stories', variables.id] });
|
|
queryClient.invalidateQueries({ queryKey: ['stories'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useAssignStory() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ id, assigneeId }: { id: string; assigneeId: string }) =>
|
|
storiesApi.assign(id, assigneeId),
|
|
onSuccess: (_, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['stories', variables.id] });
|
|
queryClient.invalidateQueries({ queryKey: ['stories'] });
|
|
toast.success('Story assigned successfully!');
|
|
},
|
|
onError: (error: any) => {
|
|
console.error('[useAssignStory] Error:', error);
|
|
toast.error(error.response?.data?.detail || 'Failed to assign story');
|
|
},
|
|
});
|
|
}
|