diff --git a/apps/fabro-web/app/routes/runs.preferences.test.tsx b/apps/fabro-web/app/routes/runs.preferences.test.tsx index 7d69134b9..003e44ae1 100644 --- a/apps/fabro-web/app/routes/runs.preferences.test.tsx +++ b/apps/fabro-web/app/routes/runs.preferences.test.tsx @@ -72,9 +72,17 @@ const pageRuns: PaginatedRunList = { meta: { has_more: false, total: 1 }, }; +const queryCalls: Array<{ hook: string; args: unknown[] }> = []; + mock.module("../lib/queries", () => ({ - useAllRuns: () => ({ data: allRuns, isLoading: false }), - useRunsPage: () => ({ data: pageRuns, isLoading: false }), + useAllRuns: (...args: unknown[]) => { + queryCalls.push({ hook: "useAllRuns", args }); + return { data: allRuns, isLoading: false }; + }, + useRunsPage: (...args: unknown[]) => { + queryCalls.push({ hook: "useRunsPage", args }); + return { data: pageRuns, isLoading: false }; + }, useAuthConfig: () => ({ data: { methods: ["github"] } }), useSystemInfo: () => ({ data: { server_url: "http://127.0.0.1:32276" } }), })); @@ -166,6 +174,7 @@ beforeEach(() => { storage = new MemoryStorage(); teardownReactEnv = setupReactTestEnv(); installWindow(); + queryCalls.length = 0; }); afterEach(() => { @@ -191,6 +200,26 @@ describe("Runs workspace preference restoration", () => { expect(buttonByLabel(renderer, "List view").props["aria-pressed"]).toBe(true); }); + test("/runs applies stored list+archived prefs on the first render (no Quick Start flash)", async () => { + storage.setItem( + RUNS_PREFERENCES_STORAGE_KEY, + JSON.stringify({ version: 1, view: "list", archived: true }), + ); + + await renderRuns("/runs"); + + // The first frame the user sees must already reflect stored prefs. + // Before this was fixed, the route briefly rendered the columns view + // with includeArchived=false (default state) before a post-commit + // useEffect restored the URL, flashing the Quick Start empty state for + // users whose only runs were archived. + const firstAllRuns = queryCalls.find((c) => c.hook === "useAllRuns"); + const firstRunsPage = queryCalls.find((c) => c.hook === "useRunsPage"); + expect(firstAllRuns?.args).toEqual([{ includeArchived: true }, false]); + expect(firstRunsPage?.args[0]).toMatchObject({ includeArchived: true }); + expect(firstRunsPage?.args[1]).toBe(true); + }); + test("/runs?view=columns ignores stored list view", async () => { storage.setItem( RUNS_PREFERENCES_STORAGE_KEY, diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index 5287664d8..2388a8859 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -783,6 +783,20 @@ export function loadStoredRunsWorkspaceSearchParams( } } +// Resolve which search params should drive rendering. If the URL has no +// workspace params (e.g. the user clicked the Runs nav link, which goes to +// `/runs`), fall back to stored preferences so the first render already +// reflects the user's view/archived/etc. choice instead of route defaults. +// Without this, users whose only runs are archived briefly see the empty +// Quick Start landing before a post-commit effect restores `archived=1`. +export function resolveRunsWorkspaceSearchParams( + urlSearchParams: URLSearchParams, +): URLSearchParams { + if (hasRunsWorkspaceParams(urlSearchParams)) return urlSearchParams; + const stored = loadStoredRunsWorkspaceSearchParams(); + return stored.toString() === "" ? urlSearchParams : stored; +} + export function persistRunsWorkspaceSearchParams( searchParams: URLSearchParams, storage: Pick | null = runsPreferencesStorage(), @@ -1719,7 +1733,11 @@ function RunsLandingEmpty({ } export default function Runs() { - const [searchParams, setSearchParams] = useSearchParams(); + const [urlSearchParams, setSearchParams] = useSearchParams(); + const searchParams = useMemo( + () => resolveRunsWorkspaceSearchParams(urlSearchParams), + [urlSearchParams], + ); const query = searchParams.get("search") ?? ""; const repoFilter = searchParams.get("repo") ?? "all"; const workflowFilter = searchParams.get("workflow") ?? "all"; @@ -1737,21 +1755,16 @@ export default function Runs() { const updateParam = useCallback( (key: string, value: string | null) => { - setSearchParams( - (prev) => { - const next = new URLSearchParams(prev); - if (value == null || value === "") { - next.delete(key); - } else { - next.set(key, value); - } - persistRunsWorkspaceSearchParams(next); - return next; - }, - { replace: true }, - ); + const next = new URLSearchParams(searchParams); + if (value == null || value === "") { + next.delete(key); + } else { + next.set(key, value); + } + persistRunsWorkspaceSearchParams(next); + setSearchParams(next, { replace: true }); }, - [setSearchParams], + [searchParams, setSearchParams], ); const setQuery = (value: string) => updateParam("search", value || null); @@ -1789,13 +1802,9 @@ export default function Runs() { ); useEffect(() => { - if (hasRunsWorkspaceParams(searchParams)) return; - - const storedParams = loadStoredRunsWorkspaceSearchParams(); - if (storedParams.toString() === "") return; - - setSearchParams(storedParams, { replace: true }); - }, [searchParams, setSearchParams]); + if (searchParams === urlSearchParams) return; + setSearchParams(searchParams, { replace: true }); + }, [searchParams, urlSearchParams, setSearchParams]); const boardRuns = useAllRuns({ includeArchived }, view === "columns"); const listRunsPage = useRunsPage(