mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
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:
co-authored by
Claude Opus 5
parent
b3695d666d
commit
1868228d29
@@ -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
|
||||
|
||||
+26
-26
File diff suppressed because one or more lines are too long
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user