fix(ui): add missing cache invalidation for React Query

- Add cache invalidation to useBoardPersistence after create/update/delete
- Add useAutoModeQueryInvalidation to board-view for WebSocket events
- Add cache invalidation to github-issues-view after converting issue to task
- Add cache invalidation to analysis-view after generating features
- Fix UI not updating when features are added, updated, or completed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-15 19:10:35 +01:00
parent 9dbec7281a
commit 3170e22383
5 changed files with 46 additions and 6 deletions

View File

@@ -1,7 +1,9 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import { useQueryClient } from '@tanstack/react-query';
import { useAppStore, FileTreeNode, ProjectAnalysis } from '@/store/app-store'; import { useAppStore, FileTreeNode, ProjectAnalysis } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@@ -72,6 +74,7 @@ export function AnalysisView() {
const [isGeneratingFeatureList, setIsGeneratingFeatureList] = useState(false); const [isGeneratingFeatureList, setIsGeneratingFeatureList] = useState(false);
const [featureListGenerated, setFeatureListGenerated] = useState(false); const [featureListGenerated, setFeatureListGenerated] = useState(false);
const [featureListError, setFeatureListError] = useState<string | null>(null); const [featureListError, setFeatureListError] = useState<string | null>(null);
const queryClient = useQueryClient();
// Recursively scan directory // Recursively scan directory
const scanDirectory = useCallback( const scanDirectory = useCallback(
@@ -647,6 +650,11 @@ ${Object.entries(projectAnalysis.filesByExtension)
} as any); } as any);
} }
// Invalidate React Query cache to sync UI
queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject.path),
});
setFeatureListGenerated(true); setFeatureListGenerated(true);
} catch (error) { } catch (error) {
logger.error('Failed to generate feature list:', error); logger.error('Failed to generate feature list:', error);
@@ -656,7 +664,7 @@ ${Object.entries(projectAnalysis.filesByExtension)
} finally { } finally {
setIsGeneratingFeatureList(false); setIsGeneratingFeatureList(false);
} }
}, [currentProject, projectAnalysis]); }, [currentProject, projectAnalysis, queryClient]);
// Toggle folder expansion // Toggle folder expansion
const toggleFolder = (path: string) => { const toggleFolder = (path: string) => {

View File

@@ -82,6 +82,7 @@ import { useInitScriptEvents } from '@/hooks/use-init-script-events';
import { usePipelineConfig } from '@/hooks/queries'; import { usePipelineConfig } from '@/hooks/queries';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys'; import { queryKeys } from '@/lib/query-keys';
import { useAutoModeQueryInvalidation } from '@/hooks/use-query-invalidation';
// Stable empty array to avoid infinite loop in selector // Stable empty array to avoid infinite loop in selector
const EMPTY_WORKTREES: ReturnType<ReturnType<typeof useAppStore.getState>['getWorktrees']> = []; const EMPTY_WORKTREES: ReturnType<ReturnType<typeof useAppStore.getState>['getWorktrees']> = [];
@@ -115,6 +116,9 @@ export function BoardView() {
// Fetch pipeline config via React Query // Fetch pipeline config via React Query
const { data: pipelineConfig } = usePipelineConfig(currentProject?.path); const { data: pipelineConfig } = usePipelineConfig(currentProject?.path);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// Subscribe to auto mode events for React Query cache invalidation
useAutoModeQueryInvalidation(currentProject?.path);
// Subscribe to worktreePanelVisibleByProject to trigger re-renders when it changes // Subscribe to worktreePanelVisibleByProject to trigger re-renders when it changes
const worktreePanelVisibleByProject = useAppStore((state) => state.worktreePanelVisibleByProject); const worktreePanelVisibleByProject = useAppStore((state) => state.worktreePanelVisibleByProject);
// Subscribe to showInitScriptIndicatorByProject to trigger re-renders when it changes // Subscribe to showInitScriptIndicatorByProject to trigger re-renders when it changes

View File

@@ -1,8 +1,10 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Feature } from '@/store/app-store'; import { Feature } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import { queryKeys } from '@/lib/query-keys';
const logger = createLogger('BoardPersistence'); const logger = createLogger('BoardPersistence');
@@ -12,6 +14,7 @@ interface UseBoardPersistenceProps {
export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps) { export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps) {
const { updateFeature } = useAppStore(); const { updateFeature } = useAppStore();
const queryClient = useQueryClient();
// Persist feature update to API (replaces saveFeatures) // Persist feature update to API (replaces saveFeatures)
const persistFeatureUpdate = useCallback( const persistFeatureUpdate = useCallback(
@@ -41,12 +44,16 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
); );
if (result.success && result.feature) { if (result.success && result.feature) {
updateFeature(result.feature.id, result.feature); updateFeature(result.feature.id, result.feature);
// Invalidate React Query cache to sync UI
queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject.path),
});
} }
} catch (error) { } catch (error) {
logger.error('Failed to persist feature update:', error); logger.error('Failed to persist feature update:', error);
} }
}, },
[currentProject, updateFeature] [currentProject, updateFeature, queryClient]
); );
// Persist feature creation to API // Persist feature creation to API
@@ -64,12 +71,16 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
const result = await api.features.create(currentProject.path, feature); const result = await api.features.create(currentProject.path, feature);
if (result.success && result.feature) { if (result.success && result.feature) {
updateFeature(result.feature.id, result.feature); updateFeature(result.feature.id, result.feature);
// Invalidate React Query cache to sync UI
queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject.path),
});
} }
} catch (error) { } catch (error) {
logger.error('Failed to persist feature creation:', error); logger.error('Failed to persist feature creation:', error);
} }
}, },
[currentProject, updateFeature] [currentProject, updateFeature, queryClient]
); );
// Persist feature deletion to API // Persist feature deletion to API
@@ -85,11 +96,15 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
} }
await api.features.delete(currentProject.path, featureId); await api.features.delete(currentProject.path, featureId);
// Invalidate React Query cache to sync UI
queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject.path),
});
} catch (error) { } catch (error) {
logger.error('Failed to persist feature deletion:', error); logger.error('Failed to persist feature deletion:', error);
} }
}, },
[currentProject] [currentProject, queryClient]
); );
return { return {

View File

@@ -2,6 +2,7 @@
import { useState, useCallback, useMemo } from 'react'; import { useState, useCallback, useMemo } from 'react';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import { CircleDot, RefreshCw } from 'lucide-react'; import { CircleDot, RefreshCw } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { getElectronAPI, GitHubIssue, IssueValidationResult } from '@/lib/electron'; import { getElectronAPI, GitHubIssue, IssueValidationResult } from '@/lib/electron';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { ConfirmDialog } from '@/components/ui/confirm-dialog';
@@ -9,6 +10,7 @@ import { LoadingState } from '@/components/ui/loading-state';
import { ErrorState } from '@/components/ui/error-state'; import { ErrorState } from '@/components/ui/error-state';
import { cn, pathsEqual } from '@/lib/utils'; import { cn, pathsEqual } from '@/lib/utils';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { queryKeys } from '@/lib/query-keys';
import { useGithubIssues, useIssueValidation } from './github-issues-view/hooks'; import { useGithubIssues, useIssueValidation } from './github-issues-view/hooks';
import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components'; import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components';
import { ValidationDialog } from './github-issues-view/dialogs'; import { ValidationDialog } from './github-issues-view/dialogs';
@@ -27,6 +29,7 @@ export function GitHubIssuesView() {
useState<ValidateIssueOptions | null>(null); useState<ValidateIssueOptions | null>(null);
const { currentProject, getCurrentWorktree, worktreesByProject } = useAppStore(); const { currentProject, getCurrentWorktree, worktreesByProject } = useAppStore();
const queryClient = useQueryClient();
// Model override for validation // Model override for validation
const validationModelOverride = useModelOverride({ phase: 'validationModel' }); const validationModelOverride = useModelOverride({ phase: 'validationModel' });
@@ -109,6 +112,10 @@ export function GitHubIssuesView() {
const result = await api.features.create(currentProject.path, feature); const result = await api.features.create(currentProject.path, feature);
if (result.success) { if (result.success) {
// Invalidate React Query cache to sync UI
queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject.path),
});
toast.success(`Created task: ${issue.title}`); toast.success(`Created task: ${issue.title}`);
} else { } else {
toast.error(result.error || 'Failed to create task'); toast.error(result.error || 'Failed to create task');
@@ -119,7 +126,7 @@ export function GitHubIssuesView() {
toast.error(err instanceof Error ? err.message : 'Failed to create task'); toast.error(err instanceof Error ? err.message : 'Failed to create task');
} }
}, },
[currentProject?.path, currentBranch] [currentProject?.path, currentBranch, queryClient]
); );
if (loading) { if (loading) {

View File

@@ -143,7 +143,13 @@ export function useGitHubValidationQueryInvalidation(projectPath: string | undef
if (!projectPath) return; if (!projectPath) return;
const api = getElectronAPI(); const api = getElectronAPI();
const unsubscribe = api.github?.onValidationEvent((event: IssueValidationEvent) => {
// Check if GitHub API is available before subscribing
if (!api.github?.onValidationEvent) {
return;
}
const unsubscribe = api.github.onValidationEvent((event: IssueValidationEvent) => {
if (event.type === 'validation_complete' || event.type === 'validation_error') { if (event.type === 'validation_complete' || event.type === 'validation_error') {
// Invalidate all validations for this project // Invalidate all validations for this project
queryClient.invalidateQueries({ queryClient.invalidateQueries({