Fix GitHub Issues/PRs Infinite Scroll Auto-Fetch (#1239)
* auto-claude: subtask-1-1 - Add onViewportRef callback prop to ScrollArea comp * auto-claude: subtask-2-1 - Add viewport ref state and update IntersectionObserver to use viewport as root * auto-claude: subtask-3-1 - Add viewport ref state and update IntersectionObserver - Added useState import for viewportElement state - Replaced scrollAreaRef with viewportElement state - Updated IntersectionObserver root from null to viewportElement - Added viewportElement to useEffect dependencies - Changed ScrollArea to use onViewportRef callback This fixes infinite scroll in PRList by using the ScrollArea viewport as the IntersectionObserver root, matching the pattern used in IssueList. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: remove node_modules symlink and improve IntersectionObserver consistency - Remove accidentally committed symlink at apps/frontend/node_modules - Add explicit .gitignore entry for symlink file (without trailing slash) - Add onLoadMore to PRList useEffect dependency array for consistency with IssueList - Add viewportElement guard to both IssueList and PRList to prevent unnecessary observer creation --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Test User <test@example.com>
This commit is contained in:
@@ -108,6 +108,7 @@ dmypy.json
|
||||
# Node.js (apps/frontend)
|
||||
# ===========================
|
||||
node_modules
|
||||
apps/frontend/node_modules
|
||||
.npm
|
||||
.yarn/
|
||||
.pnp.*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
import { useRef, useEffect, useCallback, useState } from 'react';
|
||||
import { Loader2, AlertCircle } from 'lucide-react';
|
||||
import { ScrollArea } from '../../ui/scroll-area';
|
||||
import { IssueListItem } from './IssueListItem';
|
||||
@@ -19,6 +19,7 @@ export function IssueList({
|
||||
}: IssueListProps) {
|
||||
const { t } = useTranslation('common');
|
||||
const loadMoreTriggerRef = useRef<HTMLDivElement>(null);
|
||||
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
// Intersection Observer for infinite scroll
|
||||
const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => {
|
||||
@@ -30,10 +31,10 @@ export function IssueList({
|
||||
|
||||
useEffect(() => {
|
||||
const trigger = loadMoreTriggerRef.current;
|
||||
if (!trigger || !onLoadMore) return;
|
||||
if (!trigger || !onLoadMore || !viewportElement) return;
|
||||
|
||||
const observer = new IntersectionObserver(handleIntersection, {
|
||||
root: null,
|
||||
root: viewportElement,
|
||||
rootMargin: '100px',
|
||||
threshold: 0
|
||||
});
|
||||
@@ -43,7 +44,7 @@ export function IssueList({
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [handleIntersection, onLoadMore]);
|
||||
}, [handleIntersection, onLoadMore, viewportElement]);
|
||||
|
||||
// Only show blocking error view when no issues are loaded
|
||||
// Load-more errors are shown inline near the load-more trigger
|
||||
@@ -71,7 +72,7 @@ export function IssueList({
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="flex-1">
|
||||
<ScrollArea className="flex-1" onViewportRef={setViewportElement}>
|
||||
<div className="p-2 space-y-1">
|
||||
{issues.map((issue) => (
|
||||
<IssueListItem
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
import { useRef, useEffect, useCallback, useState } from 'react';
|
||||
import { GitPullRequest, User, Clock, FileDiff, Loader2 } from 'lucide-react';
|
||||
import { ScrollArea } from '../../ui/scroll-area';
|
||||
import { Badge } from '../../ui/badge';
|
||||
@@ -207,8 +207,8 @@ export function PRList({
|
||||
onLoadMore
|
||||
}: PRListProps) {
|
||||
const { t } = useTranslation('common');
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const loadMoreTriggerRef = useRef<HTMLDivElement>(null);
|
||||
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
// Intersection Observer for infinite scroll
|
||||
const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => {
|
||||
@@ -220,11 +220,11 @@ export function PRList({
|
||||
|
||||
useEffect(() => {
|
||||
const trigger = loadMoreTriggerRef.current;
|
||||
if (!trigger) return;
|
||||
if (!trigger || !viewportElement) return;
|
||||
|
||||
const observer = new IntersectionObserver(handleIntersection, {
|
||||
root: null, // Use viewport as root
|
||||
rootMargin: '100px', // Start loading 100px before reaching the bottom
|
||||
root: viewportElement,
|
||||
rootMargin: '100px',
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
@@ -233,7 +233,7 @@ export function PRList({
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [handleIntersection]);
|
||||
}, [handleIntersection, onLoadMore, viewportElement]);
|
||||
|
||||
if (isLoading && prs.length === 0) {
|
||||
return (
|
||||
@@ -268,7 +268,7 @@ export function PRList({
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="flex-1" ref={scrollAreaRef}>
|
||||
<ScrollArea className="flex-1" onViewportRef={setViewportElement}>
|
||||
<div className="divide-y divide-border">
|
||||
{prs.map((pr) => {
|
||||
const reviewState = getReviewStateForPR(pr.number);
|
||||
|
||||
@@ -5,23 +5,34 @@ import { cn } from '../../lib/utils';
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
|
||||
viewportClassName?: string;
|
||||
}
|
||||
>(({ className, children, viewportClassName, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
className={cn('h-full w-full rounded-[inherit]', viewportClassName)}
|
||||
viewportClassName?: string;
|
||||
onViewportRef?: (element: HTMLDivElement | null) => void;
|
||||
}
|
||||
>(({ className, children, viewportClassName, onViewportRef, ...props }, ref) => {
|
||||
const viewportRef = React.useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
onViewportRef?.(element);
|
||||
},
|
||||
[onViewportRef]
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
ref={viewportRef}
|
||||
className={cn('h-full w-full rounded-[inherit]', viewportClassName)}
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
});
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
|
||||
Reference in New Issue
Block a user