mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: add tests and implementation for issue discovery and queue pages
- Implemented `DiscoveryPage` with session management and findings display. - Added tests for `DiscoveryPage` to ensure proper rendering and functionality. - Created `QueuePage` for managing issue execution queues with stats and actions. - Added tests for `QueuePage` to verify UI elements and translations. - Introduced `useIssues` hooks for fetching and managing issue data. - Added loading skeletons and error handling for better user experience. - Created `vite-env.d.ts` for TypeScript support in Vite environment.
This commit is contained in:
@@ -70,6 +70,8 @@ export {
|
||||
useUpdateIssue,
|
||||
useDeleteIssue,
|
||||
useIssueMutations,
|
||||
useQueueMutations,
|
||||
useIssueDiscovery,
|
||||
issuesKeys,
|
||||
} from './useIssues';
|
||||
export type {
|
||||
@@ -79,6 +81,9 @@ export type {
|
||||
UseCreateIssueReturn,
|
||||
UseUpdateIssueReturn,
|
||||
UseDeleteIssueReturn,
|
||||
UseQueueMutationsReturn,
|
||||
FindingFilters,
|
||||
UseIssueDiscoveryReturn,
|
||||
} from './useIssues';
|
||||
|
||||
// ========== Skills ==========
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
type CliEndpoint,
|
||||
type CliEndpointsResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { workspaceQueryKeys } from '@/lib/queryKeys';
|
||||
|
||||
// Query key factory
|
||||
export const cliEndpointsKeys = {
|
||||
@@ -273,12 +275,15 @@ export interface UseHooksReturn {
|
||||
export function useHooks(options: UseHooksOptions = {}): UseHooksReturn {
|
||||
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: hooksKeys.lists(),
|
||||
queryFn: fetchHooks,
|
||||
queryKey: workspaceQueryKeys.rulesList(projectPath),
|
||||
queryFn: () => fetchHooks(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
@@ -381,12 +386,15 @@ export interface UseRulesReturn {
|
||||
export function useRules(options: UseRulesOptions = {}): UseRulesReturn {
|
||||
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: rulesKeys.lists(),
|
||||
queryFn: fetchRules,
|
||||
queryKey: workspaceQueryKeys.rulesList(projectPath),
|
||||
queryFn: () => fetchRules(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
fetchCommands,
|
||||
type Command,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const commandsKeys = {
|
||||
@@ -50,11 +51,14 @@ export function useCommands(options: UseCommandsOptions = {}): UseCommandsReturn
|
||||
const { filter, staleTime = STALE_TIME, enabled = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: commandsKeys.list(filter),
|
||||
queryFn: fetchCommands,
|
||||
queryFn: () => fetchCommands(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export function useDashboardStats(
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: workspaceQueryKeys.projectOverview(projectPath),
|
||||
queryFn: fetchDashboardStats,
|
||||
queryFn: () => fetchDashboardStats(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
|
||||
@@ -114,7 +114,7 @@ export function usePrefetchDashboardStats() {
|
||||
if (projectPath) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: workspaceQueryKeys.projectOverview(projectPath),
|
||||
queryFn: fetchDashboardStats,
|
||||
queryFn: () => fetchDashboardStats(projectPath),
|
||||
staleTime: STALE_TIME,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
deleteAllHistory,
|
||||
type HistoryResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const historyKeys = {
|
||||
@@ -70,11 +71,14 @@ export function useHistory(options: UseHistoryOptions = {}): UseHistoryReturn {
|
||||
const { filter, staleTime = STALE_TIME, enabled = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: historyKeys.list(filter),
|
||||
queryFn: fetchHistory,
|
||||
queryFn: () => fetchHistory(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type IndexStatus,
|
||||
type IndexRebuildRequest,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// ========== Query Keys ==========
|
||||
|
||||
@@ -52,11 +53,14 @@ export function useIndexStatus(options: UseIndexStatusOptions = {}): UseIndexSta
|
||||
const { staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: indexKeys.status(),
|
||||
queryFn: fetchIndexStatus,
|
||||
queryFn: () => fetchIndexStatus(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
enabled: queryEnabled,
|
||||
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
323
ccw/frontend/src/hooks/useIssues.test.tsx
Normal file
323
ccw/frontend/src/hooks/useIssues.test.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
// ========================================
|
||||
// useIssues Hook Tests
|
||||
// ========================================
|
||||
// Tests for issue-related hooks with queue and discovery
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
useIssueQueue,
|
||||
useIssueMutations,
|
||||
useQueueMutations,
|
||||
useIssueDiscovery,
|
||||
} from './useIssues';
|
||||
import * as api from '@/lib/api';
|
||||
|
||||
// Create a proper query client wrapper
|
||||
const createTestQueryClient = () => {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// Mock store
|
||||
vi.mock('@/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => '/test/path',
|
||||
selectProjectPath: () => '/test/path',
|
||||
}));
|
||||
|
||||
// Mock API - use vi.mocked for type safety
|
||||
vi.mock('@/lib/api', () => ({
|
||||
fetchIssueQueue: vi.fn(),
|
||||
activateQueue: vi.fn(),
|
||||
deactivateQueue: vi.fn(),
|
||||
deleteQueue: vi.fn(),
|
||||
mergeQueues: vi.fn(),
|
||||
fetchDiscoveries: vi.fn(),
|
||||
fetchDiscoveryFindings: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useIssueQueue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch queue data successfully', async () => {
|
||||
const mockQueue = {
|
||||
tasks: ['task1', 'task2'],
|
||||
solutions: ['solution1'],
|
||||
conflicts: [],
|
||||
execution_groups: { 'group-1': ['task1'] },
|
||||
grouped_items: { 'parallel-group': ['task1', 'task2'] },
|
||||
};
|
||||
|
||||
vi.mocked(api.fetchIssueQueue).mockResolvedValue(mockQueue);
|
||||
|
||||
const { result } = renderHook(() => useIssueQueue(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data).toEqual(mockQueue);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
vi.mocked(api.fetchIssueQueue).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const { result } = renderHook(() => useIssueQueue(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
// Verify the hook returns expected structure even with error
|
||||
expect(result.current).toHaveProperty('isLoading');
|
||||
expect(result.current).toHaveProperty('data');
|
||||
expect(result.current).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useQueueMutations', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should activate queue successfully', async () => {
|
||||
vi.mocked(api.activateQueue).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useQueueMutations(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await result.current.activateQueue('queue-1');
|
||||
|
||||
expect(api.activateQueue).toHaveBeenCalledWith('queue-1', '/test/path');
|
||||
});
|
||||
|
||||
it('should deactivate queue successfully', async () => {
|
||||
vi.mocked(api.deactivateQueue).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useQueueMutations(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await result.current.deactivateQueue();
|
||||
|
||||
expect(api.deactivateQueue).toHaveBeenCalledWith('/test/path');
|
||||
});
|
||||
|
||||
it('should delete queue successfully', async () => {
|
||||
vi.mocked(api.deleteQueue).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useQueueMutations(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await result.current.deleteQueue('queue-1');
|
||||
|
||||
expect(api.deleteQueue).toHaveBeenCalledWith('queue-1', '/test/path');
|
||||
});
|
||||
|
||||
it('should merge queues successfully', async () => {
|
||||
vi.mocked(api.mergeQueues).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useQueueMutations(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await result.current.mergeQueues('source-1', 'target-1');
|
||||
|
||||
expect(api.mergeQueues).toHaveBeenCalledWith('source-1', 'target-1', '/test/path');
|
||||
});
|
||||
|
||||
it('should track overall mutation state', () => {
|
||||
const { result } = renderHook(() => useQueueMutations(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isMutating).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useIssueDiscovery', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch discovery sessions successfully', async () => {
|
||||
const mockSessions = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Session 1',
|
||||
status: 'running' as const,
|
||||
progress: 50,
|
||||
findings_count: 5,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(api.fetchDiscoveries).mockResolvedValue(mockSessions);
|
||||
|
||||
const { result } = renderHook(() => useIssueDiscovery(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.sessions).toHaveLength(1);
|
||||
expect(result.current.sessions[0].name).toBe('Session 1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter findings by severity', async () => {
|
||||
const mockFindings = [
|
||||
{ id: '1', title: 'Critical issue', severity: 'critical' as const, type: 'bug', description: '' },
|
||||
{ id: '2', title: 'Minor issue', severity: 'low' as const, type: 'enhancement', description: '' },
|
||||
];
|
||||
|
||||
vi.mocked(api.fetchDiscoveries).mockResolvedValue([
|
||||
{ id: '1', name: 'Session 1', status: 'completed' as const, progress: 100, findings_count: 2, created_at: '2024-01-01T00:00:00Z' },
|
||||
]);
|
||||
vi.mocked(api.fetchDiscoveryFindings).mockResolvedValue(mockFindings);
|
||||
|
||||
const { result } = renderHook(() => useIssueDiscovery(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
// Wait for sessions to load
|
||||
await waitFor(() => {
|
||||
expect(result.current.sessions).toHaveLength(1);
|
||||
});
|
||||
|
||||
// Select a session to load findings
|
||||
result.current.selectSession('1');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.findings).toHaveLength(2);
|
||||
});
|
||||
|
||||
// Apply severity filter
|
||||
result.current.setFilters({ severity: 'critical' as const });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.filteredFindings).toHaveLength(1);
|
||||
expect(result.current.filteredFindings[0].severity).toBe('critical');
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter findings by type', async () => {
|
||||
const mockFindings = [
|
||||
{ id: '1', title: 'Bug 1', severity: 'high' as const, type: 'bug', description: '' },
|
||||
{ id: '2', title: 'Enhancement 1', severity: 'medium' as const, type: 'enhancement', description: '' },
|
||||
];
|
||||
|
||||
vi.mocked(api.fetchDiscoveries).mockResolvedValue([
|
||||
{ id: '1', name: 'Session 1', status: 'completed' as const, progress: 100, findings_count: 2, created_at: '2024-01-01T00:00:00Z' },
|
||||
]);
|
||||
vi.mocked(api.fetchDiscoveryFindings).mockResolvedValue(mockFindings);
|
||||
|
||||
const { result } = renderHook(() => useIssueDiscovery(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
// Wait for sessions to load
|
||||
await waitFor(() => {
|
||||
expect(result.current.sessions).toHaveLength(1);
|
||||
});
|
||||
|
||||
// Select a session to load findings
|
||||
result.current.selectSession('1');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.findings).toHaveLength(2);
|
||||
});
|
||||
|
||||
// Apply type filter
|
||||
result.current.setFilters({ type: 'bug' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.filteredFindings).toHaveLength(1);
|
||||
expect(result.current.filteredFindings[0].type).toBe('bug');
|
||||
});
|
||||
});
|
||||
|
||||
it('should search findings by text', async () => {
|
||||
const mockFindings = [
|
||||
{ id: '1', title: 'Authentication error', severity: 'high' as const, type: 'bug', description: 'Login fails' },
|
||||
{ id: '2', title: 'UI bug', severity: 'medium' as const, type: 'bug', description: 'Button color' },
|
||||
];
|
||||
|
||||
vi.mocked(api.fetchDiscoveries).mockResolvedValue([
|
||||
{ id: '1', name: 'Session 1', status: 'completed' as const, progress: 100, findings_count: 2, created_at: '2024-01-01T00:00:00Z' },
|
||||
]);
|
||||
vi.mocked(api.fetchDiscoveryFindings).mockResolvedValue(mockFindings);
|
||||
|
||||
const { result } = renderHook(() => useIssueDiscovery(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
// Wait for sessions to load
|
||||
await waitFor(() => {
|
||||
expect(result.current.sessions).toHaveLength(1);
|
||||
});
|
||||
|
||||
// Select a session to load findings
|
||||
result.current.selectSession('1');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.findings).toHaveLength(2);
|
||||
});
|
||||
|
||||
// Apply search filter
|
||||
result.current.setFilters({ search: 'authentication' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.filteredFindings).toHaveLength(1);
|
||||
expect(result.current.filteredFindings[0].title).toContain('Authentication');
|
||||
});
|
||||
});
|
||||
|
||||
it('should export findings as JSON', async () => {
|
||||
const mockFindings = [
|
||||
{ id: '1', title: 'Test finding', severity: 'high' as const, type: 'bug', description: 'Test' },
|
||||
];
|
||||
|
||||
vi.mocked(api.fetchDiscoveries).mockResolvedValue([
|
||||
{ id: '1', name: 'Session 1', status: 'completed' as const, progress: 100, findings_count: 1, created_at: '2024-01-01T00:00:00Z' },
|
||||
]);
|
||||
vi.mocked(api.fetchDiscoveryFindings).mockResolvedValue(mockFindings);
|
||||
|
||||
const { result } = renderHook(() => useIssueDiscovery(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
// Wait for sessions to load
|
||||
await waitFor(() => {
|
||||
expect(result.current.sessions).toHaveLength(1);
|
||||
});
|
||||
|
||||
// Select a session to load findings
|
||||
result.current.selectSession('1');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.findings).toHaveLength(1);
|
||||
});
|
||||
|
||||
// Verify exportFindings is available as a function
|
||||
expect(typeof result.current.exportFindings).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
// ========================================
|
||||
// TanStack Query hooks for issues with queue management
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient, type UseQueryResult } from '@tanstack/react-query';
|
||||
import {
|
||||
fetchIssues,
|
||||
fetchIssueHistory,
|
||||
@@ -11,11 +11,21 @@ import {
|
||||
createIssue,
|
||||
updateIssue,
|
||||
deleteIssue,
|
||||
activateQueue,
|
||||
deactivateQueue,
|
||||
deleteQueue as deleteQueueApi,
|
||||
mergeQueues as mergeQueuesApi,
|
||||
fetchDiscoveries,
|
||||
fetchDiscoveryFindings,
|
||||
type Issue,
|
||||
type IssueQueue,
|
||||
type IssuesResponse,
|
||||
type DiscoverySession,
|
||||
type Finding,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { workspaceQueryKeys } from '@/lib/queryKeys';
|
||||
import { useState, useMemo } from 'react';
|
||||
|
||||
// Query key factory
|
||||
export const issuesKeys = {
|
||||
@@ -181,9 +191,9 @@ export function useIssues(options: UseIssuesOptions = {}): UseIssuesReturn {
|
||||
/**
|
||||
* Hook for fetching issue queue
|
||||
*/
|
||||
export function useIssueQueue(): ReturnType<typeof useQuery> {
|
||||
export function useIssueQueue(): UseQueryResult<IssueQueue> {
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
return useQuery({
|
||||
return useQuery<IssueQueue>({
|
||||
queryKey: projectPath ? workspaceQueryKeys.issueQueue(projectPath) : ['issueQueue', 'no-project'],
|
||||
queryFn: () => fetchIssueQueue(projectPath),
|
||||
staleTime: STALE_TIME,
|
||||
@@ -288,3 +298,171 @@ export function useIssueMutations() {
|
||||
isMutating: create.isCreating || update.isUpdating || remove.isDeleting,
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Queue Mutations ==========
|
||||
|
||||
export interface UseQueueMutationsReturn {
|
||||
activateQueue: (queueId: string) => Promise<void>;
|
||||
deactivateQueue: () => Promise<void>;
|
||||
deleteQueue: (queueId: string) => Promise<void>;
|
||||
mergeQueues: (sourceId: string, targetId: string) => Promise<void>;
|
||||
isActivating: boolean;
|
||||
isDeactivating: boolean;
|
||||
isDeleting: boolean;
|
||||
isMerging: boolean;
|
||||
isMutating: boolean;
|
||||
}
|
||||
|
||||
export function useQueueMutations(): UseQueueMutationsReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const activateMutation = useMutation({
|
||||
mutationFn: (queueId: string) => activateQueue(queueId, projectPath),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.issueQueue(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
const deactivateMutation = useMutation({
|
||||
mutationFn: () => deactivateQueue(projectPath),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.issueQueue(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (queueId: string) => deleteQueueApi(queueId, projectPath),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.issueQueue(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
const mergeMutation = useMutation({
|
||||
mutationFn: ({ sourceId, targetId }: { sourceId: string; targetId: string }) =>
|
||||
mergeQueuesApi(sourceId, targetId, projectPath),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.issueQueue(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
activateQueue: activateMutation.mutateAsync,
|
||||
deactivateQueue: deactivateMutation.mutateAsync,
|
||||
deleteQueue: deleteMutation.mutateAsync,
|
||||
mergeQueues: (sourceId, targetId) => mergeMutation.mutateAsync({ sourceId, targetId }),
|
||||
isActivating: activateMutation.isPending,
|
||||
isDeactivating: deactivateMutation.isPending,
|
||||
isDeleting: deleteMutation.isPending,
|
||||
isMerging: mergeMutation.isPending,
|
||||
isMutating: activateMutation.isPending || deactivateMutation.isPending || deleteMutation.isPending || mergeMutation.isPending,
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Discovery Hook ==========
|
||||
|
||||
export interface FindingFilters {
|
||||
severity?: 'critical' | 'high' | 'medium' | 'low';
|
||||
type?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface UseIssueDiscoveryReturn {
|
||||
sessions: DiscoverySession[];
|
||||
activeSession: DiscoverySession | null;
|
||||
findings: Finding[];
|
||||
filteredFindings: Finding[];
|
||||
isLoadingSessions: boolean;
|
||||
isLoadingFindings: boolean;
|
||||
error: Error | null;
|
||||
filters: FindingFilters;
|
||||
setFilters: (filters: FindingFilters) => void;
|
||||
selectSession: (sessionId: string) => void;
|
||||
refetchSessions: () => void;
|
||||
exportFindings: () => void;
|
||||
}
|
||||
|
||||
export function useIssueDiscovery(options?: { refetchInterval?: number }): UseIssueDiscoveryReturn {
|
||||
const { refetchInterval = 0 } = options ?? {};
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
const [filters, setFilters] = useState<FindingFilters>({});
|
||||
|
||||
const sessionsQuery = useQuery({
|
||||
queryKey: workspaceQueryKeys.discoveries(projectPath),
|
||||
queryFn: () => fetchDiscoveries(projectPath),
|
||||
staleTime: STALE_TIME,
|
||||
enabled: !!projectPath,
|
||||
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const findingsQuery = useQuery({
|
||||
queryKey: activeSessionId ? ['discoveryFindings', activeSessionId, projectPath] : ['discoveryFindings', 'no-session'],
|
||||
queryFn: () => activeSessionId ? fetchDiscoveryFindings(activeSessionId, projectPath) : [],
|
||||
staleTime: STALE_TIME,
|
||||
enabled: !!activeSessionId && !!projectPath,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const activeSession = useMemo(
|
||||
() => sessionsQuery.data?.find(s => s.id === activeSessionId) ?? null,
|
||||
[sessionsQuery.data, activeSessionId]
|
||||
);
|
||||
|
||||
const filteredFindings = useMemo(() => {
|
||||
let findings = findingsQuery.data ?? [];
|
||||
if (filters.severity) {
|
||||
findings = findings.filter(f => f.severity === filters.severity);
|
||||
}
|
||||
if (filters.type) {
|
||||
findings = findings.filter(f => f.type === filters.type);
|
||||
}
|
||||
if (filters.search) {
|
||||
const searchLower = filters.search.toLowerCase();
|
||||
findings = findings.filter(f =>
|
||||
f.title.toLowerCase().includes(searchLower) ||
|
||||
f.description.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
return findings;
|
||||
}, [findingsQuery.data, filters]);
|
||||
|
||||
const selectSession = (sessionId: string) => {
|
||||
setActiveSessionId(sessionId);
|
||||
};
|
||||
|
||||
const exportFindings = () => {
|
||||
if (!activeSessionId || !findingsQuery.data) return;
|
||||
const data = {
|
||||
session: activeSession,
|
||||
findings: findingsQuery.data,
|
||||
exported_at: new Date().toISOString(),
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `discovery-${activeSessionId}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return {
|
||||
sessions: sessionsQuery.data ?? [],
|
||||
activeSession,
|
||||
findings: findingsQuery.data ?? [],
|
||||
filteredFindings,
|
||||
isLoadingSessions: sessionsQuery.isLoading,
|
||||
isLoadingFindings: findingsQuery.isLoading,
|
||||
error: sessionsQuery.error || findingsQuery.error,
|
||||
filters,
|
||||
setFilters,
|
||||
selectSession,
|
||||
refetchSessions: () => {
|
||||
sessionsQuery.refetch();
|
||||
},
|
||||
exportFindings,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { fetchLiteTasks, fetchLiteTaskSession, type LiteTaskSession, type LiteTasksResponse } from '@/lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { workspaceQueryKeys } from '@/lib/queryKeys';
|
||||
|
||||
type LiteTaskType = 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
|
||||
|
||||
@@ -18,6 +20,10 @@ interface UseLiteTasksOptions {
|
||||
*/
|
||||
export function useLiteTasks(options: UseLiteTasksOptions = {}) {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
// Only enable query when projectPath is available
|
||||
const queryEnabled = (options.enabled ?? true) && !!projectPath;
|
||||
|
||||
const {
|
||||
data = { litePlan: [], liteFix: [], multiCliPlan: [] },
|
||||
@@ -25,11 +31,11 @@ export function useLiteTasks(options: UseLiteTasksOptions = {}) {
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery<LiteTasksResponse>({
|
||||
queryKey: ['liteTasks'],
|
||||
queryFn: fetchLiteTasks,
|
||||
queryKey: workspaceQueryKeys.liteTasks(projectPath),
|
||||
queryFn: () => fetchLiteTasks(projectPath),
|
||||
staleTime: 30000,
|
||||
refetchInterval: options.refetchInterval,
|
||||
enabled: options.enabled ?? true,
|
||||
enabled: queryEnabled,
|
||||
});
|
||||
|
||||
// Get all sessions flattened
|
||||
@@ -55,7 +61,7 @@ export function useLiteTasks(options: UseLiteTasksOptions = {}) {
|
||||
const prefetchSession = (sessionId: string, type: LiteTaskType) => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['liteTask', sessionId, type],
|
||||
queryFn: () => fetchLiteTaskSession(sessionId, type),
|
||||
queryFn: () => fetchLiteTaskSession(sessionId, type, projectPath),
|
||||
staleTime: 60000,
|
||||
});
|
||||
};
|
||||
@@ -77,15 +83,20 @@ export function useLiteTasks(options: UseLiteTasksOptions = {}) {
|
||||
* Hook for fetching a single lite task session
|
||||
*/
|
||||
export function useLiteTaskSession(sessionId: string | undefined, type: LiteTaskType) {
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
// Only enable query when sessionId, type, and projectPath are available
|
||||
const queryEnabled = !!sessionId && !!type && !!projectPath;
|
||||
|
||||
const {
|
||||
data: session,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery<LiteTaskSession | null>({
|
||||
queryKey: ['liteTask', sessionId, type],
|
||||
queryFn: () => (sessionId ? fetchLiteTaskSession(sessionId, type) : Promise.resolve(null)),
|
||||
enabled: !!sessionId && !!type,
|
||||
queryKey: ['liteTask', sessionId, type, projectPath],
|
||||
queryFn: () => (sessionId ? fetchLiteTaskSession(sessionId, type, projectPath) : Promise.resolve(null)),
|
||||
enabled: queryEnabled,
|
||||
staleTime: 60000,
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type Loop,
|
||||
type LoopsResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const loopsKeys = {
|
||||
@@ -58,11 +59,14 @@ export function useLoops(options: UseLoopsOptions = {}): UseLoopsReturn {
|
||||
const { filter, staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: loopsKeys.list(filter),
|
||||
queryFn: fetchLoops,
|
||||
queryFn: () => fetchLoops(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
enabled: queryEnabled,
|
||||
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
|
||||
retry: 2,
|
||||
});
|
||||
@@ -129,10 +133,13 @@ export function useLoops(options: UseLoopsOptions = {}): UseLoopsReturn {
|
||||
* Hook for fetching a single loop
|
||||
*/
|
||||
export function useLoop(loopId: string, options: { enabled?: boolean } = {}) {
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = (options.enabled ?? !!loopId) && !!projectPath;
|
||||
|
||||
return useQuery({
|
||||
queryKey: loopsKeys.detail(loopId),
|
||||
queryFn: () => fetchLoop(loopId),
|
||||
enabled: options.enabled ?? !!loopId,
|
||||
queryFn: () => fetchLoop(loopId, projectPath),
|
||||
enabled: queryEnabled,
|
||||
staleTime: STALE_TIME,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type McpServer,
|
||||
type McpServersResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const mcpServersKeys = {
|
||||
@@ -50,11 +51,14 @@ export function useMcpServers(options: UseMcpServersOptions = {}): UseMcpServers
|
||||
const { scope, staleTime = STALE_TIME, enabled = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: mcpServersKeys.list(scope),
|
||||
queryFn: fetchMcpServers,
|
||||
queryFn: () => fetchMcpServers(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export function useMemory(options: UseMemoryOptions = {}): UseMemoryReturn {
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: workspaceQueryKeys.memoryList(projectPath),
|
||||
queryFn: fetchMemories,
|
||||
queryFn: () => fetchMemories(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
@@ -137,7 +137,7 @@ export function useCreateMemory(): UseCreateMemoryReturn {
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: createMemory,
|
||||
mutationFn: (input: { content: string; tags?: string[] }) => createMemory(input, projectPath),
|
||||
onSuccess: () => {
|
||||
// Invalidate memory cache to trigger refetch
|
||||
queryClient.invalidateQueries({ queryKey: projectPath ? workspaceQueryKeys.memory(projectPath) : ['memory'] });
|
||||
@@ -163,7 +163,7 @@ export function useUpdateMemory(): UseUpdateMemoryReturn {
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ memoryId, input }: { memoryId: string; input: Partial<CoreMemory> }) =>
|
||||
updateMemory(memoryId, input),
|
||||
updateMemory(memoryId, input, projectPath),
|
||||
onSuccess: () => {
|
||||
// Invalidate memory cache to trigger refetch
|
||||
queryClient.invalidateQueries({ queryKey: projectPath ? workspaceQueryKeys.memory(projectPath) : ['memory'] });
|
||||
@@ -188,7 +188,7 @@ export function useDeleteMemory(): UseDeleteMemoryReturn {
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: deleteMemory,
|
||||
mutationFn: (memoryId: string) => deleteMemory(memoryId, projectPath),
|
||||
onSuccess: () => {
|
||||
// Invalidate to ensure sync with server
|
||||
queryClient.invalidateQueries({ queryKey: projectPath ? workspaceQueryKeys.memory(projectPath) : ['memory'] });
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchProjectOverview } from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const projectOverviewKeys = {
|
||||
@@ -33,11 +34,14 @@ export interface UseProjectOverviewOptions {
|
||||
export function useProjectOverview(options: UseProjectOverviewOptions = {}) {
|
||||
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: projectOverviewKeys.detail(),
|
||||
queryFn: fetchProjectOverview,
|
||||
queryFn: () => fetchProjectOverview(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type PromptsResponse,
|
||||
type PromptInsightsResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const promptHistoryKeys = {
|
||||
@@ -63,11 +64,14 @@ export function usePromptHistory(options: UsePromptHistoryOptions = {}): UseProm
|
||||
const { filter, staleTime = STALE_TIME, enabled = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: promptHistoryKeys.list(filter),
|
||||
queryFn: fetchPrompts,
|
||||
queryFn: () => fetchPrompts(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
@@ -159,11 +163,14 @@ export function usePromptHistory(options: UsePromptHistoryOptions = {}): UseProm
|
||||
export function usePromptInsights(options: { enabled?: boolean; staleTime?: number } = {}) {
|
||||
const { enabled = true, staleTime = STALE_TIME } = options;
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!projectPath;
|
||||
|
||||
return useQuery({
|
||||
queryKey: promptHistoryKeys.insights(),
|
||||
queryFn: fetchPromptInsights,
|
||||
queryFn: () => fetchPromptInsights(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchSessionDetail } from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
export const sessionDetailKeys = {
|
||||
@@ -33,11 +34,14 @@ export interface UseSessionDetailOptions {
|
||||
export function useSessionDetail(sessionId: string, options: UseSessionDetailOptions = {}) {
|
||||
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const queryEnabled = enabled && !!sessionId && !!projectPath;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: sessionDetailKeys.detail(sessionId),
|
||||
queryFn: () => fetchSessionDetail(sessionId),
|
||||
queryFn: () => fetchSessionDetail(sessionId, projectPath),
|
||||
staleTime,
|
||||
enabled: enabled && !!sessionId,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ export function useSessions(options: UseSessionsOptions = {}): UseSessionsReturn
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: workspaceQueryKeys.sessionsList(projectPath),
|
||||
queryFn: fetchSessions,
|
||||
queryFn: () => fetchSessions(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
|
||||
|
||||
@@ -63,7 +63,7 @@ export function useSkills(options: UseSkillsOptions = {}): UseSkillsReturn {
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: workspaceQueryKeys.skillsList(projectPath),
|
||||
queryFn: fetchSkills,
|
||||
queryFn: () => fetchSkills(projectPath),
|
||||
staleTime,
|
||||
enabled: queryEnabled,
|
||||
retry: 2,
|
||||
|
||||
Reference in New Issue
Block a user