auto-claude: 182-implement-pagination-and-filtering-for-github-pr-l (#1654)
* auto-claude: subtask-1-1 - Add GITHUB_PR_LIST_MORE IPC channel constant Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-1-2 - Add listMorePRs IPC handler for pagination - Add GITHUB_PR_LIST_MORE handler that accepts cursor parameter - Update PRListResult interface to include endCursor field - Update existing GITHUB_PR_LIST handler to also return endCursor - Both handlers use GraphQL pagination with cursor-based navigation Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-1-3 - Update PRListResult type to include endCursor field Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-1-4 - Add listMorePRs method to GitHubAPI interface Add listMorePRs method for cursor-based pagination to the GitHubAPI interface and createGitHubAPI implementation in preload. Also update browser-mock.ts to include the new method for type consistency. Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-2-1 - Add sortBy option to PRFilterState interface and DEFAULT_FILTERS - Added PRSortOption type with 'newest' | 'oldest' | 'largest' options - Added sortBy field to PRFilterState interface - Set default sortBy to 'newest' in DEFAULT_FILTERS Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-2-2 - Add sorting logic to filteredPRs useMemo in usePRF * auto-claude: subtask-2-4 - Add loadMore function, endCursor state, and isLoadingMore state - Add isLoadingMore state to track pagination loading - Add endCursor state to track pagination cursor - Add loadMore function for cursor-based pagination - Update UseGitHubPRsResult interface with new properties - Store endCursor from fetchPRs API response - Reset endCursor when project changes - Batch preload review results for newly loaded PRs Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-3-1 - Add sort dropdown to PRFilterBar using FilterDropdown - Add SortDropdown component with single-select behavior for sorting PRs - Add SORT_OPTIONS constant with newest/oldest/largest options - Add onSortChange prop to PRFilterBarProps interface - Import ArrowUpDown, Clock, FileCode icons from lucide-react - Import PRSortOption type from usePRFiltering hook - Update GitHubPRs.tsx to pass setSortBy as onSortChange prop - Add i18n translations for sort labels (en/fr) Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-3-2 - Add onLoadMore and isLoadingMore props to PRList component Add pagination props to PRListProps interface: - onLoadMore: Optional callback to load more PRs when hasMore is true - isLoadingMore: Optional boolean to track loading state for pagination Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-3-3 - Replace status indicator text with Load More button - Add Load More button component to PRList when hasMore is true - Show loading spinner with "Loading..." text when isLoadingMore is true - Keep "All PRs loaded" text when all PRs are displayed - Add prReview.loadMore and prReview.loadingMore translation keys - Import Button and Loader2 components Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-3-4 - Wire up new props in GitHubPRs.tsx parent component - Add loadMore and isLoadingMore to useGitHubPRs destructuring - Pass loadMore as onLoadMore prop to PRList component - Pass isLoadingMore to PRList component for loading state - setSortBy already wired to PRFilterBar's onSortChange Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-4-1 - Add English translation keys for sortBy, sortNewest, sortOldest, sortLargest, loadMore, loadingMore Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-4-2 - Add French translation keys for sortBy, sortNewest Adds French translations for pagination and sorting UI elements: - sort.sortBy: "Trier par" - sort.sortNewest: "Plus récent" - sort.sortOldest: "Plus ancien" - sort.sortLargest: "Plus grand" - pagination.loadMore: "Charger plus" - pagination.loadingMore: "Chargement..." Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix ruff formatting and Windows dataclass import error - Apply ruff formatting to parallel_orchestrator_reviewer.py (3 long lines) - Apply ruff formatting to pydantic_models.py (Field on single line) - Fix Windows test collection error: register module in sys.modules before exec_module so dataclass decorator can find it Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix PR review findings: dedup mapping, race conditions, unused i18n keys - Extract shared mapGraphQLPRToData helper to eliminate duplicated PR mapping logic between listPRs and listMorePRs handlers - Add staleness checks to loadMore using generation counter and projectId ref to prevent race conditions with refresh and project switching - Reset isLoadingMore on project change to prevent stuck loading state - Remove unused common.sort.* and common.pagination.* translation keys from en/fr locale files (code uses prReview.* keys instead) - Align endCursor type to string | null in preload PRListResult - Preserve sortBy preference when clearing filters for consistency with hasActiveFilters Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix PR review findings: dedup PR handlers, stable sort order - Extract fetchPRsFromGraphQL helper to deduplicate listPRs/listMorePRs handlers - Add secondary sort key (createdAt) to 'largest' sort for stable ordering Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix PR review findings: deduplication and keyboard navigation - Add deduplication by PR number when appending paginated PRs to prevent duplicates if a PR shifts position between pagination requests - Add keyboard navigation to SortDropdown (ArrowUp/Down, Enter, Space, Escape) matching the pattern used in FilterDropdown for accessibility consistency Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix follow-up review findings: pagination, sort, keyboard UX - Preserve pagination state on failure response to allow retry - Pre-compute timestamps before sorting to avoid Date object creation - Add scrollIntoView for keyboard-focused items in FilterDropdown - Focus current selection when SortDropdown opens for better keyboard UX Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: Test User <[email protected]>
This commit is contained in:
committed by
StillKnotKnown
co-authored by
Claude Opus 4.5
Test User
parent
4e6d2b3fad
commit
fd263b424f
@@ -225,6 +225,23 @@ query($owner: String!, $repo: String!, $first: Int!, $after: String) {
|
||||
* GraphQL response type for PR list query
|
||||
* Note: repository can be null if the repo doesn't exist or user lacks access
|
||||
*/
|
||||
interface GraphQLPRNode {
|
||||
number: number;
|
||||
title: string;
|
||||
body: string | null;
|
||||
state: string;
|
||||
author: { login: string } | null;
|
||||
headRefName: string;
|
||||
baseRefName: string;
|
||||
additions: number;
|
||||
deletions: number;
|
||||
changedFiles: number;
|
||||
assignees: { nodes: Array<{ login: string }> };
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface GraphQLPRListResponse {
|
||||
data: {
|
||||
repository: {
|
||||
@@ -233,28 +250,37 @@ interface GraphQLPRListResponse {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
};
|
||||
nodes: Array<{
|
||||
number: number;
|
||||
title: string;
|
||||
body: string | null;
|
||||
state: string;
|
||||
author: { login: string } | null;
|
||||
headRefName: string;
|
||||
baseRefName: string;
|
||||
additions: number;
|
||||
deletions: number;
|
||||
changedFiles: number;
|
||||
assignees: { nodes: Array<{ login: string }> };
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
url: string;
|
||||
}>;
|
||||
nodes: GraphQLPRNode[];
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
errors?: Array<{ message: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a GraphQL PR node to the frontend PRData format.
|
||||
* Shared between listPRs and listMorePRs handlers.
|
||||
*/
|
||||
function mapGraphQLPRToData(pr: GraphQLPRNode): PRData {
|
||||
return {
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
body: pr.body ?? "",
|
||||
state: pr.state.toLowerCase(),
|
||||
author: { login: pr.author?.login ?? "unknown" },
|
||||
headRefName: pr.headRefName,
|
||||
baseRefName: pr.baseRefName,
|
||||
additions: pr.additions,
|
||||
deletions: pr.deletions,
|
||||
changedFiles: pr.changedFiles,
|
||||
assignees: pr.assignees.nodes.map((a) => ({ login: a.login })),
|
||||
files: [],
|
||||
createdAt: pr.createdAt,
|
||||
updatedAt: pr.updatedAt,
|
||||
htmlUrl: pr.url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a GraphQL request to GitHub API
|
||||
*/
|
||||
@@ -724,6 +750,7 @@ export interface PRData {
|
||||
export interface PRListResult {
|
||||
prs: PRData[];
|
||||
hasNextPage: boolean; // True if more PRs exist beyond the 100 limit
|
||||
endCursor?: string | null; // Cursor for fetching next page (null if no more pages)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1821,7 +1848,7 @@ async function fetchPRsFromGraphQL(
|
||||
export function registerPRHandlers(getMainWindow: () => BrowserWindow | null): void {
|
||||
debugLog("Registering PR handlers");
|
||||
|
||||
// List open PRs - fetches up to 100 open PRs at once, returns hasNextPage from API
|
||||
// List open PRs - fetches up to 100 open PRs at once, returns hasNextPage and endCursor from API
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.GITHUB_PR_LIST,
|
||||
async (_, projectId: string): Promise<PRListResult> => {
|
||||
@@ -1830,70 +1857,28 @@ export function registerPRHandlers(getMainWindow: () => BrowserWindow | null): v
|
||||
const config = getGitHubConfig(project);
|
||||
if (!config) {
|
||||
debugLog("No GitHub config found for project");
|
||||
return { prs: [], hasNextPage: false };
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse owner/repo from config - must be exactly "owner/repo" format
|
||||
const normalizedRepo = normalizeRepoReference(config.repo);
|
||||
const repoParts = normalizedRepo.split("/");
|
||||
if (repoParts.length !== 2 || !repoParts[0] || !repoParts[1]) {
|
||||
debugLog("Invalid repo format - expected 'owner/repo'", { repo: config.repo, normalized: normalizedRepo });
|
||||
return { prs: [], hasNextPage: false };
|
||||
}
|
||||
const [owner, repo] = repoParts;
|
||||
|
||||
// Use GraphQL API to get PRs with diff stats (REST list endpoint doesn't include them)
|
||||
// Fetches up to 100 open PRs (GitHub GraphQL max per request)
|
||||
const response = await githubGraphQL<GraphQLPRListResponse>(
|
||||
config.token,
|
||||
LIST_PRS_QUERY,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
first: 100, // GitHub GraphQL max is 100
|
||||
after: null, // Start from beginning
|
||||
}
|
||||
);
|
||||
|
||||
// Handle case where repository doesn't exist or user lacks access
|
||||
if (!response.data.repository) {
|
||||
debugLog("Repository not found or access denied", { owner, repo });
|
||||
return { prs: [], hasNextPage: false };
|
||||
}
|
||||
|
||||
const { nodes: prNodes, pageInfo } = response.data.repository.pullRequests;
|
||||
|
||||
debugLog("Fetched PRs via GraphQL", { count: prNodes.length, hasNextPage: pageInfo.hasNextPage });
|
||||
return {
|
||||
prs: prNodes.map((pr) => ({
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
body: pr.body ?? "",
|
||||
state: pr.state.toLowerCase(),
|
||||
author: { login: pr.author?.login ?? "unknown" },
|
||||
headRefName: pr.headRefName,
|
||||
baseRefName: pr.baseRefName,
|
||||
additions: pr.additions,
|
||||
deletions: pr.deletions,
|
||||
changedFiles: pr.changedFiles,
|
||||
assignees: pr.assignees.nodes.map((a) => ({ login: a.login })),
|
||||
files: [],
|
||||
createdAt: pr.createdAt,
|
||||
updatedAt: pr.updatedAt,
|
||||
htmlUrl: pr.url,
|
||||
})),
|
||||
hasNextPage: pageInfo.hasNextPage,
|
||||
};
|
||||
} catch (error) {
|
||||
debugLog("Failed to fetch PRs", {
|
||||
error: error instanceof Error ? error.message : error,
|
||||
});
|
||||
return { prs: [], hasNextPage: false };
|
||||
return { prs: [], hasNextPage: false, endCursor: null };
|
||||
}
|
||||
return fetchPRsFromGraphQL(config, null, "initial");
|
||||
});
|
||||
return result ?? { prs: [], hasNextPage: false };
|
||||
return result ?? { prs: [], hasNextPage: false, endCursor: null };
|
||||
}
|
||||
);
|
||||
|
||||
// Load more PRs (pagination) - fetches next page of PRs using cursor
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.GITHUB_PR_LIST_MORE,
|
||||
async (_, projectId: string, cursor: string): Promise<PRListResult> => {
|
||||
debugLog("listMorePRs handler called", { projectId, cursor });
|
||||
const result = await withProjectOrNull(projectId, async (project) => {
|
||||
const config = getGitHubConfig(project);
|
||||
if (!config) {
|
||||
debugLog("No GitHub config found for project");
|
||||
return { prs: [], hasNextPage: false, endCursor: null };
|
||||
}
|
||||
return fetchPRsFromGraphQL(config, cursor, "pagination");
|
||||
});
|
||||
return result ?? { prs: [], hasNextPage: false, endCursor: null };
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -271,6 +271,8 @@ export interface GitHubAPI {
|
||||
|
||||
// PR operations (fetches up to 100 open PRs at once - GitHub GraphQL limit)
|
||||
listPRs: (projectId: string) => Promise<PRListResult>;
|
||||
/** Load more PRs using cursor-based pagination */
|
||||
listMorePRs: (projectId: string, cursor: string) => Promise<PRListResult>;
|
||||
getPR: (projectId: string, prNumber: number) => Promise<PRData | null>;
|
||||
runPRReview: (projectId: string, prNumber: number) => void;
|
||||
cancelPRReview: (projectId: string, prNumber: number) => Promise<boolean>;
|
||||
@@ -357,6 +359,7 @@ export interface PRData {
|
||||
export interface PRListResult {
|
||||
prs: PRData[];
|
||||
hasNextPage: boolean; // True if more PRs exist beyond the 100 limit
|
||||
endCursor?: string | null; // Cursor for fetching next page (null if no more pages)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -694,6 +697,10 @@ export const createGitHubAPI = (): GitHubAPI => ({
|
||||
listPRs: (projectId: string): Promise<PRListResult> =>
|
||||
invokeIpc(IPC_CHANNELS.GITHUB_PR_LIST, projectId),
|
||||
|
||||
// Load more PRs using cursor-based pagination
|
||||
listMorePRs: (projectId: string, cursor: string): Promise<PRListResult> =>
|
||||
invokeIpc(IPC_CHANNELS.GITHUB_PR_LIST_MORE, projectId, cursor),
|
||||
|
||||
getPR: (projectId: string, prNumber: number): Promise<PRData | null> =>
|
||||
invokeIpc(IPC_CHANNELS.GITHUB_PR_GET, projectId, prNumber),
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps)
|
||||
const {
|
||||
prs,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
isLoadingPRDetails,
|
||||
error,
|
||||
selectedPRNumber,
|
||||
@@ -78,6 +79,7 @@ export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps)
|
||||
assignPR,
|
||||
markReviewPosted,
|
||||
refresh,
|
||||
loadMore,
|
||||
isConnected,
|
||||
repoFullName,
|
||||
getReviewStateForPR,
|
||||
@@ -252,6 +254,8 @@ export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps)
|
||||
error={error}
|
||||
getReviewStateForPR={getReviewStateForPR}
|
||||
onSelectPR={selectPR}
|
||||
onLoadMore={loadMore}
|
||||
isLoadingMore={isLoadingMore}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GitPullRequest, User, Clock, FileDiff } from 'lucide-react';
|
||||
import { GitPullRequest, User, Clock, FileDiff, Loader2 } from 'lucide-react';
|
||||
import { ScrollArea } from '../../ui/scroll-area';
|
||||
import { Badge } from '../../ui/badge';
|
||||
import { Button } from '../../ui/button';
|
||||
@@ -179,6 +179,10 @@ interface PRListProps {
|
||||
error: string | null;
|
||||
getReviewStateForPR: (prNumber: number) => PRReviewInfo | null;
|
||||
onSelectPR: (prNumber: number) => void;
|
||||
/** Callback to load more PRs when hasMore is true */
|
||||
onLoadMore?: () => void;
|
||||
/** Whether additional PRs are currently being loaded */
|
||||
isLoadingMore?: boolean;
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
@@ -209,6 +213,8 @@ export function PRList({
|
||||
error,
|
||||
getReviewStateForPR,
|
||||
onSelectPR,
|
||||
onLoadMore,
|
||||
isLoadingMore,
|
||||
}: PRListProps) {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
@@ -322,12 +328,30 @@ export function PRList({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Status indicator */}
|
||||
{/* Status indicator / Load More button */}
|
||||
{prs.length > 0 && (
|
||||
<div className="py-4 flex justify-center">
|
||||
<span className="text-xs text-muted-foreground opacity-50">
|
||||
{hasMore ? t('prReview.maxPRsShown') : t('prReview.allPRsLoaded')}
|
||||
</span>
|
||||
{hasMore && onLoadMore ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onLoadMore}
|
||||
disabled={isLoadingMore}
|
||||
>
|
||||
{isLoadingMore ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('prReview.loadingMore')}
|
||||
</>
|
||||
) : (
|
||||
t('prReview.loadMore')
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground opacity-50">
|
||||
{t('prReview.allPRsLoaded')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ interface UseGitHubPRsOptions {
|
||||
interface UseGitHubPRsResult {
|
||||
prs: PRData[];
|
||||
isLoading: boolean;
|
||||
isLoadingMore: boolean; // Loading additional PRs via pagination
|
||||
isLoadingPRDetails: boolean; // Loading full PR details including files
|
||||
error: string | null;
|
||||
selectedPR: PRData | null;
|
||||
@@ -38,6 +39,7 @@ interface UseGitHubPRsResult {
|
||||
hasMore: boolean; // True when 100 PRs returned (GitHub limit) - more may exist
|
||||
selectPR: (prNumber: number | null) => void;
|
||||
refresh: () => Promise<void>;
|
||||
loadMore: () => Promise<void>; // Load next page of PRs
|
||||
runReview: (prNumber: number) => void;
|
||||
runFollowupReview: (prNumber: number) => void;
|
||||
checkNewCommits: (prNumber: number) => Promise<NewCommitsCheck>;
|
||||
@@ -76,6 +78,8 @@ export function useGitHubPRs(
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [repoFullName, setRepoFullName] = useState<string | null>(null);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const [endCursor, setEndCursor] = useState<string | null>(null);
|
||||
|
||||
// Track previous isActive state to detect tab navigation
|
||||
const wasActiveRef = useRef(isActive);
|
||||
@@ -158,6 +162,9 @@ export function useGitHubPRs(
|
||||
async () => {
|
||||
if (!projectId) return;
|
||||
|
||||
// Increment generation to invalidate any in-flight loadMore requests
|
||||
fetchGenerationRef.current += 1;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
@@ -174,6 +181,8 @@ export function useGitHubPRs(
|
||||
if (result) {
|
||||
// Use hasNextPage from API to determine if more PRs exist
|
||||
setHasMore(result.hasNextPage);
|
||||
// Store endCursor for pagination
|
||||
setEndCursor(result.endCursor ?? null);
|
||||
setPrs(result.prs);
|
||||
|
||||
// Batch preload review results for PRs not in store (single IPC call)
|
||||
@@ -242,6 +251,7 @@ export function useGitHubPRs(
|
||||
fetchGenerationRef.current += 1;
|
||||
hasLoadedRef.current = false;
|
||||
setHasMore(false);
|
||||
setEndCursor(null);
|
||||
setPrs([]);
|
||||
setSelectedPRNumber(null);
|
||||
setSelectedPRDetails(null);
|
||||
@@ -433,6 +443,91 @@ export function useGitHubPRs(
|
||||
await fetchPRs();
|
||||
}, [fetchPRs]);
|
||||
|
||||
// Load more PRs using cursor-based pagination
|
||||
const loadMore = useCallback(async () => {
|
||||
if (!projectId || !endCursor || !hasMore || isLoadingMore) return;
|
||||
|
||||
// Capture current state for staleness checks
|
||||
const requestProjectId = projectId;
|
||||
const requestGeneration = fetchGenerationRef.current;
|
||||
|
||||
setIsLoadingMore(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.github.listMorePRs(projectId, endCursor);
|
||||
|
||||
// Discard response if project changed or a refresh happened while loading
|
||||
if (
|
||||
requestProjectId !== currentProjectIdRef.current ||
|
||||
requestGeneration !== fetchGenerationRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
// Check if this is a failure response (empty result with no next page)
|
||||
// In this case, preserve existing pagination state to allow retry
|
||||
const isFailureResponse = result.prs.length === 0 && !result.hasNextPage && !result.endCursor;
|
||||
|
||||
if (!isFailureResponse) {
|
||||
// Update pagination state only on successful response
|
||||
setHasMore(result.hasNextPage);
|
||||
setEndCursor(result.endCursor ?? null);
|
||||
|
||||
// Append new PRs to existing list, deduplicating by PR number
|
||||
// (handles edge case where PR shifts position between pagination requests)
|
||||
setPrs((prevPrs) => {
|
||||
const existingNumbers = new Set(prevPrs.map((pr) => pr.number));
|
||||
const newPrs = result.prs.filter((pr) => !existingNumbers.has(pr.number));
|
||||
return [...prevPrs, ...newPrs];
|
||||
});
|
||||
}
|
||||
|
||||
// Batch preload review results for new PRs not in store
|
||||
const prsNeedingPreload = result.prs.filter((pr) => {
|
||||
const existingState = getPRReviewState(requestProjectId, pr.number);
|
||||
return !existingState?.result && !existingState?.isReviewing;
|
||||
});
|
||||
|
||||
if (prsNeedingPreload.length > 0) {
|
||||
const prNumbers = prsNeedingPreload.map((pr) => pr.number);
|
||||
const batchReviews = await window.electronAPI.github.getPRReviewsBatch(
|
||||
requestProjectId,
|
||||
prNumbers
|
||||
);
|
||||
|
||||
// Check staleness again after async batch fetch
|
||||
if (
|
||||
requestProjectId !== currentProjectIdRef.current ||
|
||||
requestGeneration !== fetchGenerationRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update store with loaded results
|
||||
for (const reviewResult of Object.values(batchReviews)) {
|
||||
if (reviewResult) {
|
||||
usePRReviewStore.getState().setPRReviewResult(requestProjectId, reviewResult, {
|
||||
preserveNewCommitsCheck: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Only show error if still relevant
|
||||
if (
|
||||
requestProjectId === currentProjectIdRef.current &&
|
||||
requestGeneration === fetchGenerationRef.current
|
||||
) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load more PRs");
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
}, [projectId, endCursor, hasMore, isLoadingMore, getPRReviewState]);
|
||||
|
||||
const runReview = useCallback(
|
||||
(prNumber: number) => {
|
||||
if (!projectId) return;
|
||||
@@ -623,6 +718,7 @@ export function useGitHubPRs(
|
||||
return {
|
||||
prs,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
isLoadingPRDetails,
|
||||
error,
|
||||
selectedPR,
|
||||
@@ -638,6 +734,7 @@ export function useGitHubPRs(
|
||||
hasMore,
|
||||
selectPR,
|
||||
refresh,
|
||||
loadMore,
|
||||
runReview,
|
||||
runFollowupReview,
|
||||
checkNewCommits,
|
||||
|
||||
@@ -203,6 +203,7 @@ const browserMockAPI: ElectronAPI = {
|
||||
onAutoFixComplete: () => () => {},
|
||||
onAutoFixError: () => () => {},
|
||||
listPRs: async () => ({ prs: [], hasNextPage: false }),
|
||||
listMorePRs: async () => ({ prs: [], hasNextPage: false }),
|
||||
getPR: async () => null,
|
||||
runPRReview: () => {},
|
||||
cancelPRReview: async () => true,
|
||||
|
||||
@@ -367,6 +367,8 @@
|
||||
"branchUpdateFailed": "Failed to update branch",
|
||||
"allPRsLoaded": "All PRs loaded",
|
||||
"maxPRsShown": "Showing first 100 PRs",
|
||||
"loadMore": "Load More",
|
||||
"loadingMore": "Loading...",
|
||||
"workflowsAwaitingApproval": "{{count}} Workflow Awaiting Approval",
|
||||
"workflowsAwaitingApproval_plural": "{{count}} Workflows Awaiting Approval",
|
||||
"blockedByWorkflows": "Blocked",
|
||||
|
||||
@@ -376,6 +376,8 @@
|
||||
"branchUpdateFailed": "Échec de la mise à jour de la branche",
|
||||
"allPRsLoaded": "Tous les PRs chargés",
|
||||
"maxPRsShown": "Affichage des 100 premières PRs",
|
||||
"loadMore": "Charger plus",
|
||||
"loadingMore": "Chargement...",
|
||||
"workflowsAwaitingApproval": "{{count}} workflow en attente d'approbation",
|
||||
"workflowsAwaitingApproval_plural": "{{count}} workflows en attente d'approbation",
|
||||
"blockedByWorkflows": "Bloqué",
|
||||
|
||||
Reference in New Issue
Block a user