/** * Issue Management React Query Hooks * * Hooks for fetching and mutating issues. * Uses TanStack Query for server state management. * * Note: Until backend API is implemented, these hooks use mock data. * The API integration points are marked for future implementation. * * @module features/issues/hooks/useIssues */ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import type { IssueSummary, IssueDetail, IssueFilters, IssueSort, IssueUpdateRequest, IssueBulkActionRequest, PaginatedIssuesResponse, } from '../types'; import { mockIssues, mockIssueDetail } from '../mocks'; /** * Query keys for issues */ export const issueKeys = { all: ['issues'] as const, lists: () => [...issueKeys.all, 'list'] as const, list: (projectId: string, filters?: IssueFilters, sort?: IssueSort) => [...issueKeys.lists(), projectId, filters, sort] as const, details: () => [...issueKeys.all, 'detail'] as const, detail: (issueId: string) => [...issueKeys.details(), issueId] as const, }; /** * Mock filtering and sorting logic * This simulates server-side filtering until API is ready */ function filterAndSortIssues( issues: IssueSummary[], filters?: IssueFilters, sort?: IssueSort ): IssueSummary[] { let result = [...issues]; if (filters) { // Search filter if (filters.search) { const searchLower = filters.search.toLowerCase(); result = result.filter( (issue) => issue.title.toLowerCase().includes(searchLower) || issue.description.toLowerCase().includes(searchLower) || issue.number.toString().includes(searchLower) ); } // Status filter if (filters.status && filters.status !== 'all') { result = result.filter((issue) => issue.status === filters.status); } // Priority filter if (filters.priority && filters.priority !== 'all') { result = result.filter((issue) => issue.priority === filters.priority); } // Sprint filter if (filters.sprint && filters.sprint !== 'all') { if (filters.sprint === 'backlog') { result = result.filter((issue) => !issue.sprint); } else { result = result.filter((issue) => issue.sprint === filters.sprint); } } // Assignee filter if (filters.assignee && filters.assignee !== 'all') { if (filters.assignee === 'unassigned') { result = result.filter((issue) => !issue.assignee); } else { result = result.filter((issue) => issue.assignee?.id === filters.assignee); } } } // Sorting if (sort) { const direction = sort.direction === 'asc' ? 1 : -1; result.sort((a, b) => { switch (sort.field) { case 'number': return (a.number - b.number) * direction; case 'priority': { const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 }; return (priorityOrder[a.priority] - priorityOrder[b.priority]) * direction; } case 'updated_at': return (new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime()) * direction; case 'created_at': return (new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) * direction; default: return 0; } }); } return result; } /** * Hook to fetch paginated issues for a project * * @param projectId - Project ID * @param filters - Optional filters * @param sort - Optional sort configuration * @param page - Page number (1-based) * @param pageSize - Number of items per page */ export function useIssues( projectId: string, filters?: IssueFilters, sort?: IssueSort, page: number = 1, pageSize: number = 25 ) { return useQuery({ queryKey: issueKeys.list(projectId, filters, sort), queryFn: async (): Promise => { // TODO: Replace with actual API call when backend is ready // const response = await getProjectIssues({ // path: { project_id: projectId }, // query: { ...filters, ...sort, page, page_size: pageSize }, // }); // Simulate API delay await new Promise((resolve) => setTimeout(resolve, 300)); const filteredIssues = filterAndSortIssues(mockIssues, filters, sort); const start = (page - 1) * pageSize; const paginatedIssues = filteredIssues.slice(start, start + pageSize); return { data: paginatedIssues, pagination: { total: filteredIssues.length, page, page_size: pageSize, total_pages: Math.ceil(filteredIssues.length / pageSize), has_next: start + pageSize < filteredIssues.length, has_prev: page > 1, }, }; }, staleTime: 30000, // 30 seconds }); } /** * Hook to fetch a single issue detail * * @param issueId - Issue ID */ export function useIssue(issueId: string) { return useQuery({ queryKey: issueKeys.detail(issueId), queryFn: async (): Promise => { // TODO: Replace with actual API call when backend is ready // const response = await getIssue({ // path: { issue_id: issueId }, // }); // Simulate API delay await new Promise((resolve) => setTimeout(resolve, 200)); // Return mock detail for any issue ID return { ...mockIssueDetail, id: issueId, }; }, staleTime: 30000, enabled: !!issueId, }); } /** * Hook to update an issue */ export function useUpdateIssue() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ issueId, data, }: { issueId: string; data: IssueUpdateRequest; }): Promise => { // TODO: Replace with actual API call when backend is ready // const response = await updateIssue({ // path: { issue_id: issueId }, // body: data, // }); // Simulate API delay await new Promise((resolve) => setTimeout(resolve, 300)); // Return updated mock data - only apply non-label fields from data return { ...mockIssueDetail, id: issueId, title: data.title || mockIssueDetail.title, description: data.description || mockIssueDetail.description, status: data.status || mockIssueDetail.status, priority: data.priority || mockIssueDetail.priority, sprint: data.sprint !== undefined ? data.sprint : mockIssueDetail.sprint, due_date: data.due_date !== undefined ? data.due_date : mockIssueDetail.due_date, }; }, onSuccess: (data) => { // Invalidate and update cache queryClient.invalidateQueries({ queryKey: issueKeys.lists() }); queryClient.setQueryData(issueKeys.detail(data.id), data); }, }); } /** * Hook to update issue status (optimistic update) */ export function useUpdateIssueStatus() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ issueId, status, }: { issueId: string; status: IssueUpdateRequest['status']; }): Promise => { // TODO: Replace with actual API call await new Promise((resolve) => setTimeout(resolve, 300)); return { ...mockIssueDetail, id: issueId, status: status || mockIssueDetail.status, }; }, onMutate: async ({ issueId, status }) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: issueKeys.detail(issueId) }); // Snapshot previous value const previousIssue = queryClient.getQueryData(issueKeys.detail(issueId)); // Optimistically update if (previousIssue && status) { queryClient.setQueryData(issueKeys.detail(issueId), { ...previousIssue, status, }); } return { previousIssue }; }, onError: (_err, { issueId }, context) => { // Rollback on error if (context?.previousIssue) { queryClient.setQueryData(issueKeys.detail(issueId), context.previousIssue); } }, onSettled: () => { // Invalidate lists to refetch queryClient.invalidateQueries({ queryKey: issueKeys.lists() }); }, }); } /** * Hook for bulk actions on issues */ export function useBulkIssueAction() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (request: IssueBulkActionRequest): Promise<{ affected_count: number }> => { // TODO: Replace with actual API call await new Promise((resolve) => setTimeout(resolve, 500)); return { affected_count: request.issue_ids.length }; }, onSuccess: () => { // Invalidate all issue queries queryClient.invalidateQueries({ queryKey: issueKeys.all }); }, }); } /** * Hook to sync an issue with external tracker */ export function useSyncIssue() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ issueId }: { issueId: string }): Promise => { // TODO: Replace with actual API call // const response = await syncIssue({ // path: { issue_id: issueId }, // body: { direction: 'bidirectional' }, // }); await new Promise((resolve) => setTimeout(resolve, 1000)); const issue = mockIssues.find((i) => i.id === issueId); return { ...(issue || mockIssues[0]), sync_status: 'synced', }; }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: issueKeys.lists() }); queryClient.invalidateQueries({ queryKey: issueKeys.detail(data.id) }); }, }); }