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 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-08-06 11:54:38 -04:00
parent 0abf2297c0
commit 57547ed7b6
No known key found for this signature in database
2 changed files with 57 additions and 11 deletions

View file

@ -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<string, string>();

View file

@ -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,