fix(webapp): history shows it is loading instead of reading as empty (BEA-131) (#179)

The history filters are part of the react-query key, so changing one drops
`data` back to undefined. The feed rendered the filter bar and nothing else,
which is pixel-identical to the resolved "No changes match these filters."
state — a reader concluded twice that a file had no history when it had
three entries.

Render the shell unconditionally and put the in-repo `.empty` loading row
inside it, so a pending request and an empty result look different. Not
while `error`: failures already report through onMeta, and a permanent
spinner would hide them.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Snow Lee (Sungwon)
2026-08-19 11:29:19 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent b3695d666d
commit 1868228d29
4 changed files with 76 additions and 29 deletions
@@ -142,3 +142,35 @@ test("?path= lands on that file's feed, normalizes the URL, and keeps filters",
await expect(page.locator("#crumb")).toHaveText("History — guide.md");
await expect(page).toHaveURL(`/${pid}/history/guide.md?path=notes/readme.md`);
});
// BEA-131: the filters are in the query key, so changing one drops the feed
// back to no-data. It used to render the bar and nothing else — pixel-identical
// to "nothing matched", and a reader concluded twice that a file had no history
// when it had three entries. Both strings are asserted here, because the whole
// point is that the two states look different.
test("a filter change shows a loading row, never a premature 'no matches'", async ({ page }) => {
await login(page);
const pid = await wikiId(page);
await page.goto(`/${pid}/history`);
await expect(page.locator(rows).first()).toBeVisible();
const stall = (url: URL) => url.pathname.endsWith("/history");
await page.route(stall, async (route) => {
await new Promise((r) => setTimeout(r, 2000));
await route.continue();
});
await page.fill(".hfilters input[type=search]", "runbook");
await page.waitForURL(`/${pid}/history?q=runbook`);
const empty = page.locator(".history .empty");
await expect(empty).toHaveText("Loading…");
await expect(page.locator(".history .empty", { hasText: "No changes match these filters." })).toHaveCount(0);
// and the bar is still there, still holding what was typed
await expect(page.locator(".hfilters input[type=search]")).toHaveValue("runbook");
// …then the rows land and the loading row goes away.
await expect(page.locator(rows).first()).toBeVisible({ timeout: 10_000 });
await expect(page.locator(".history .empty")).toHaveCount(0);
await page.unroute(stall);
});
@@ -63,7 +63,7 @@ export function HistoryView(props: {
// into one array — groupRuns and prevBlob both work over the whole window,
// so a run straddling a page boundary becomes one card when its second page
// lands, and the oldest loaded row shows no diff base rather than a wrong one.
const { data, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
const { data, error, isPending, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ["history", apiBase, qs],
queryFn: ({ pageParam }) =>
getJSON<{ entries: HistoryEntry[]; next_cursor?: string }>(
@@ -100,7 +100,22 @@ export function HistoryView(props: {
onChange={props.onFilters}
/>
);
if (!data) return bar ? <div className="history">{bar}</div> : null;
// Nothing loaded yet. The filters are in the query key, so every keystroke
// in the path box drops `data` back to undefined — returning a bare filter
// bar here made a pending request pixel-identical to "nothing matched", and
// a reader twice concluded a file had no history when it had three entries
// (BEA-131). The shell always renders so the bar stays interactive, and the
// loading row is the same `.empty` one-liner the empty state uses, so the
// section doesn't jump when the response lands. Not while `error`: failures
// already report through onMeta above, and a permanent spinner would hide
// them.
if (!data)
return (
<div className="history">
{bar}
{isPending && !error && <div className="empty">Loading</div>}
</div>
);
// Entries arrive newest-first, so a row's predecessor is the next entry
// below it on the same path that still has content. This keeps scanning the
// flat list, never a group: it is a per-path lookup, and grouping must not
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
<script type="module" crossorigin src="/assets/index-BTpeDYWL.js"></script>
<script type="module" crossorigin src="/assets/index-DU4-OSS9.js"></script>
<link rel="modulepreload" crossorigin href="/assets/_commonjsHelpers-CqkleIqs.js">
<link rel="modulepreload" crossorigin href="/assets/mermaid-DQuCJ8Gi.js">
<link rel="stylesheet" crossorigin href="/assets/index-Bvo9-TXr.css">