From 57547ed7b6384be9e2096ff4b4f4c213adb48e8d Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 6 Aug 2026 11:54:38 -0400 Subject: [PATCH] Populate repo and workflow filters in runs list view The Repo and Workflow dropdowns on the runs page derived their options from the board query, which is disabled in list view. With ?view=list, the options were always empty even when runs were visible. Derive the options from whichever data source the current view loads: the board query in columns view, or the current page of the paginated list query in list view. Extract the option-building into an exported buildFilterOptions helper that also keeps the active selection in the options when no loaded run matches it, so the filter button never renders an undefined label while paginating. A future change will replace page-derived options with a facets endpoint plus server-side repo/workflow query params. Co-Authored-By: Claude Fable 5 --- apps/fabro-web/app/routes/runs.test.tsx | 34 +++++++++++++++++++++++++ apps/fabro-web/app/routes/runs.tsx | 34 +++++++++++++++++-------- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/apps/fabro-web/app/routes/runs.test.tsx b/apps/fabro-web/app/routes/runs.test.tsx index 52196878b..71f30dd43 100644 --- a/apps/fabro-web/app/routes/runs.test.tsx +++ b/apps/fabro-web/app/routes/runs.test.tsx @@ -3,6 +3,7 @@ import type { BoardColumn, Run } from "@qltysh/fabro-api-client"; import { buildBoardColumns, + buildFilterOptions, loadStoredRunsWorkspaceSearchParams, placeArchivedColumnLast, persistRunsWorkspacePreferences, @@ -11,6 +12,7 @@ import { shouldRefreshBoardForEvent, } from "./runs"; import { summarizeBatchLifecycleAction } from "../components/runs-list/batch-lifecycle"; +import { mapRunListItem } from "../data/runs"; import { TEST_PRINCIPAL } from "../lib/test-fixtures"; function boardRun(id: string, column: BoardColumn, questionText?: string): Run { @@ -217,6 +219,38 @@ describe("runs route board mapping", () => { }); }); +describe("runs route filter options", () => { + function runWith(id: string, repoName: string, workflowName: string): Run { + const run = boardRun(id, "running"); + return { + ...run, + repository: { ...run.repository, name: repoName }, + workflow: { ...run.workflow, name: workflowName }, + }; + } + + test("derives sorted unique options from run items", () => { + const items = [ + runWith("a", "qlty/beta", "release"), + runWith("b", "qlty/alpha", "hello"), + runWith("c", "qlty/beta", "release"), + ].map(mapRunListItem); + + expect(buildFilterOptions(items, (item) => item.repo, "all")).toEqual(["alpha", "beta"]); + expect(buildFilterOptions(items, (item) => item.workflow, "all")).toEqual([ + "hello", + "release", + ]); + }); + + test("keeps the active selection when no loaded run matches it", () => { + const items = [runWith("a", "qlty/beta", "release")].map(mapRunListItem); + + expect(buildFilterOptions(items, (item) => item.repo, "gamma")).toEqual(["beta", "gamma"]); + expect(buildFilterOptions([], (item) => item.workflow, "release")).toEqual(["release"]); + }); +}); + describe("runs route workspace preferences", () => { class MemoryStorage { values = new Map(); diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index b1e09bf67..9aeceb78c 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -140,6 +140,18 @@ export function buildBoardColumns( }); } +export function buildFilterOptions( + items: RunItem[], + pick: (item: RunItem) => string, + selected: string, +): string[] { + const values = new Set(items.map(pick)); + // Keep the active selection visible even when no loaded run matches it, + // e.g. a stored repo filter while paginating the list view. + if (selected !== "all") values.add(selected); + return Array.from(values).sort(); +} + export function placeArchivedColumnLast(columns: Column[], includeArchived: boolean): Column[] { if (!includeArchived) return columns; const archived = columns.find((column) => column.id === "archived"); @@ -771,18 +783,18 @@ export default function Runs() { ); const hasGitHubAuth = authConfig.data?.methods.includes("github") === true; const serverUrl = systemInfo.data?.server_url; - const allRepos = Array.from( - new Set( - initialColumns.flatMap((col: Column) => col.items.map((item: RunItem) => String(item.repo))), - ), + // Filter options come from the loaded runs: all runs in columns view, the + // current page in list view (until a facets endpoint provides the full set). + const filterSourceItems: RunItem[] = + view === "list" + ? (listRunsPage.data?.data ?? []).map(mapRunListItem) + : initialColumns.flatMap((col: Column) => col.items); + const allRepos = buildFilterOptions(filterSourceItems, (item) => item.repo, repoFilter); + const allWorkflows = buildFilterOptions( + filterSourceItems, + (item) => item.workflow, + workflowFilter, ); - allRepos.sort(); - const allWorkflows = Array.from( - new Set( - initialColumns.flatMap((col: Column) => col.items.map((item: RunItem) => String(item.workflow))), - ), - ); - allWorkflows.sort(); const [columnsState, setColumnsState] = useState(() => ({ base: initialColumns, columns: initialColumns,