Files
ColaFlow-Web/lib/hooks/use-epics.ts
Yaojia Wang ea67d90880 fix(frontend): Fix critical type safety issues from code review
Address all Critical and High Priority issues identified in frontend code review report:

Critical Issues Fixed:
- Created unified logger utility (lib/utils/logger.ts) to replace all console.log statements
- Consolidated User type definitions - removed duplicate from authStore, using single source from types/user.ts
- Eliminated 'any' types in API client - added proper generic types with AxiosRequestConfig
- Fixed SignalR ConnectionManager - replaced 'any' with generic types <T>
- Created API error types (lib/types/errors.ts) with ApiError and getErrorMessage helper
- Fixed IssueCard component - removed all type assertions, created discriminated union types for Kanban items
- Added React.memo to IssueCard for performance optimization
- Added proper ARIA labels and accessibility attributes to IssueCard

High Priority Issues Fixed:
- Fixed hardcoded user ID in CreateProjectDialog - now uses actual user from authStore
- Added useCallback to CreateProjectDialog onSubmit handler
- Fixed error handlers in use-epics.ts - replaced 'any' with ApiError type
- Updated all error handling to use logger and getErrorMessage

Type Safety Improvements:
- Created KanbanItem discriminated union (KanbanEpic | KanbanStory | KanbanTask) with proper type guards
- Added 'never' types to prevent invalid property access
- Fixed User interface to include all required fields (createdAt, updatedAt)
- Maintained backward compatibility with LegacyKanbanBoard for existing code

Files Changed:
- lib/utils/logger.ts - New centralized logging utility
- lib/types/errors.ts - New API error types and helpers
- types/user.ts - Consolidated User type with TenantRole
- types/kanban.ts - New discriminated union types for type-safe Kanban items
- components/features/kanban/IssueCard.tsx - Type-safe with React.memo
- components/features/projects/CreateProjectDialog.tsx - Fixed hardcoded user ID, added useCallback
- lib/api/client.ts - Eliminated 'any', added proper generics
- lib/signalr/ConnectionManager.ts - Replaced console.log, added generics
- lib/hooks/use-epics.ts - Fixed error handler types
- stores/authStore.ts - Removed duplicate User type
- lib/hooks/useAuth.ts - Added createdAt field to User

TypeScript compilation:  All type checks passing (0 errors)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 19:11:48 +01:00

170 lines
5.2 KiB
TypeScript

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';
import { logger } from '@/lib/utils/logger';
import { ApiError, getErrorMessage } from '@/lib/types/errors';
// ==================== Query Hooks ====================
export function useEpics(projectId?: string) {
return useQuery<Epic[]>({
queryKey: ['epics', projectId],
queryFn: async () => {
logger.debug('[useEpics] Fetching epics', { projectId });
try {
const result = await epicsApi.list(projectId);
logger.debug('[useEpics] Fetch successful', result);
return result;
} catch (error) {
logger.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: ApiError) => {
logger.error('[useCreateEpic] Error', error);
toast.error(getErrorMessage(error));
},
});
}
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: ApiError, variables, context) => {
logger.error('[useUpdateEpic] Error', error);
// Rollback
if (context?.previousEpic) {
queryClient.setQueryData(['epics', variables.id], context.previousEpic);
}
toast.error(getErrorMessage(error));
},
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: ApiError) => {
logger.error('[useDeleteEpic] Error', error);
toast.error(getErrorMessage(error));
},
});
}
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: ApiError, variables, context) => {
logger.error('[useChangeEpicStatus] Error', error);
if (context?.previousEpic) {
queryClient.setQueryData(['epics', variables.id], context.previousEpic);
}
toast.error(getErrorMessage(error));
},
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: ApiError) => {
logger.error('[useAssignEpic] Error', error);
toast.error(getErrorMessage(error));
},
});
}