fix(desktop): paginate repository entries

Signed-off-by: Evan Chen <evanc@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
This commit is contained in:
Evan Chen
2026-08-06 09:25:35 -07:00
co-authored by Codex
parent 52c72ae2b3
commit e9de8ecad1
3 changed files with 56 additions and 13 deletions
@@ -1,7 +1,10 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { relativeTime } from "./projectsViewHelpers.ts";
import {
nextRepositoryEntryLimit,
relativeTime,
} from "./projectsViewHelpers.ts";
const DAY_SECONDS = 24 * 60 * 60;
@@ -43,3 +46,9 @@ test("relativeTime includes the year only across a year boundary", () => {
crossYearExpected,
);
});
test("repository entry pagination advances and clamps to the total", () => {
assert.equal(nextRepositoryEntryLimit(200, 450), 400);
assert.equal(nextRepositoryEntryLimit(400, 450), 450);
assert.equal(nextRepositoryEntryLimit(450, 450), 450);
});
@@ -26,6 +26,12 @@ export type ProjectsFilter =
| "users";
export type ProjectsSort = "updated" | "created" | "name";
export const REPOSITORY_ENTRY_PAGE_SIZE = 200;
export function nextRepositoryEntryLimit(current: number, total: number) {
return Math.min(current + REPOSITORY_ENTRY_PAGE_SIZE, total);
}
const PROJECTS_VIEW_MODE_STORAGE_KEY = "buzz.projects.viewMode";
const PROJECTS_FILTER_STORAGE_KEY = "buzz.projects.filter";
const PROJECTS_REPOSITORY_SCOPE_STORAGE_KEY = "buzz.projects.repositoryScope";
@@ -26,7 +26,11 @@ import type {
ProjectRepoFile,
ProjectRepoSnapshot,
} from "@/features/projects/hooks";
import { relativeTime } from "@/features/projects/lib/projectsViewHelpers";
import {
nextRepositoryEntryLimit,
relativeTime,
REPOSITORY_ENTRY_PAGE_SIZE,
} from "@/features/projects/lib/projectsViewHelpers";
import { useUserSearchQuery } from "@/features/profile/hooks";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import type { UserSearchResult } from "@/shared/api/types";
@@ -633,11 +637,18 @@ export function RepositoryFilesPanel({
const [currentPath, setCurrentPath] = React.useState("");
const [selectedFile, setSelectedFile] =
React.useState<ProjectRepoFile | null>(null);
const [visibleEntryCount, setVisibleEntryCount] = React.useState(
REPOSITORY_ENTRY_PAGE_SIZE,
);
const openPath = React.useCallback((path: string) => {
setCurrentPath(path);
setVisibleEntryCount(REPOSITORY_ENTRY_PAGE_SIZE);
}, []);
const entries = React.useMemo(
() => repositoryEntries(files, currentPath),
[currentPath, files],
);
const visibleEntries = entries.slice(0, 200);
const visibleEntries = entries.slice(0, visibleEntryCount);
const latestCommit = snapshot?.latestCommit ?? null;
const knownLatestCommitProfile = React.useMemo(
() => profileForCommitAuthor(latestCommit, profiles),
@@ -679,6 +690,7 @@ export function RepositoryFilesPanel({
if (!filesKey) return;
setCurrentPath("");
setSelectedFile(null);
setVisibleEntryCount(REPOSITORY_ENTRY_PAGE_SIZE);
}, [filesKey]);
// Loading/error/empty states keep the header controls visible — the
@@ -737,7 +749,7 @@ export function RepositoryFilesPanel({
file={selectedFile}
onOpenPath={(path) => {
setSelectedFile(null);
setCurrentPath(path);
openPath(path);
}}
/>
);
@@ -766,14 +778,14 @@ export function RepositoryFilesPanel({
/>
</>
) : (
<BreadcrumbButton onClick={() => setCurrentPath("")}>
<BreadcrumbButton onClick={() => openPath("")}>
Files
</BreadcrumbButton>
)}
{sourceControls && pathSegments.length > 0 ? (
<>
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground/60" />
<BreadcrumbButton onClick={() => setCurrentPath("")}>
<BreadcrumbButton onClick={() => openPath("")}>
Files
</BreadcrumbButton>
</>
@@ -783,7 +795,7 @@ export function RepositoryFilesPanel({
return (
<React.Fragment key={nextPath}>
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground/60" />
<BreadcrumbButton onClick={() => setCurrentPath(nextPath)}>
<BreadcrumbButton onClick={() => openPath(nextPath)}>
{segment}
</BreadcrumbButton>
</React.Fragment>
@@ -855,7 +867,7 @@ export function RepositoryFilesPanel({
const latestCommit = entry.latestCommit;
const rowIsLast = index === visibleEntries.length - 1;
const openEntry = () =>
openRepositoryEntry(entry, setCurrentPath, setSelectedFile);
openRepositoryEntry(entry, openPath, setSelectedFile);
return (
<tr
@@ -904,11 +916,27 @@ export function RepositoryFilesPanel({
</tbody>
</table>
</div>
{entries.length > 200 ? (
<p className="border-border/50 border-t px-4 py-3 text-2xs text-muted-foreground">
Showing the first 200 entries in this folder. Open a folder to narrow
the list.
</p>
{entries.length > visibleEntries.length ? (
<div className="flex items-center justify-between gap-3 border-border/50 border-t px-4 py-3 text-2xs text-muted-foreground">
<span>
Showing {visibleEntries.length} of {entries.length} entries.
</span>
<button
className="shrink-0 font-medium text-foreground hover:underline"
onClick={() =>
setVisibleEntryCount((current) =>
nextRepositoryEntryLimit(current, entries.length),
)
}
type="button"
>
Show next{" "}
{Math.min(
REPOSITORY_ENTRY_PAGE_SIZE,
entries.length - visibleEntries.length,
)}
</button>
</div>
) : null}
</div>
);