mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
Stabilize infinite issue paging
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
889cf660ac
commit
e71b27fbd4
2 files changed
+43
-5
No files matched your search
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildIssuesSearchUrl, getNextIssuesPageOffset } from "./Issues";
|
||||
import type { Issue } from "@paperclipai/shared";
|
||||
import { buildIssuesSearchUrl, getNextIssuesPageOffset, mergeIssuePagesStable } from "./Issues";
|
||||
|
||||
function createIssue(id: string, title: string): Issue {
|
||||
return { id, title } as Issue;
|
||||
}
|
||||
|
||||
describe("buildIssuesSearchUrl", () => {
|
||||
it("preserves trailing spaces in the synced search param", () => {
|
||||
@@ -26,4 +31,17 @@ describe("issues page pagination helpers", () => {
|
||||
expect(getNextIssuesPageOffset(499, 0)).toBeUndefined();
|
||||
expect(getNextIssuesPageOffset(999, 2000, 1000)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("dedupes overlapping pages without moving the original issue position", () => {
|
||||
const first = createIssue("issue-1", "Original first");
|
||||
const second = createIssue("issue-2", "Second");
|
||||
const duplicateFirst = createIssue("issue-1", "Duplicate first");
|
||||
const third = createIssue("issue-3", "Third");
|
||||
|
||||
expect(mergeIssuePagesStable([[first, second], [duplicateFirst, third]])).toEqual([
|
||||
first,
|
||||
second,
|
||||
third,
|
||||
]);
|
||||
});
|
||||
});
|
||||
+24
-4
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useCallback, useState } from "react";
|
||||
import { useEffect, useMemo, useCallback, useRef, useState } from "react";
|
||||
import { useLocation, useSearchParams } from "@/lib/router";
|
||||
import { useInfiniteQuery, useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { issuesApi } from "../api/issues";
|
||||
@@ -13,6 +13,7 @@ import { createIssueDetailLocationState } from "../lib/issueDetailBreadcrumb";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { IssuesList } from "../components/IssuesList";
|
||||
import { CircleDot } from "lucide-react";
|
||||
import type { Issue } from "@paperclipai/shared";
|
||||
|
||||
const WORKSPACE_FILTER_ISSUE_LIMIT = 1000;
|
||||
const ISSUES_PAGE_SIZE = 500;
|
||||
@@ -25,6 +26,21 @@ export function getNextIssuesPageOffset(
|
||||
return loadedPageSize >= pageSize ? currentOffset + pageSize : undefined;
|
||||
}
|
||||
|
||||
export function mergeIssuePagesStable(pages: Issue[][]): Issue[] {
|
||||
const seenIssueIds = new Set<string>();
|
||||
const merged: Issue[] = [];
|
||||
|
||||
for (const page of pages) {
|
||||
for (const issue of page) {
|
||||
if (seenIssueIds.has(issue.id)) continue;
|
||||
seenIssueIds.add(issue.id);
|
||||
merged.push(issue);
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function buildIssuesSearchUrl(currentHref: string, search: string): string | null {
|
||||
const url = new URL(currentHref);
|
||||
const currentSearch = url.searchParams.get("q") ?? "";
|
||||
@@ -45,6 +61,7 @@ export function Issues() {
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
const fetchNextPageInFlightRef = useRef(false);
|
||||
|
||||
const initialSearch = searchParams.get("q") ?? "";
|
||||
const [syncedSearch, setSyncedSearch] = useState(initialSearch);
|
||||
@@ -131,12 +148,15 @@ export function Issues() {
|
||||
placeholderData: (previousData) => previousData,
|
||||
});
|
||||
|
||||
const issues = useMemo(() => issuePages?.pages.flat() ?? [], [issuePages]);
|
||||
const issues = useMemo(() => mergeIssuePagesStable(issuePages?.pages ?? []), [issuePages]);
|
||||
const hasMoreServerIssues = syncedSearch.trim().length === 0
|
||||
&& hasNextPage === true;
|
||||
const loadMoreServerIssues = useCallback(() => {
|
||||
if (!hasNextPage || isFetchingNextPage) return;
|
||||
void fetchNextPage();
|
||||
if (!hasNextPage || isFetchingNextPage || fetchNextPageInFlightRef.current) return;
|
||||
fetchNextPageInFlightRef.current = true;
|
||||
void fetchNextPage({ cancelRefetch: false }).finally(() => {
|
||||
fetchNextPageInFlightRef.current = false;
|
||||
});
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
||||
|
||||
const updateIssue = useMutation({
|
||||
|
||||
Reference in new issue
Block a user