Files
ColaFlow-Web/lib/hooks/use-stories.ts
Yaojia Wang 90e3d2416c feat(frontend): Refactor Kanban board to focus on Story management
Refactored the Kanban board from a mixed Epic/Story/Task view to focus exclusively on Stories, which are the right granularity for Kanban management.

Changes:
- Created StoryCard component with Epic breadcrumb, priority badges, and estimated hours display
- Updated KanbanColumn to use Story type and display epic names
- Created CreateStoryDialog for story creation with epic selection
- Added useProjectStories hook to fetch all stories across epics for a project
- Refactored Kanban page to show Stories only with drag-and-drop status updates
- Updated SignalR event handlers to focus on Story events only
- Changed UI text from 'New Issue' to 'New Story' and 'update issue status' to 'update story status'
- Implemented story status change via useChangeStoryStatus hook

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 15:03:12 +01:00

201 lines
6.4 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { storiesApi } from '@/lib/api/pm';
import { epicsApi } 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,
});
}
// Fetch all stories for a project (by fetching epics first, then all stories)
export function useProjectStories(projectId?: string) {
return useQuery<Story[]>({
queryKey: ['project-stories', projectId],
queryFn: async () => {
if (!projectId) {
throw new Error('projectId is required');
}
console.log('[useProjectStories] Fetching all stories for project...', { projectId });
try {
// First fetch all epics for the project
const epics = await epicsApi.list(projectId);
console.log('[useProjectStories] Epics fetched:', epics.length);
// Then fetch stories for each epic
const storiesPromises = epics.map((epic) => storiesApi.list(epic.id));
const storiesArrays = await Promise.all(storiesPromises);
// Flatten the array of arrays into a single array
const allStories = storiesArrays.flat();
console.log('[useProjectStories] Total stories fetched:', allStories.length);
return allStories;
} catch (error) {
console.error('[useProjectStories] Fetch failed:', error);
throw error;
}
},
enabled: !!projectId,
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');
},
});
}