fix(github): use selectedPR from hook to restore Files changed list (#822)
* fix(github): use selectedPR from hook to restore Files changed list
The hook useGitHubPRs returns a selectedPR that includes full PR details
including the files array and changedFiles count. GitHubPRs.tsx was ignoring
this and doing its own lookup in the prs array (which only contains list-view
PRs without file details). This caused the Files changed list to appear empty
in the PR detail view.
Fixes ACS-173
* fix(github): add null-safe fallbacks for PR additions/deletions counts
The GitHub API may return null for additions, deletions, and changed_files
fields in certain edge cases (e.g., draft PRs, PRs with no diff yet).
Add null-safe fallbacks (?? 0) to ensure the frontend always receives
numeric values instead of null.
Also added debug logging to inspect the raw API response for troubleshooting.
Related to ACS-173
* refactor: standardize selected item pattern across issues/PRs hooks
This addresses PR review findings about inconsistent patterns:
1. Fix UI flicker in useGitHubPRs hook
- Don't clear previous PR details when switching PRs
- Preserve previous details during fetch to avoid empty state
2. Add selectedIssue to useGitLabIssues hook
- Return computed selectedIssue instead of manual lookup
- Update GitLabIssues.tsx to use hook-provided value
3. Add selectedIssue to useGitHubIssues hook
- Return computed selectedIssue instead of manual lookup
- Update GitHubIssues.tsx to use hook-provided value
Related to ACS-173
* fix(pr): prevent stale data and race conditions when switching PRs
Fixes two HIGH priority issues from PR review:
1. Stale PR data when switching between PRs
- Validate that selectedPRDetails.number matches selectedPRNumber
- Added useMemo wrapper for consistency with other hooks
- Previously, old PR data (with its file list) was briefly shown
under new PR's header until fetch completed
2. Race condition for out-of-order API responses
- Track current PR being fetched in module-level variable
- Only update selectedPRDetails if response matches current PR
- Prevents stale responses from overwriting newer data
Related to ACS-173
* refactor(pr): address code quality issues from PR review
Fixes 4 issues identified during PR review:
1. Replace module-level mutable variable with per-hook ref
- Removed module-level currentFetchPRNumber variable
- Added currentFetchPRNumberRef using useRef inside hook
- Prevents shared state across hook instances
2. Fix fetchPRs useCallback dependency array
- Removed setNewCommitsCheckAction from dependencies
- Function doesn't reference it, so it wasn't needed
3. Remove async modifier from fire-and-forget functions
- runReview and runFollowupReview don't await anything
- Store functions return void, not Promise
- Updated interface to reflect void return type
4. Normalize API response to camelCase in handler layer
- Updated checkNewCommits handler comment for clarity
- Removed defensive fallbacks and "as any" casts in hook
- Data is now properly camelCased by the handler
Related to ACS-173
---------
Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com>
Co-authored-by: Alex <63423455+AlexMadera@users.noreply.github.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,13 @@
|
||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useProjectStore } from '../stores/project-store';
|
||||
import { useTaskStore } from '../stores/task-store';
|
||||
import { useGitHubIssues, useGitHubInvestigation, useIssueFiltering, useAutoFix } from './github-issues/hooks';
|
||||
import { useAnalyzePreview } from './github-issues/hooks/useAnalyzePreview';
|
||||
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||
import { useProjectStore } from "../stores/project-store";
|
||||
import { useTaskStore } from "../stores/task-store";
|
||||
import {
|
||||
useGitHubIssues,
|
||||
useGitHubInvestigation,
|
||||
useIssueFiltering,
|
||||
useAutoFix,
|
||||
} from "./github-issues/hooks";
|
||||
import { useAnalyzePreview } from "./github-issues/hooks/useAnalyzePreview";
|
||||
import {
|
||||
NotConnectedState,
|
||||
EmptyState,
|
||||
@@ -10,11 +15,11 @@ import {
|
||||
IssueList,
|
||||
IssueDetail,
|
||||
InvestigationDialog,
|
||||
BatchReviewWizard
|
||||
} from './github-issues/components';
|
||||
import { GitHubSetupModal } from './GitHubSetupModal';
|
||||
import type { GitHubIssue } from '../../shared/types';
|
||||
import type { GitHubIssuesProps } from './github-issues/types';
|
||||
BatchReviewWizard,
|
||||
} from "./github-issues/components";
|
||||
import { GitHubSetupModal } from "./GitHubSetupModal";
|
||||
import type { GitHubIssue } from "../../shared/types";
|
||||
import type { GitHubIssuesProps } from "./github-issues/types";
|
||||
|
||||
export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesProps) {
|
||||
const projects = useProjectStore((state) => state.projects);
|
||||
@@ -28,19 +33,20 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
|
||||
isLoading,
|
||||
error,
|
||||
selectedIssueNumber,
|
||||
selectedIssue,
|
||||
filterState,
|
||||
selectIssue,
|
||||
getFilteredIssues,
|
||||
getOpenIssuesCount,
|
||||
handleRefresh,
|
||||
handleFilterChange
|
||||
handleFilterChange,
|
||||
} = useGitHubIssues(selectedProject?.id);
|
||||
|
||||
const {
|
||||
investigationStatus,
|
||||
lastInvestigationResult,
|
||||
startInvestigation,
|
||||
resetInvestigationStatus
|
||||
resetInvestigationStatus,
|
||||
} = useGitHubInvestigation(selectedProject?.id);
|
||||
|
||||
const { searchQuery, setSearchQuery, filteredIssues } = useIssueFiltering(getFilteredIssues());
|
||||
@@ -66,15 +72,16 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
|
||||
closeWizard,
|
||||
startAnalysis,
|
||||
approveBatches,
|
||||
} = useAnalyzePreview({ projectId: selectedProject?.id || '' });
|
||||
} = useAnalyzePreview({ projectId: selectedProject?.id || "" });
|
||||
|
||||
const [showInvestigateDialog, setShowInvestigateDialog] = useState(false);
|
||||
const [selectedIssueForInvestigation, setSelectedIssueForInvestigation] = useState<GitHubIssue | null>(null);
|
||||
const [selectedIssueForInvestigation, setSelectedIssueForInvestigation] =
|
||||
useState<GitHubIssue | null>(null);
|
||||
const [showGitHubSetup, setShowGitHubSetup] = useState(false);
|
||||
|
||||
// Show GitHub setup modal when module is not installed
|
||||
useEffect(() => {
|
||||
if (analysisError?.includes('GitHub automation module not installed')) {
|
||||
if (analysisError?.includes("GitHub automation module not installed")) {
|
||||
setShowGitHubSetup(true);
|
||||
}
|
||||
}, [analysisError]);
|
||||
@@ -104,34 +111,30 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
|
||||
setShowInvestigateDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleStartInvestigation = useCallback((selectedCommentIds: number[]) => {
|
||||
if (selectedIssueForInvestigation) {
|
||||
startInvestigation(selectedIssueForInvestigation, selectedCommentIds);
|
||||
}
|
||||
}, [selectedIssueForInvestigation, startInvestigation]);
|
||||
const handleStartInvestigation = useCallback(
|
||||
(selectedCommentIds: number[]) => {
|
||||
if (selectedIssueForInvestigation) {
|
||||
startInvestigation(selectedIssueForInvestigation, selectedCommentIds);
|
||||
}
|
||||
},
|
||||
[selectedIssueForInvestigation, startInvestigation]
|
||||
);
|
||||
|
||||
const handleCloseDialog = useCallback(() => {
|
||||
setShowInvestigateDialog(false);
|
||||
resetInvestigationStatus();
|
||||
}, [resetInvestigationStatus]);
|
||||
|
||||
const selectedIssue = issues.find(i => i.number === selectedIssueNumber);
|
||||
|
||||
// Not connected state
|
||||
if (!syncStatus?.connected) {
|
||||
return (
|
||||
<NotConnectedState
|
||||
error={syncStatus?.error || null}
|
||||
onOpenSettings={onOpenSettings}
|
||||
/>
|
||||
);
|
||||
return <NotConnectedState error={syncStatus?.error || null} onOpenSettings={onOpenSettings} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<IssueListHeader
|
||||
repoFullName={syncStatus.repoFullName ?? ''}
|
||||
repoFullName={syncStatus.repoFullName ?? ""}
|
||||
openIssuesCount={getOpenIssuesCount()}
|
||||
isLoading={isLoading}
|
||||
searchQuery={searchQuery}
|
||||
@@ -199,7 +202,7 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
|
||||
<BatchReviewWizard
|
||||
isOpen={isWizardOpen}
|
||||
onClose={closeWizard}
|
||||
projectId={selectedProject?.id || ''}
|
||||
projectId={selectedProject?.id || ""}
|
||||
onStartAnalysis={startAnalysis}
|
||||
onApproveBatches={approveBatches}
|
||||
analysisProgress={analysisProgress}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useProjectStore } from '../stores/project-store';
|
||||
import { useTaskStore } from '../stores/task-store';
|
||||
import { useGitLabIssues, useGitLabInvestigation, useIssueFiltering } from './gitlab-issues/hooks';
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useProjectStore } from "../stores/project-store";
|
||||
import { useTaskStore } from "../stores/task-store";
|
||||
import { useGitLabIssues, useGitLabInvestigation, useIssueFiltering } from "./gitlab-issues/hooks";
|
||||
import {
|
||||
NotConnectedState,
|
||||
EmptyState,
|
||||
IssueListHeader,
|
||||
IssueList,
|
||||
IssueDetail,
|
||||
InvestigationDialog
|
||||
} from './gitlab-issues/components';
|
||||
import type { GitLabIssue } from '../../shared/types';
|
||||
import type { GitLabIssuesProps } from './gitlab-issues/types';
|
||||
InvestigationDialog,
|
||||
} from "./gitlab-issues/components";
|
||||
import type { GitLabIssue } from "../../shared/types";
|
||||
import type { GitLabIssuesProps } from "./gitlab-issues/types";
|
||||
|
||||
export function GitLabIssues({ onOpenSettings, onNavigateToTask }: GitLabIssuesProps) {
|
||||
const { t } = useTranslation('gitlab');
|
||||
const { t } = useTranslation("gitlab");
|
||||
const projects = useProjectStore((state) => state.projects);
|
||||
const selectedProjectId = useProjectStore((state) => state.selectedProjectId);
|
||||
const selectedProject = projects.find((p) => p.id === selectedProjectId);
|
||||
@@ -27,25 +27,27 @@ export function GitLabIssues({ onOpenSettings, onNavigateToTask }: GitLabIssuesP
|
||||
isLoading,
|
||||
error,
|
||||
selectedIssueIid,
|
||||
selectedIssue,
|
||||
filterState,
|
||||
selectIssue,
|
||||
getFilteredIssues,
|
||||
getOpenIssuesCount,
|
||||
handleRefresh,
|
||||
handleFilterChange
|
||||
handleFilterChange,
|
||||
} = useGitLabIssues(selectedProject?.id);
|
||||
|
||||
const {
|
||||
investigationStatus,
|
||||
lastInvestigationResult,
|
||||
startInvestigation,
|
||||
resetInvestigationStatus
|
||||
resetInvestigationStatus,
|
||||
} = useGitLabInvestigation(selectedProject?.id);
|
||||
|
||||
const { searchQuery, setSearchQuery, filteredIssues } = useIssueFiltering(getFilteredIssues());
|
||||
|
||||
const [showInvestigateDialog, setShowInvestigateDialog] = useState(false);
|
||||
const [selectedIssueForInvestigation, setSelectedIssueForInvestigation] = useState<GitLabIssue | null>(null);
|
||||
const [selectedIssueForInvestigation, setSelectedIssueForInvestigation] =
|
||||
useState<GitLabIssue | null>(null);
|
||||
|
||||
// Build a map of GitLab issue IIDs to task IDs for quick lookup
|
||||
const issueToTaskMap = useMemo(() => {
|
||||
@@ -63,34 +65,30 @@ export function GitLabIssues({ onOpenSettings, onNavigateToTask }: GitLabIssuesP
|
||||
setShowInvestigateDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleStartInvestigation = useCallback((selectedNoteIds: number[]) => {
|
||||
if (selectedIssueForInvestigation) {
|
||||
startInvestigation(selectedIssueForInvestigation, selectedNoteIds);
|
||||
}
|
||||
}, [selectedIssueForInvestigation, startInvestigation]);
|
||||
const handleStartInvestigation = useCallback(
|
||||
(selectedNoteIds: number[]) => {
|
||||
if (selectedIssueForInvestigation) {
|
||||
startInvestigation(selectedIssueForInvestigation, selectedNoteIds);
|
||||
}
|
||||
},
|
||||
[selectedIssueForInvestigation, startInvestigation]
|
||||
);
|
||||
|
||||
const handleCloseDialog = useCallback(() => {
|
||||
setShowInvestigateDialog(false);
|
||||
resetInvestigationStatus();
|
||||
}, [resetInvestigationStatus]);
|
||||
|
||||
const selectedIssue = issues.find(i => i.iid === selectedIssueIid);
|
||||
|
||||
// Not connected state
|
||||
if (!syncStatus?.connected) {
|
||||
return (
|
||||
<NotConnectedState
|
||||
error={syncStatus?.error || null}
|
||||
onOpenSettings={onOpenSettings}
|
||||
/>
|
||||
);
|
||||
return <NotConnectedState error={syncStatus?.error || null} onOpenSettings={onOpenSettings} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<IssueListHeader
|
||||
projectPath={syncStatus.projectPathWithNamespace ?? ''}
|
||||
projectPath={syncStatus.projectPathWithNamespace ?? ""}
|
||||
openIssuesCount={getOpenIssuesCount()}
|
||||
isLoading={isLoading}
|
||||
searchQuery={searchQuery}
|
||||
@@ -129,7 +127,7 @@ export function GitLabIssues({ onOpenSettings, onNavigateToTask }: GitLabIssuesP
|
||||
onViewTask={onNavigateToTask}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState message={t('empty.selectIssue')} />
|
||||
<EmptyState message={t("empty.selectIssue")} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import {
|
||||
useIssuesStore,
|
||||
useSyncStatusStore,
|
||||
loadGitHubIssues,
|
||||
checkGitHubConnection,
|
||||
type IssueFilterState
|
||||
} from '../../../stores/github';
|
||||
import type { FilterState } from '../types';
|
||||
type IssueFilterState,
|
||||
} from "../../../stores/github";
|
||||
import type { FilterState } from "../types";
|
||||
|
||||
export function useGitHubIssues(projectId: string | undefined) {
|
||||
const {
|
||||
@@ -18,7 +18,7 @@ export function useGitHubIssues(projectId: string | undefined) {
|
||||
selectIssue,
|
||||
setFilterState,
|
||||
getFilteredIssues,
|
||||
getOpenIssuesCount
|
||||
getOpenIssuesCount,
|
||||
} = useIssuesStore();
|
||||
|
||||
const { syncStatus } = useSyncStatusStore();
|
||||
@@ -50,12 +50,20 @@ export function useGitHubIssues(projectId: string | undefined) {
|
||||
}
|
||||
}, [projectId, filterState]);
|
||||
|
||||
const handleFilterChange = useCallback((state: FilterState) => {
|
||||
setFilterState(state);
|
||||
if (projectId) {
|
||||
loadGitHubIssues(projectId, state);
|
||||
}
|
||||
}, [projectId, setFilterState]);
|
||||
const handleFilterChange = useCallback(
|
||||
(state: FilterState) => {
|
||||
setFilterState(state);
|
||||
if (projectId) {
|
||||
loadGitHubIssues(projectId, state);
|
||||
}
|
||||
},
|
||||
[projectId, setFilterState]
|
||||
);
|
||||
|
||||
// Compute selectedIssue from issues array
|
||||
const selectedIssue = useMemo(() => {
|
||||
return issues.find((i) => i.number === selectedIssueNumber) || null;
|
||||
}, [issues, selectedIssueNumber]);
|
||||
|
||||
return {
|
||||
issues,
|
||||
@@ -63,11 +71,12 @@ export function useGitHubIssues(projectId: string | undefined) {
|
||||
isLoading,
|
||||
error,
|
||||
selectedIssueNumber,
|
||||
selectedIssue,
|
||||
filterState,
|
||||
selectIssue,
|
||||
getFilteredIssues,
|
||||
getOpenIssuesCount,
|
||||
handleRefresh,
|
||||
handleFilterChange
|
||||
handleFilterChange,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
import { GitPullRequest, RefreshCw, ExternalLink, Settings } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useProjectStore } from '../../stores/project-store';
|
||||
import { useGitHubPRs, usePRFiltering } from './hooks';
|
||||
import { PRList, PRDetail, PRFilterBar } from './components';
|
||||
import { Button } from '../ui/button';
|
||||
import { ResizablePanels } from '../ui/resizable-panels';
|
||||
import { useCallback } from "react";
|
||||
import { GitPullRequest, RefreshCw, ExternalLink, Settings } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useProjectStore } from "../../stores/project-store";
|
||||
import { useGitHubPRs, usePRFiltering } from "./hooks";
|
||||
import { PRList, PRDetail, PRFilterBar } from "./components";
|
||||
import { Button } from "../ui/button";
|
||||
import { ResizablePanels } from "../ui/resizable-panels";
|
||||
|
||||
interface GitHubPRsProps {
|
||||
onOpenSettings?: () => void;
|
||||
@@ -15,7 +15,7 @@ interface GitHubPRsProps {
|
||||
function NotConnectedState({
|
||||
error,
|
||||
onOpenSettings,
|
||||
t
|
||||
t,
|
||||
}: {
|
||||
error: string | null;
|
||||
onOpenSettings?: () => void;
|
||||
@@ -25,14 +25,12 @@ function NotConnectedState({
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<div className="text-center max-w-md">
|
||||
<GitPullRequest className="h-12 w-12 mx-auto mb-4 text-muted-foreground opacity-50" />
|
||||
<h3 className="text-lg font-medium mb-2">{t('prReview.notConnected')}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{error || t('prReview.connectPrompt')}
|
||||
</p>
|
||||
<h3 className="text-lg font-medium mb-2">{t("prReview.notConnected")}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">{error || t("prReview.connectPrompt")}</p>
|
||||
{onOpenSettings && (
|
||||
<Button onClick={onOpenSettings} variant="outline">
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
{t('prReview.openSettings')}
|
||||
{t("prReview.openSettings")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -52,7 +50,7 @@ function EmptyState({ message }: { message: string }) {
|
||||
}
|
||||
|
||||
export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps) {
|
||||
const { t } = useTranslation('common');
|
||||
const { t } = useTranslation("common");
|
||||
const projects = useProjectStore((state) => state.projects);
|
||||
const selectedProjectId = useProjectStore((state) => state.selectedProjectId);
|
||||
const selectedProject = projects.find((p) => p.id === selectedProjectId);
|
||||
@@ -82,14 +80,11 @@ export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps)
|
||||
isConnected,
|
||||
repoFullName,
|
||||
getReviewStateForPR,
|
||||
selectedPR,
|
||||
} = useGitHubPRs(selectedProject?.id, { isActive });
|
||||
|
||||
const selectedPR = prs.find(pr => pr.number === selectedPRNumber);
|
||||
|
||||
// Get previousResult and newCommitsCheck for follow-up review continuity
|
||||
const selectedPRReviewState = selectedPRNumber
|
||||
? getReviewStateForPR(selectedPRNumber)
|
||||
: null;
|
||||
const selectedPRReviewState = selectedPRNumber ? getReviewStateForPR(selectedPRNumber) : null;
|
||||
const previousReviewResult = selectedPRReviewState?.previousResult ?? null;
|
||||
const storedNewCommitsCheck = selectedPRReviewState?.newCommitsCheck ?? null;
|
||||
|
||||
@@ -130,30 +125,45 @@ export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps)
|
||||
}
|
||||
}, [selectedPRNumber, cancelReview]);
|
||||
|
||||
const handlePostReview = useCallback(async (selectedFindingIds?: string[], options?: { forceApprove?: boolean }): Promise<boolean> => {
|
||||
if (selectedPRNumber && reviewResult) {
|
||||
return await postReview(selectedPRNumber, selectedFindingIds, options);
|
||||
}
|
||||
return false;
|
||||
}, [selectedPRNumber, reviewResult, postReview]);
|
||||
const handlePostReview = useCallback(
|
||||
async (
|
||||
selectedFindingIds?: string[],
|
||||
options?: { forceApprove?: boolean }
|
||||
): Promise<boolean> => {
|
||||
if (selectedPRNumber && reviewResult) {
|
||||
return await postReview(selectedPRNumber, selectedFindingIds, options);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[selectedPRNumber, reviewResult, postReview]
|
||||
);
|
||||
|
||||
const handlePostComment = useCallback(async (body: string) => {
|
||||
if (selectedPRNumber) {
|
||||
await postComment(selectedPRNumber, body);
|
||||
}
|
||||
}, [selectedPRNumber, postComment]);
|
||||
const handlePostComment = useCallback(
|
||||
async (body: string) => {
|
||||
if (selectedPRNumber) {
|
||||
await postComment(selectedPRNumber, body);
|
||||
}
|
||||
},
|
||||
[selectedPRNumber, postComment]
|
||||
);
|
||||
|
||||
const handleMergePR = useCallback(async (mergeMethod?: 'merge' | 'squash' | 'rebase') => {
|
||||
if (selectedPRNumber) {
|
||||
await mergePR(selectedPRNumber, mergeMethod);
|
||||
}
|
||||
}, [selectedPRNumber, mergePR]);
|
||||
const handleMergePR = useCallback(
|
||||
async (mergeMethod?: "merge" | "squash" | "rebase") => {
|
||||
if (selectedPRNumber) {
|
||||
await mergePR(selectedPRNumber, mergeMethod);
|
||||
}
|
||||
},
|
||||
[selectedPRNumber, mergePR]
|
||||
);
|
||||
|
||||
const handleAssignPR = useCallback(async (username: string) => {
|
||||
if (selectedPRNumber) {
|
||||
await assignPR(selectedPRNumber, username);
|
||||
}
|
||||
}, [selectedPRNumber, assignPR]);
|
||||
const handleAssignPR = useCallback(
|
||||
async (username: string) => {
|
||||
if (selectedPRNumber) {
|
||||
await assignPR(selectedPRNumber, username);
|
||||
}
|
||||
},
|
||||
[selectedPRNumber, assignPR]
|
||||
);
|
||||
|
||||
const handleGetLogs = useCallback(async () => {
|
||||
if (selectedProjectId && selectedPRNumber) {
|
||||
@@ -174,7 +184,7 @@ export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps)
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-sm font-medium flex items-center gap-2">
|
||||
<GitPullRequest className="h-4 w-4" />
|
||||
{t('prReview.pullRequests')}
|
||||
{t("prReview.pullRequests")}
|
||||
</h2>
|
||||
{repoFullName && (
|
||||
<a
|
||||
@@ -188,16 +198,11 @@ export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps)
|
||||
</a>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{prs.length} {t('prReview.open')}
|
||||
{prs.length} {t("prReview.open")}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={refresh}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
<Button variant="ghost" size="icon" onClick={refresh} disabled={isLoading}>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -235,7 +240,7 @@ export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps)
|
||||
selectedPR ? (
|
||||
<PRDetail
|
||||
pr={selectedPR}
|
||||
projectId={selectedProjectId || ''}
|
||||
projectId={selectedProjectId || ""}
|
||||
reviewResult={reviewResult}
|
||||
previousReviewResult={previousReviewResult}
|
||||
reviewProgress={reviewProgress}
|
||||
@@ -254,7 +259,7 @@ export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps)
|
||||
onGetLogs={handleGetLogs}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState message={t('prReview.selectPRToView')} />
|
||||
<EmptyState message={t("prReview.selectPRToView")} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import type {
|
||||
PRData,
|
||||
PRReviewResult,
|
||||
PRReviewProgress,
|
||||
NewCommitsCheck
|
||||
} from '../../../../preload/api/modules/github-api';
|
||||
import { usePRReviewStore, startPRReview as storeStartPRReview, startFollowupReview as storeStartFollowupReview } from '../../../stores/github';
|
||||
NewCommitsCheck,
|
||||
} from "../../../../preload/api/modules/github-api";
|
||||
import {
|
||||
usePRReviewStore,
|
||||
startPRReview as storeStartPRReview,
|
||||
startFollowupReview as storeStartFollowupReview,
|
||||
} from "../../../stores/github";
|
||||
|
||||
// Re-export types for consumers
|
||||
export type { PRData, PRReviewResult, PRReviewProgress };
|
||||
export type { PRReviewFinding } from '../../../../preload/api/modules/github-api';
|
||||
export type { PRReviewFinding } from "../../../../preload/api/modules/github-api";
|
||||
|
||||
interface UseGitHubPRsOptions {
|
||||
/** Whether the component is currently active/visible */
|
||||
@@ -34,18 +38,32 @@ interface UseGitHubPRsResult {
|
||||
selectPR: (prNumber: number | null) => void;
|
||||
refresh: () => Promise<void>;
|
||||
loadMore: () => Promise<void>;
|
||||
runReview: (prNumber: number) => Promise<void>;
|
||||
runFollowupReview: (prNumber: number) => Promise<void>;
|
||||
runReview: (prNumber: number) => void;
|
||||
runFollowupReview: (prNumber: number) => void;
|
||||
checkNewCommits: (prNumber: number) => Promise<NewCommitsCheck>;
|
||||
cancelReview: (prNumber: number) => Promise<boolean>;
|
||||
postReview: (prNumber: number, selectedFindingIds?: string[], options?: { forceApprove?: boolean }) => Promise<boolean>;
|
||||
postReview: (
|
||||
prNumber: number,
|
||||
selectedFindingIds?: string[],
|
||||
options?: { forceApprove?: boolean }
|
||||
) => Promise<boolean>;
|
||||
postComment: (prNumber: number, body: string) => Promise<boolean>;
|
||||
mergePR: (prNumber: number, mergeMethod?: 'merge' | 'squash' | 'rebase') => Promise<boolean>;
|
||||
mergePR: (prNumber: number, mergeMethod?: "merge" | "squash" | "rebase") => Promise<boolean>;
|
||||
assignPR: (prNumber: number, username: string) => Promise<boolean>;
|
||||
getReviewStateForPR: (prNumber: number) => { isReviewing: boolean; progress: PRReviewProgress | null; result: PRReviewResult | null; previousResult: PRReviewResult | null; error: string | null; newCommitsCheck?: NewCommitsCheck | null } | null;
|
||||
getReviewStateForPR: (prNumber: number) => {
|
||||
isReviewing: boolean;
|
||||
progress: PRReviewProgress | null;
|
||||
result: PRReviewResult | null;
|
||||
previousResult: PRReviewResult | null;
|
||||
error: string | null;
|
||||
newCommitsCheck?: NewCommitsCheck | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function useGitHubPRs(projectId?: string, options: UseGitHubPRsOptions = {}): UseGitHubPRsResult {
|
||||
export function useGitHubPRs(
|
||||
projectId?: string,
|
||||
options: UseGitHubPRsOptions = {}
|
||||
): UseGitHubPRsResult {
|
||||
const { isActive = true } = options;
|
||||
const [prs, setPrs] = useState<PRData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -63,6 +81,8 @@ export function useGitHubPRs(projectId?: string, options: UseGitHubPRsOptions =
|
||||
const wasActiveRef = useRef(isActive);
|
||||
// Track if initial load has happened
|
||||
const hasLoadedRef = useRef(false);
|
||||
// Track the current PR being fetched (for race condition prevention)
|
||||
const currentFetchPRNumberRef = useRef<number | null>(null);
|
||||
|
||||
// Get PR review state from the global store
|
||||
const prReviews = usePRReviewStore((state) => state.prReviews);
|
||||
@@ -84,99 +104,115 @@ export function useGitHubPRs(projectId?: string, options: UseGitHubPRsOptions =
|
||||
// Get list of PR numbers currently being reviewed
|
||||
const activePRReviews = useMemo(() => {
|
||||
if (!projectId) return [];
|
||||
return getActivePRReviews(projectId).map(review => review.prNumber);
|
||||
return getActivePRReviews(projectId).map((review) => review.prNumber);
|
||||
}, [projectId, prReviews, getActivePRReviews]);
|
||||
|
||||
// Helper to get review state for any PR
|
||||
const getReviewStateForPR = useCallback((prNumber: number) => {
|
||||
if (!projectId) return null;
|
||||
const state = getPRReviewState(projectId, prNumber);
|
||||
if (!state) return null;
|
||||
return {
|
||||
isReviewing: state.isReviewing,
|
||||
progress: state.progress,
|
||||
result: state.result,
|
||||
previousResult: state.previousResult,
|
||||
error: state.error,
|
||||
newCommitsCheck: state.newCommitsCheck
|
||||
};
|
||||
}, [projectId, prReviews, getPRReviewState]);
|
||||
const getReviewStateForPR = useCallback(
|
||||
(prNumber: number) => {
|
||||
if (!projectId) return null;
|
||||
const state = getPRReviewState(projectId, prNumber);
|
||||
if (!state) return null;
|
||||
return {
|
||||
isReviewing: state.isReviewing,
|
||||
progress: state.progress,
|
||||
result: state.result,
|
||||
previousResult: state.previousResult,
|
||||
error: state.error,
|
||||
newCommitsCheck: state.newCommitsCheck,
|
||||
};
|
||||
},
|
||||
[projectId, prReviews, getPRReviewState]
|
||||
);
|
||||
|
||||
// Use detailed PR data if available (includes files), otherwise fall back to list data
|
||||
const selectedPR = selectedPRDetails || prs.find(pr => pr.number === selectedPRNumber) || null;
|
||||
// Validate that selectedPRDetails matches selectedPRNumber to avoid showing stale data
|
||||
const selectedPR = useMemo(() => {
|
||||
const matchingDetails =
|
||||
selectedPRDetails?.number === selectedPRNumber ? selectedPRDetails : null;
|
||||
return matchingDetails || prs.find((pr) => pr.number === selectedPRNumber) || null;
|
||||
}, [selectedPRDetails, prs, selectedPRNumber]);
|
||||
|
||||
// Check connection and fetch PRs
|
||||
const fetchPRs = useCallback(async (page: number = 1, append: boolean = false) => {
|
||||
if (!projectId) return;
|
||||
const fetchPRs = useCallback(
|
||||
async (page: number = 1, append: boolean = false) => {
|
||||
if (!projectId) return;
|
||||
|
||||
if (append) {
|
||||
setIsLoadingMore(true);
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
if (append) {
|
||||
setIsLoadingMore(true);
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// First check connection
|
||||
const connectionResult = await window.electronAPI.github.checkGitHubConnection(projectId);
|
||||
if (connectionResult.success && connectionResult.data) {
|
||||
setIsConnected(connectionResult.data.connected);
|
||||
setRepoFullName(connectionResult.data.repoFullName || null);
|
||||
try {
|
||||
// First check connection
|
||||
const connectionResult = await window.electronAPI.github.checkGitHubConnection(projectId);
|
||||
if (connectionResult.success && connectionResult.data) {
|
||||
setIsConnected(connectionResult.data.connected);
|
||||
setRepoFullName(connectionResult.data.repoFullName || null);
|
||||
|
||||
if (connectionResult.data.connected) {
|
||||
// Fetch PRs with pagination
|
||||
const result = await window.electronAPI.github.listPRs(projectId, page);
|
||||
if (result) {
|
||||
// Check if there are more PRs to load (GitHub returns up to 100 per page)
|
||||
setHasMore(result.length === 100);
|
||||
setCurrentPage(page);
|
||||
if (connectionResult.data.connected) {
|
||||
// Fetch PRs with pagination
|
||||
const result = await window.electronAPI.github.listPRs(projectId, page);
|
||||
if (result) {
|
||||
// Check if there are more PRs to load (GitHub returns up to 100 per page)
|
||||
setHasMore(result.length === 100);
|
||||
setCurrentPage(page);
|
||||
|
||||
if (append) {
|
||||
// Append to existing PRs, deduplicating by PR number
|
||||
setPrs(prevPrs => {
|
||||
const existingNumbers = new Set(prevPrs.map(pr => pr.number));
|
||||
const newPrs = result.filter(pr => !existingNumbers.has(pr.number));
|
||||
return [...prevPrs, ...newPrs];
|
||||
if (append) {
|
||||
// Append to existing PRs, deduplicating by PR number
|
||||
setPrs((prevPrs) => {
|
||||
const existingNumbers = new Set(prevPrs.map((pr) => pr.number));
|
||||
const newPrs = result.filter((pr) => !existingNumbers.has(pr.number));
|
||||
return [...prevPrs, ...newPrs];
|
||||
});
|
||||
} else {
|
||||
setPrs(result);
|
||||
}
|
||||
|
||||
// Batch preload review results for PRs not in store (single IPC call)
|
||||
const prsNeedingPreload = result.filter((pr) => {
|
||||
const existingState = getPRReviewState(projectId, pr.number);
|
||||
return !existingState?.result;
|
||||
});
|
||||
} else {
|
||||
setPrs(result);
|
||||
}
|
||||
|
||||
// Batch preload review results for PRs not in store (single IPC call)
|
||||
const prsNeedingPreload = result.filter(pr => {
|
||||
const existingState = getPRReviewState(projectId, pr.number);
|
||||
return !existingState?.result;
|
||||
});
|
||||
if (prsNeedingPreload.length > 0) {
|
||||
const prNumbers = prsNeedingPreload.map((pr) => pr.number);
|
||||
const batchReviews = await window.electronAPI.github.getPRReviewsBatch(
|
||||
projectId,
|
||||
prNumbers
|
||||
);
|
||||
|
||||
if (prsNeedingPreload.length > 0) {
|
||||
const prNumbers = prsNeedingPreload.map(pr => pr.number);
|
||||
const batchReviews = await window.electronAPI.github.getPRReviewsBatch(projectId, prNumbers);
|
||||
|
||||
// Update store with loaded results
|
||||
for (const reviewResult of Object.values(batchReviews)) {
|
||||
if (reviewResult) {
|
||||
usePRReviewStore.getState().setPRReviewResult(projectId, reviewResult, { preserveNewCommitsCheck: true });
|
||||
// Update store with loaded results
|
||||
for (const reviewResult of Object.values(batchReviews)) {
|
||||
if (reviewResult) {
|
||||
usePRReviewStore.getState().setPRReviewResult(projectId, reviewResult, {
|
||||
preserveNewCommitsCheck: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: New commits check is now lazy - only done when user selects a PR
|
||||
// or explicitly triggers a check. This significantly speeds up list loading.
|
||||
// Note: New commits check is now lazy - only done when user selects a PR
|
||||
// or explicitly triggers a check. This significantly speeds up list loading.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setIsConnected(false);
|
||||
setRepoFullName(null);
|
||||
setError(connectionResult.error || "Failed to check connection");
|
||||
}
|
||||
} else {
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to fetch PRs");
|
||||
setIsConnected(false);
|
||||
setRepoFullName(null);
|
||||
setError(connectionResult.error || 'Failed to check connection');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch PRs');
|
||||
setIsConnected(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
}, [projectId, getPRReviewState, setNewCommitsCheckAction]);
|
||||
},
|
||||
[projectId, getPRReviewState]
|
||||
);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
@@ -210,68 +246,88 @@ export function useGitHubPRs(projectId?: string, options: UseGitHubPRsOptions =
|
||||
|
||||
// No need for local IPC listeners - they're handled globally in github-store
|
||||
|
||||
const selectPR = useCallback((prNumber: number | null) => {
|
||||
setSelectedPRNumber(prNumber);
|
||||
// Note: Don't reset review result - it comes from the store now
|
||||
// and persists across navigation
|
||||
const selectPR = useCallback(
|
||||
(prNumber: number | null) => {
|
||||
setSelectedPRNumber(prNumber);
|
||||
// Note: Don't reset review result - it comes from the store now
|
||||
// and persists across navigation
|
||||
|
||||
// Clear previous detailed PR data when deselecting
|
||||
if (prNumber === null) {
|
||||
setSelectedPRDetails(null);
|
||||
return;
|
||||
}
|
||||
// Clear previous detailed PR data when deselecting
|
||||
if (prNumber === null) {
|
||||
setSelectedPRDetails(null);
|
||||
currentFetchPRNumberRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (prNumber && projectId) {
|
||||
// Fetch full PR details including files
|
||||
setIsLoadingPRDetails(true);
|
||||
window.electronAPI.github.getPR(projectId, prNumber)
|
||||
.then(prDetails => {
|
||||
if (prDetails) {
|
||||
setSelectedPRDetails(prDetails);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn(`Failed to fetch PR details for #${prNumber}:`, err);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingPRDetails(false);
|
||||
});
|
||||
if (prNumber && projectId) {
|
||||
// Track the current PR being fetched (for race condition prevention)
|
||||
currentFetchPRNumberRef.current = prNumber;
|
||||
|
||||
// Load existing review from disk if not already in store
|
||||
const existingState = getPRReviewState(projectId, prNumber);
|
||||
// Only fetch from disk if we don't have a result in the store
|
||||
if (!existingState?.result) {
|
||||
window.electronAPI.github.getPRReview(projectId, prNumber).then(result => {
|
||||
if (result) {
|
||||
// Update store with the loaded result
|
||||
// Preserve newCommitsCheck when loading existing review from disk
|
||||
usePRReviewStore.getState().setPRReviewResult(projectId, result, { preserveNewCommitsCheck: true });
|
||||
// Fetch full PR details including files
|
||||
setIsLoadingPRDetails(true);
|
||||
window.electronAPI.github
|
||||
.getPR(projectId, prNumber)
|
||||
.then((prDetails) => {
|
||||
// Only update if this response is still for the current PR (prevents race condition)
|
||||
if (prDetails && prNumber === currentFetchPRNumberRef.current) {
|
||||
setSelectedPRDetails(prDetails);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn(`Failed to fetch PR details for #${prNumber}:`, err);
|
||||
})
|
||||
.finally(() => {
|
||||
// Only clear loading state if this was the last fetch
|
||||
if (prNumber === currentFetchPRNumberRef.current) {
|
||||
setIsLoadingPRDetails(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Always check for new commits when selecting a reviewed PR
|
||||
// This ensures fresh data even if we have a cached check from earlier in the session
|
||||
const reviewedCommitSha = result.reviewedCommitSha || (result as any).reviewed_commit_sha;
|
||||
if (reviewedCommitSha) {
|
||||
window.electronAPI.github.checkNewCommits(projectId, prNumber).then(newCommitsResult => {
|
||||
// Load existing review from disk if not already in store
|
||||
const existingState = getPRReviewState(projectId, prNumber);
|
||||
// Only fetch from disk if we don't have a result in the store
|
||||
if (!existingState?.result) {
|
||||
window.electronAPI.github.getPRReview(projectId, prNumber).then((result) => {
|
||||
if (result) {
|
||||
// Update store with the loaded result
|
||||
// Preserve newCommitsCheck when loading existing review from disk
|
||||
usePRReviewStore
|
||||
.getState()
|
||||
.setPRReviewResult(projectId, result, { preserveNewCommitsCheck: true });
|
||||
|
||||
// Always check for new commits when selecting a reviewed PR
|
||||
// This ensures fresh data even if we have a cached check from earlier in the session
|
||||
const reviewedCommitSha = result.reviewedCommitSha;
|
||||
if (reviewedCommitSha) {
|
||||
window.electronAPI.github
|
||||
.checkNewCommits(projectId, prNumber)
|
||||
.then((newCommitsResult) => {
|
||||
setNewCommitsCheckAction(projectId, prNumber, newCommitsResult);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn(`Failed to check new commits for PR #${prNumber}:`, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (existingState?.result) {
|
||||
// Review already in store - always check for new commits to get fresh status
|
||||
const reviewedCommitSha = existingState.result.reviewedCommitSha;
|
||||
if (reviewedCommitSha) {
|
||||
window.electronAPI.github
|
||||
.checkNewCommits(projectId, prNumber)
|
||||
.then((newCommitsResult) => {
|
||||
setNewCommitsCheckAction(projectId, prNumber, newCommitsResult);
|
||||
}).catch(err => {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn(`Failed to check new commits for PR #${prNumber}:`, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (existingState?.result) {
|
||||
// Review already in store - always check for new commits to get fresh status
|
||||
const reviewedCommitSha = existingState.result.reviewedCommitSha || (existingState.result as any).reviewed_commit_sha;
|
||||
if (reviewedCommitSha) {
|
||||
window.electronAPI.github.checkNewCommits(projectId, prNumber).then(newCommitsResult => {
|
||||
setNewCommitsCheckAction(projectId, prNumber, newCommitsResult);
|
||||
}).catch(err => {
|
||||
console.warn(`Failed to check new commits for PR #${prNumber}:`, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [projectId, getPRReviewState, setNewCommitsCheckAction]);
|
||||
},
|
||||
[projectId, getPRReviewState, setNewCommitsCheckAction]
|
||||
);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setCurrentPage(1);
|
||||
@@ -284,115 +340,155 @@ export function useGitHubPRs(projectId?: string, options: UseGitHubPRsOptions =
|
||||
await fetchPRs(currentPage + 1, true);
|
||||
}, [fetchPRs, hasMore, isLoadingMore, isLoading, currentPage]);
|
||||
|
||||
const runReview = useCallback(async (prNumber: number) => {
|
||||
if (!projectId) return;
|
||||
const runReview = useCallback(
|
||||
(prNumber: number) => {
|
||||
if (!projectId) return;
|
||||
|
||||
// Use the store function which handles both state and IPC
|
||||
storeStartPRReview(projectId, prNumber);
|
||||
}, [projectId]);
|
||||
// Use the store function which handles both state and IPC
|
||||
storeStartPRReview(projectId, prNumber);
|
||||
},
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const runFollowupReview = useCallback(async (prNumber: number) => {
|
||||
if (!projectId) return;
|
||||
const runFollowupReview = useCallback(
|
||||
(prNumber: number) => {
|
||||
if (!projectId) return;
|
||||
|
||||
// Use the store function which handles both state and IPC
|
||||
storeStartFollowupReview(projectId, prNumber);
|
||||
}, [projectId]);
|
||||
// Use the store function which handles both state and IPC
|
||||
storeStartFollowupReview(projectId, prNumber);
|
||||
},
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const checkNewCommits = useCallback(async (prNumber: number): Promise<NewCommitsCheck> => {
|
||||
if (!projectId) {
|
||||
return { hasNewCommits: false, newCommitCount: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.github.checkNewCommits(projectId, prNumber);
|
||||
// Cache the result in the store so the list view can use it
|
||||
// Use the action from the hook subscription to ensure proper React re-renders
|
||||
setNewCommitsCheckAction(projectId, prNumber, result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to check for new commits');
|
||||
return { hasNewCommits: false, newCommitCount: 0 };
|
||||
}
|
||||
}, [projectId, setNewCommitsCheckAction]);
|
||||
|
||||
const cancelReview = useCallback(async (prNumber: number): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
|
||||
try {
|
||||
const success = await window.electronAPI.github.cancelPRReview(projectId, prNumber);
|
||||
if (success) {
|
||||
// Update store to mark review as cancelled
|
||||
usePRReviewStore.getState().setPRReviewError(projectId, prNumber, 'Review cancelled by user');
|
||||
const checkNewCommits = useCallback(
|
||||
async (prNumber: number): Promise<NewCommitsCheck> => {
|
||||
if (!projectId) {
|
||||
return { hasNewCommits: false, newCommitCount: 0 };
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to cancel review');
|
||||
return false;
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const postReview = useCallback(async (prNumber: number, selectedFindingIds?: string[], options?: { forceApprove?: boolean }): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
try {
|
||||
const result = await window.electronAPI.github.checkNewCommits(projectId, prNumber);
|
||||
// Cache the result in the store so the list view can use it
|
||||
// Use the action from the hook subscription to ensure proper React re-renders
|
||||
setNewCommitsCheckAction(projectId, prNumber, result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to check for new commits");
|
||||
return { hasNewCommits: false, newCommitCount: 0 };
|
||||
}
|
||||
},
|
||||
[projectId, setNewCommitsCheckAction]
|
||||
);
|
||||
|
||||
try {
|
||||
const success = await window.electronAPI.github.postPRReview(projectId, prNumber, selectedFindingIds, options);
|
||||
if (success) {
|
||||
// Reload review result to get updated postedAt and finding status
|
||||
const result = await window.electronAPI.github.getPRReview(projectId, prNumber);
|
||||
if (result) {
|
||||
// Preserve newCommitsCheck - posting doesn't change whether there are new commits
|
||||
usePRReviewStore.getState().setPRReviewResult(projectId, result, { preserveNewCommitsCheck: true });
|
||||
const cancelReview = useCallback(
|
||||
async (prNumber: number): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
|
||||
try {
|
||||
const success = await window.electronAPI.github.cancelPRReview(projectId, prNumber);
|
||||
if (success) {
|
||||
// Update store to mark review as cancelled
|
||||
usePRReviewStore
|
||||
.getState()
|
||||
.setPRReviewError(projectId, prNumber, "Review cancelled by user");
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to cancel review");
|
||||
return false;
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to post review');
|
||||
return false;
|
||||
}
|
||||
}, [projectId]);
|
||||
},
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const postComment = useCallback(async (prNumber: number, body: string): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
const postReview = useCallback(
|
||||
async (
|
||||
prNumber: number,
|
||||
selectedFindingIds?: string[],
|
||||
options?: { forceApprove?: boolean }
|
||||
): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
|
||||
try {
|
||||
return await window.electronAPI.github.postPRComment(projectId, prNumber, body);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to post comment');
|
||||
return false;
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const mergePR = useCallback(async (prNumber: number, mergeMethod: 'merge' | 'squash' | 'rebase' = 'squash'): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
|
||||
try {
|
||||
const success = await window.electronAPI.github.mergePR(projectId, prNumber, mergeMethod);
|
||||
if (success) {
|
||||
// Refresh PR list after merge
|
||||
await fetchPRs();
|
||||
try {
|
||||
const success = await window.electronAPI.github.postPRReview(
|
||||
projectId,
|
||||
prNumber,
|
||||
selectedFindingIds,
|
||||
options
|
||||
);
|
||||
if (success) {
|
||||
// Reload review result to get updated postedAt and finding status
|
||||
const result = await window.electronAPI.github.getPRReview(projectId, prNumber);
|
||||
if (result) {
|
||||
// Preserve newCommitsCheck - posting doesn't change whether there are new commits
|
||||
usePRReviewStore
|
||||
.getState()
|
||||
.setPRReviewResult(projectId, result, { preserveNewCommitsCheck: true });
|
||||
}
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to post review");
|
||||
return false;
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to merge PR');
|
||||
return false;
|
||||
}
|
||||
}, [projectId, fetchPRs]);
|
||||
},
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const assignPR = useCallback(async (prNumber: number, username: string): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
const postComment = useCallback(
|
||||
async (prNumber: number, body: string): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
|
||||
try {
|
||||
const success = await window.electronAPI.github.assignPR(projectId, prNumber, username);
|
||||
if (success) {
|
||||
// Refresh PR list to update assignees
|
||||
await fetchPRs();
|
||||
try {
|
||||
return await window.electronAPI.github.postPRComment(projectId, prNumber, body);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to post comment");
|
||||
return false;
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to assign user');
|
||||
return false;
|
||||
}
|
||||
}, [projectId, fetchPRs]);
|
||||
},
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const mergePR = useCallback(
|
||||
async (
|
||||
prNumber: number,
|
||||
mergeMethod: "merge" | "squash" | "rebase" = "squash"
|
||||
): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
|
||||
try {
|
||||
const success = await window.electronAPI.github.mergePR(projectId, prNumber, mergeMethod);
|
||||
if (success) {
|
||||
// Refresh PR list after merge
|
||||
await fetchPRs();
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to merge PR");
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[projectId, fetchPRs]
|
||||
);
|
||||
|
||||
const assignPR = useCallback(
|
||||
async (prNumber: number, username: string): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
|
||||
try {
|
||||
const success = await window.electronAPI.github.assignPR(projectId, prNumber, username);
|
||||
if (success) {
|
||||
// Refresh PR list to update assignees
|
||||
await fetchPRs();
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to assign user");
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[projectId, fetchPRs]
|
||||
);
|
||||
|
||||
return {
|
||||
prs,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useGitLabStore, loadGitLabIssues, checkGitLabConnection } from '../../../stores/gitlab-store';
|
||||
import type { FilterState } from '../types';
|
||||
import { useEffect, useCallback, useMemo } from "react";
|
||||
import {
|
||||
useGitLabStore,
|
||||
loadGitLabIssues,
|
||||
checkGitLabConnection,
|
||||
} from "../../../stores/gitlab-store";
|
||||
import type { FilterState } from "../types";
|
||||
|
||||
export function useGitLabIssues(projectId: string | undefined) {
|
||||
const {
|
||||
@@ -13,7 +17,7 @@ export function useGitLabIssues(projectId: string | undefined) {
|
||||
selectIssue,
|
||||
setFilterState,
|
||||
getFilteredIssues,
|
||||
getOpenIssuesCount
|
||||
getOpenIssuesCount,
|
||||
} = useGitLabStore();
|
||||
|
||||
// Always check connection when component mounts or projectId changes
|
||||
@@ -39,12 +43,20 @@ export function useGitLabIssues(projectId: string | undefined) {
|
||||
}
|
||||
}, [projectId, filterState]);
|
||||
|
||||
const handleFilterChange = useCallback((state: FilterState) => {
|
||||
setFilterState(state);
|
||||
if (projectId) {
|
||||
loadGitLabIssues(projectId, state);
|
||||
}
|
||||
}, [projectId, setFilterState]);
|
||||
const handleFilterChange = useCallback(
|
||||
(state: FilterState) => {
|
||||
setFilterState(state);
|
||||
if (projectId) {
|
||||
loadGitLabIssues(projectId, state);
|
||||
}
|
||||
},
|
||||
[projectId, setFilterState]
|
||||
);
|
||||
|
||||
// Compute selectedIssue from issues array
|
||||
const selectedIssue = useMemo(() => {
|
||||
return issues.find((i) => i.iid === selectedIssueIid) || null;
|
||||
}, [issues, selectedIssueIid]);
|
||||
|
||||
return {
|
||||
issues,
|
||||
@@ -52,11 +64,12 @@ export function useGitLabIssues(projectId: string | undefined) {
|
||||
isLoading,
|
||||
error,
|
||||
selectedIssueIid,
|
||||
selectedIssue,
|
||||
filterState,
|
||||
selectIssue,
|
||||
getFilteredIssues,
|
||||
getOpenIssuesCount,
|
||||
handleRefresh,
|
||||
handleFilterChange
|
||||
handleFilterChange,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user