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.js (apps/frontend)
|
||||||
# ===========================
|
# ===========================
|
||||||
node_modules
|
node_modules
|
||||||
|
apps/frontend/node_modules
|
||||||
.npm
|
.npm
|
||||||
.yarn/
|
.yarn/
|
||||||
.pnp.*
|
.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 { Loader2, AlertCircle } from 'lucide-react';
|
||||||
import { ScrollArea } from '../../ui/scroll-area';
|
import { ScrollArea } from '../../ui/scroll-area';
|
||||||
import { IssueListItem } from './IssueListItem';
|
import { IssueListItem } from './IssueListItem';
|
||||||
@@ -19,6 +19,7 @@ export function IssueList({
|
|||||||
}: IssueListProps) {
|
}: IssueListProps) {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
const loadMoreTriggerRef = useRef<HTMLDivElement>(null);
|
const loadMoreTriggerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// Intersection Observer for infinite scroll
|
// Intersection Observer for infinite scroll
|
||||||
const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => {
|
const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => {
|
||||||
@@ -30,10 +31,10 @@ export function IssueList({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const trigger = loadMoreTriggerRef.current;
|
const trigger = loadMoreTriggerRef.current;
|
||||||
if (!trigger || !onLoadMore) return;
|
if (!trigger || !onLoadMore || !viewportElement) return;
|
||||||
|
|
||||||
const observer = new IntersectionObserver(handleIntersection, {
|
const observer = new IntersectionObserver(handleIntersection, {
|
||||||
root: null,
|
root: viewportElement,
|
||||||
rootMargin: '100px',
|
rootMargin: '100px',
|
||||||
threshold: 0
|
threshold: 0
|
||||||
});
|
});
|
||||||
@@ -43,7 +44,7 @@ export function IssueList({
|
|||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
};
|
};
|
||||||
}, [handleIntersection, onLoadMore]);
|
}, [handleIntersection, onLoadMore, viewportElement]);
|
||||||
|
|
||||||
// Only show blocking error view when no issues are loaded
|
// Only show blocking error view when no issues are loaded
|
||||||
// Load-more errors are shown inline near the load-more trigger
|
// Load-more errors are shown inline near the load-more trigger
|
||||||
@@ -71,7 +72,7 @@ export function IssueList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1" onViewportRef={setViewportElement}>
|
||||||
<div className="p-2 space-y-1">
|
<div className="p-2 space-y-1">
|
||||||
{issues.map((issue) => (
|
{issues.map((issue) => (
|
||||||
<IssueListItem
|
<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 { GitPullRequest, User, Clock, FileDiff, Loader2 } from 'lucide-react';
|
||||||
import { ScrollArea } from '../../ui/scroll-area';
|
import { ScrollArea } from '../../ui/scroll-area';
|
||||||
import { Badge } from '../../ui/badge';
|
import { Badge } from '../../ui/badge';
|
||||||
@@ -207,8 +207,8 @@ export function PRList({
|
|||||||
onLoadMore
|
onLoadMore
|
||||||
}: PRListProps) {
|
}: PRListProps) {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
||||||
const loadMoreTriggerRef = useRef<HTMLDivElement>(null);
|
const loadMoreTriggerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// Intersection Observer for infinite scroll
|
// Intersection Observer for infinite scroll
|
||||||
const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => {
|
const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => {
|
||||||
@@ -220,11 +220,11 @@ export function PRList({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const trigger = loadMoreTriggerRef.current;
|
const trigger = loadMoreTriggerRef.current;
|
||||||
if (!trigger) return;
|
if (!trigger || !viewportElement) return;
|
||||||
|
|
||||||
const observer = new IntersectionObserver(handleIntersection, {
|
const observer = new IntersectionObserver(handleIntersection, {
|
||||||
root: null, // Use viewport as root
|
root: viewportElement,
|
||||||
rootMargin: '100px', // Start loading 100px before reaching the bottom
|
rootMargin: '100px',
|
||||||
threshold: 0
|
threshold: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ export function PRList({
|
|||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
};
|
};
|
||||||
}, [handleIntersection]);
|
}, [handleIntersection, onLoadMore, viewportElement]);
|
||||||
|
|
||||||
if (isLoading && prs.length === 0) {
|
if (isLoading && prs.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -268,7 +268,7 @@ export function PRList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="flex-1" ref={scrollAreaRef}>
|
<ScrollArea className="flex-1" onViewportRef={setViewportElement}>
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
{prs.map((pr) => {
|
{prs.map((pr) => {
|
||||||
const reviewState = getReviewStateForPR(pr.number);
|
const reviewState = getReviewStateForPR(pr.number);
|
||||||
|
|||||||
@@ -5,23 +5,34 @@ import { cn } from '../../lib/utils';
|
|||||||
const ScrollArea = React.forwardRef<
|
const ScrollArea = React.forwardRef<
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
|
||||||
viewportClassName?: string;
|
viewportClassName?: string;
|
||||||
}
|
onViewportRef?: (element: HTMLDivElement | null) => void;
|
||||||
>(({ className, children, viewportClassName, ...props }, ref) => (
|
}
|
||||||
<ScrollAreaPrimitive.Root
|
>(({ className, children, viewportClassName, onViewportRef, ...props }, ref) => {
|
||||||
ref={ref}
|
const viewportRef = React.useCallback(
|
||||||
className={cn('relative overflow-hidden', className)}
|
(element: HTMLDivElement | null) => {
|
||||||
{...props}
|
onViewportRef?.(element);
|
||||||
>
|
},
|
||||||
<ScrollAreaPrimitive.Viewport
|
[onViewportRef]
|
||||||
className={cn('h-full w-full rounded-[inherit]', viewportClassName)}
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn('relative overflow-hidden', className)}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
<ScrollAreaPrimitive.Viewport
|
||||||
</ScrollAreaPrimitive.Viewport>
|
ref={viewportRef}
|
||||||
<ScrollBar />
|
className={cn('h-full w-full rounded-[inherit]', viewportClassName)}
|
||||||
<ScrollAreaPrimitive.Corner />
|
>
|
||||||
</ScrollAreaPrimitive.Root>
|
{children}
|
||||||
));
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
);
|
||||||
|
});
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
const ScrollBar = React.forwardRef<
|
||||||
|
|||||||
Reference in New Issue
Block a user