mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
feat(web): add multi-select run status filter to /runs toolbar
Status filter operates on the eight non-archived BoardColumn lanes and filters both the board (hides whole lanes) and the list (hides rows). Show archived remains a standalone toggle alongside it; an `archived` token in a previously-saved status string is migrated into the toggle on read so the two controls stay independent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7148e37bbe
commit
7c69807f1d
8 changed files with 217 additions and 13 deletions
|
|
@ -1,8 +1,41 @@
|
|||
import type { ListRunsDirectionEnum, ListRunsSortEnum } from "@qltysh/fabro-api-client";
|
||||
import type {
|
||||
BoardColumn,
|
||||
ListRunsDirectionEnum,
|
||||
ListRunsSortEnum,
|
||||
} from "@qltysh/fabro-api-client";
|
||||
|
||||
import { columnStatuses } from "../../data/runs";
|
||||
import { parseHiddenColumns, serializeHiddenColumns } from "./toggleable-column";
|
||||
import type { ToggleableColumn } from "./toggleable-column";
|
||||
|
||||
// The Status filter is independent of the "Show archived" toggle; archived
|
||||
// runs are gated by that toggle and not by selecting "archived" here.
|
||||
export const STATUS_FILTER_OPTIONS: readonly BoardColumn[] = columnStatuses.filter(
|
||||
(c) => c !== "archived",
|
||||
);
|
||||
|
||||
const STATUS_FILTER_OPTION_SET = new Set<string>(STATUS_FILTER_OPTIONS);
|
||||
|
||||
export function parseStatusFilter(raw: string): Set<BoardColumn> {
|
||||
if (raw === "") return new Set<BoardColumn>();
|
||||
const filter = new Set<BoardColumn>();
|
||||
for (const value of raw.split(",")) {
|
||||
const trimmed = value.trim();
|
||||
if (STATUS_FILTER_OPTION_SET.has(trimmed)) filter.add(trimmed as BoardColumn);
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
function serializeStatusFilter(filter: Set<BoardColumn>): string {
|
||||
return STATUS_FILTER_OPTIONS.filter((c) => filter.has(c)).join(",");
|
||||
}
|
||||
|
||||
// Empty set and a full selection both mean "no filter is narrowing results" —
|
||||
// canonicalize to empty in the URL so back-button history stays clean.
|
||||
function statusFilterIsTrivial(filter: Set<BoardColumn>): boolean {
|
||||
return filter.size === 0 || filter.size === STATUS_FILTER_OPTIONS.length;
|
||||
}
|
||||
|
||||
// Columns hidden by default in both the main runs list and the Children
|
||||
// sub-tab. Users can still reveal them via the column picker.
|
||||
export const DEFAULT_HIDDEN_RUN_LIST_COLUMNS: readonly ToggleableColumn[] = [
|
||||
|
|
@ -105,6 +138,7 @@ const RUNS_WORKSPACE_PARAM_KEYS = [
|
|||
"repo",
|
||||
"workflow",
|
||||
"created",
|
||||
"status",
|
||||
"archived",
|
||||
"sort",
|
||||
"direction",
|
||||
|
|
@ -121,6 +155,7 @@ export interface RunsWorkspacePreferences {
|
|||
repo: string;
|
||||
workflow: string;
|
||||
created: CreatedFilter;
|
||||
status: Set<BoardColumn>;
|
||||
archived: boolean;
|
||||
sort: ListRunsSortEnum;
|
||||
direction: ListRunsDirectionEnum;
|
||||
|
|
@ -138,6 +173,7 @@ export function defaultRunsWorkspacePreferences(): RunsWorkspacePreferences {
|
|||
repo: "all",
|
||||
workflow: "all",
|
||||
created: "all",
|
||||
status: new Set<BoardColumn>(),
|
||||
archived: false,
|
||||
sort: "created_at",
|
||||
direction: "desc",
|
||||
|
|
@ -177,6 +213,11 @@ function normalizeStoredRunsWorkspacePreferences(value: unknown): RunsWorkspaceP
|
|||
|
||||
const size = record.size;
|
||||
|
||||
const { status, archived } = normalizeStoredStatusAndArchived(
|
||||
record.status,
|
||||
record.archived,
|
||||
);
|
||||
|
||||
return {
|
||||
version: RUNS_PREFERENCES_VERSION,
|
||||
view: parseView(stringValue(record.view)),
|
||||
|
|
@ -184,7 +225,8 @@ function normalizeStoredRunsWorkspacePreferences(value: unknown): RunsWorkspaceP
|
|||
repo: filterPreference(stringValue(record.repo)),
|
||||
workflow: filterPreference(stringValue(record.workflow)),
|
||||
created: parseCreatedFilter(stringValue(record.created)),
|
||||
archived: record.archived === true || record.archived === "1",
|
||||
status,
|
||||
archived,
|
||||
sort: parseSort(stringValue(record.sort)),
|
||||
direction: parseDirection(stringValue(record.direction)),
|
||||
size: parsePageSize(typeof size === "number" || typeof size === "string" ? String(size) : null),
|
||||
|
|
@ -193,6 +235,24 @@ function normalizeStoredRunsWorkspacePreferences(value: unknown): RunsWorkspaceP
|
|||
};
|
||||
}
|
||||
|
||||
// Some intermediate records folded "archived" into the status string. Strip
|
||||
// that token out and flip the archived toggle instead, since "Show archived"
|
||||
// is owned by its own preference now.
|
||||
function normalizeStoredStatusAndArchived(
|
||||
storedStatus: unknown,
|
||||
storedArchived: unknown,
|
||||
): { status: Set<BoardColumn>; archived: boolean } {
|
||||
let archived = storedArchived === true || storedArchived === "1";
|
||||
let status = new Set<BoardColumn>();
|
||||
if (typeof storedStatus === "string") {
|
||||
status = parseStatusFilter(storedStatus);
|
||||
if ((storedStatus.split(",").map((s) => s.trim())).includes("archived")) {
|
||||
archived = true;
|
||||
}
|
||||
}
|
||||
return { status, archived };
|
||||
}
|
||||
|
||||
// Stored records from before the default-hidden-columns change have no `hide`
|
||||
// field. Treat the absence as "use the new defaults" so existing users pick up
|
||||
// the new default automatically. An explicit empty string means the user
|
||||
|
|
@ -205,6 +265,7 @@ function normalizeStoredHide(stored: string | null): string {
|
|||
export function runsWorkspacePreferencesFromSearchParams(
|
||||
searchParams: URLSearchParams,
|
||||
): RunsWorkspacePreferences {
|
||||
const rawStatus = searchParams.get("status");
|
||||
return {
|
||||
version: RUNS_PREFERENCES_VERSION,
|
||||
view: parseView(searchParams.get("view")),
|
||||
|
|
@ -212,6 +273,7 @@ export function runsWorkspacePreferencesFromSearchParams(
|
|||
repo: filterPreference(searchParams.get("repo")),
|
||||
workflow: filterPreference(searchParams.get("workflow")),
|
||||
created: parseCreatedFilter(searchParams.get("created")),
|
||||
status: rawStatus == null ? new Set<BoardColumn>() : parseStatusFilter(rawStatus),
|
||||
archived: searchParams.get("archived") === "1",
|
||||
sort: parseSort(searchParams.get("sort")),
|
||||
direction: parseDirection(searchParams.get("direction")),
|
||||
|
|
@ -230,6 +292,9 @@ export function runsWorkspacePreferencesToSearchParams(
|
|||
if (preferences.repo !== "all") params.set("repo", preferences.repo);
|
||||
if (preferences.workflow !== "all") params.set("workflow", preferences.workflow);
|
||||
if (preferences.created !== "all") params.set("created", preferences.created);
|
||||
if (!statusFilterIsTrivial(preferences.status)) {
|
||||
params.set("status", serializeStatusFilter(preferences.status));
|
||||
}
|
||||
if (preferences.archived) params.set("archived", "1");
|
||||
if (preferences.sort !== "created_at") params.set("sort", preferences.sort);
|
||||
if (preferences.direction === "asc") params.set("direction", "asc");
|
||||
|
|
@ -278,7 +343,10 @@ export function persistRunsWorkspacePreferences(
|
|||
) {
|
||||
if (storage == null) return;
|
||||
// `page` is URL-only ephemeral view state; strip it before persisting.
|
||||
const { page: _page, ...storable } = preferences;
|
||||
// `status` is a Set, which doesn't survive JSON.stringify — flatten to the
|
||||
// same comma-separated form we use in the URL.
|
||||
const { page: _page, status, ...rest } = preferences;
|
||||
const storable = { ...rest, status: serializeStatusFilter(status) };
|
||||
try {
|
||||
storage.setItem(RUNS_PREFERENCES_STORAGE_KEY, JSON.stringify(storable));
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useMemo, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import type {
|
||||
BoardColumn,
|
||||
ListRunsDirectionEnum,
|
||||
ListRunsSortEnum,
|
||||
PaginatedRunList,
|
||||
|
|
@ -17,6 +18,7 @@ import { SortHeader } from "./sort-header";
|
|||
import type { ToggleableColumn } from "./toggleable-column";
|
||||
|
||||
const EMPTY_SELECTION = new Set<string>();
|
||||
const EMPTY_STATUS_FILTER: ReadonlySet<BoardColumn> = new Set();
|
||||
|
||||
export type RunsListViewProps = {
|
||||
data: PaginatedRunList | undefined;
|
||||
|
|
@ -33,6 +35,7 @@ export type RunsListViewProps = {
|
|||
query: string;
|
||||
repoFilter: string;
|
||||
workflowFilter: string;
|
||||
statusFilter?: ReadonlySet<BoardColumn>;
|
||||
createdCutoffMs: number | null;
|
||||
};
|
||||
|
||||
|
|
@ -51,15 +54,18 @@ export function RunsListView({
|
|||
query,
|
||||
repoFilter,
|
||||
workflowFilter,
|
||||
statusFilter = EMPTY_STATUS_FILTER,
|
||||
createdCutoffMs,
|
||||
}: RunsListViewProps) {
|
||||
const show = (col: ToggleableColumn) => !hiddenColumns.has(col);
|
||||
const rows: RunWithStatus[] = useMemo(() => {
|
||||
const apiRuns = data?.data ?? [];
|
||||
const next: RunWithStatus[] = [];
|
||||
const filterStatuses = statusFilter.size > 0;
|
||||
for (const run of apiRuns) {
|
||||
const item = toRunWithStatus(run);
|
||||
if (
|
||||
(!filterStatuses || statusFilter.has(item.status)) &&
|
||||
(repoFilter === "all" || item.repo === repoFilter) &&
|
||||
(workflowFilter === "all" || item.workflow === workflowFilter) &&
|
||||
(createdCutoffMs == null ||
|
||||
|
|
@ -74,7 +80,7 @@ export function RunsListView({
|
|||
}
|
||||
}
|
||||
return next;
|
||||
}, [data, repoFilter, workflowFilter, createdCutoffMs, query]);
|
||||
}, [data, repoFilter, workflowFilter, statusFilter, createdCutoffMs, query]);
|
||||
|
||||
const hasMore = data?.meta.has_more ?? false;
|
||||
const total = data?.meta.total ?? null;
|
||||
|
|
@ -83,7 +89,8 @@ export function RunsListView({
|
|||
const apiRunCount = data?.data.length ?? 0;
|
||||
const isEmptyServerSide = data !== undefined && apiRunCount === 0 && page === 1;
|
||||
|
||||
const selectionScopeKey = `${page}:${sort}:${direction}:${query}:${repoFilter}:${workflowFilter}:${createdCutoffMs ?? ""}`;
|
||||
const statusScopeKey = [...statusFilter].sort().join(",");
|
||||
const selectionScopeKey = `${page}:${sort}:${direction}:${query}:${repoFilter}:${workflowFilter}:${statusScopeKey}:${createdCutoffMs ?? ""}`;
|
||||
const [selection, setSelection] = useState<{
|
||||
scopeKey: string;
|
||||
ids: Set<string>;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import { CheckIcon, PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/react";
|
||||
import type { BoardColumn } from "@qltysh/fabro-api-client";
|
||||
|
||||
import { columnStatusDisplay } from "../../data/runs";
|
||||
import { STATUS_FILTER_OPTIONS } from "./preferences";
|
||||
|
||||
export function StatusFilterButton({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: Set<BoardColumn>;
|
||||
onChange: (next: Set<BoardColumn>) => void;
|
||||
}) {
|
||||
// Empty set and a full selection both mean "no filter is narrowing the
|
||||
// result set", so leave the button looking inactive in both cases.
|
||||
const active =
|
||||
value.size > 0 && value.size < STATUS_FILTER_OPTIONS.length;
|
||||
|
||||
const selectedList = STATUS_FILTER_OPTIONS.filter((c) => value.has(c));
|
||||
const buttonLabel = (() => {
|
||||
if (!active) return "Status";
|
||||
if (selectedList.length === 1) {
|
||||
return `Status: ${columnStatusDisplay[selectedList[0]].label}`;
|
||||
}
|
||||
return `Status: ${selectedList.length}`;
|
||||
})();
|
||||
|
||||
const selectedArray = [...value];
|
||||
|
||||
return (
|
||||
<Listbox
|
||||
value={selectedArray}
|
||||
onChange={(next: BoardColumn[]) => onChange(new Set<BoardColumn>(next))}
|
||||
multiple
|
||||
>
|
||||
<ListboxButton
|
||||
className={`inline-flex items-center gap-1.5 rounded-md border px-3 py-2 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? "border-line-strong bg-panel text-fg-2"
|
||||
: "border-line bg-panel/80 text-fg-muted hover:text-fg-3"
|
||||
}`}
|
||||
>
|
||||
<PlusCircleIcon className="size-4" aria-hidden="true" />
|
||||
<span>{buttonLabel}</span>
|
||||
</ListboxButton>
|
||||
<ListboxOptions
|
||||
anchor="bottom start"
|
||||
className="z-20 mt-1 min-w-[12rem] rounded-md border border-line bg-panel py-1 text-xs shadow-lg focus:outline-none"
|
||||
>
|
||||
{STATUS_FILTER_OPTIONS.map((status) => {
|
||||
const display = columnStatusDisplay[status];
|
||||
return (
|
||||
<ListboxOption
|
||||
key={status}
|
||||
value={status}
|
||||
className={({ focus }) =>
|
||||
`flex cursor-pointer items-center justify-between gap-3 px-3 py-1.5 text-fg-2 ${focus ? "bg-overlay" : ""}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className={`size-2 shrink-0 rounded-full ${display.dot}`} aria-hidden="true" />
|
||||
<span className={selected ? display.text : "text-fg-2"}>{display.label}</span>
|
||||
</span>
|
||||
{selected ? (
|
||||
<CheckIcon className="size-4 shrink-0 text-teal-500" aria-hidden="true" />
|
||||
) : (
|
||||
<span className="size-4 shrink-0" aria-hidden="true" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
);
|
||||
})}
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
);
|
||||
}
|
||||
|
|
@ -283,6 +283,9 @@ describe("Runs workspace preference restoration", () => {
|
|||
await act(async () => {
|
||||
compositeByName(renderer, "FilterButton", (props) => props.label === "Time").props.onChange("7d");
|
||||
});
|
||||
await act(async () => {
|
||||
compositeByName(renderer, "StatusFilterButton").props.onChange(new Set(["running", "blocked"]));
|
||||
});
|
||||
await act(async () => {
|
||||
renderer.root.findByProps({ title: "Show archived runs" }).props.onClick();
|
||||
});
|
||||
|
|
@ -296,6 +299,7 @@ describe("Runs workspace preference restoration", () => {
|
|||
repo: "qlty/docs",
|
||||
workflow: "docs",
|
||||
created: "7d",
|
||||
status: "running,blocked",
|
||||
archived: true,
|
||||
hide: "repo,workflow",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -263,6 +263,7 @@ describe("runs route workspace preferences", () => {
|
|||
repo: "qlty/fabro",
|
||||
workflow: "release",
|
||||
created: "7d",
|
||||
status: "running,blocked",
|
||||
archived: true,
|
||||
sort: "updated_at",
|
||||
direction: "asc",
|
||||
|
|
@ -273,10 +274,25 @@ describe("runs route workspace preferences", () => {
|
|||
);
|
||||
|
||||
expect(loadStoredRunsWorkspaceSearchParams(storage).toString()).toBe(
|
||||
"view=list&search=retry+failures&repo=qlty%2Ffabro&workflow=release&created=7d&archived=1&sort=updated_at&direction=asc&size=50&hide=repo%2Cchanges",
|
||||
"view=list&search=retry+failures&repo=qlty%2Ffabro&workflow=release&created=7d&status=running%2Cblocked&archived=1&sort=updated_at&direction=asc&size=50&hide=repo%2Cchanges",
|
||||
);
|
||||
});
|
||||
|
||||
test("stored archived in a status string migrates into the standalone archived toggle", () => {
|
||||
const storage = new MemoryStorage();
|
||||
storage.setItem(
|
||||
RUNS_PREFERENCES_STORAGE_KEY,
|
||||
JSON.stringify({ version: 1, view: "list", status: "running,archived" }),
|
||||
);
|
||||
|
||||
const params = loadStoredRunsWorkspaceSearchParams(storage);
|
||||
expect(params.get("view")).toBe("list");
|
||||
// The "archived" token is stripped out of the status filter and flipped
|
||||
// into the separate archived toggle so the two controls don't entangle.
|
||||
expect(params.get("status")).toBe("running");
|
||||
expect(params.get("archived")).toBe("1");
|
||||
});
|
||||
|
||||
test("persisting preferences omits page and stores canonical values", () => {
|
||||
const storage = new MemoryStorage();
|
||||
|
||||
|
|
@ -288,6 +304,7 @@ describe("runs route workspace preferences", () => {
|
|||
repo: "all",
|
||||
workflow: "all",
|
||||
created: "1d",
|
||||
status: new Set<BoardColumn>(["running", "blocked"]),
|
||||
archived: false,
|
||||
sort: "created_at",
|
||||
direction: "asc",
|
||||
|
|
@ -305,6 +322,7 @@ describe("runs route workspace preferences", () => {
|
|||
repo: "all",
|
||||
workflow: "all",
|
||||
created: "1d",
|
||||
status: "running,blocked",
|
||||
archived: false,
|
||||
sort: "created_at",
|
||||
direction: "asc",
|
||||
|
|
|
|||
|
|
@ -721,6 +721,7 @@ export default function Runs() {
|
|||
repoFilter,
|
||||
workflowFilter,
|
||||
createdFilter,
|
||||
statusFilter,
|
||||
includeArchived,
|
||||
view,
|
||||
sort,
|
||||
|
|
@ -732,6 +733,7 @@ export default function Runs() {
|
|||
setRepoFilter,
|
||||
setWorkflowFilter,
|
||||
setCreatedFilter,
|
||||
setStatusFilter,
|
||||
setIncludeArchived,
|
||||
setView,
|
||||
setPage,
|
||||
|
|
@ -827,7 +829,13 @@ export default function Runs() {
|
|||
(sum, col) => sum + col.items.length,
|
||||
0,
|
||||
);
|
||||
const visibleColumns = placeArchivedColumnLast(filteredColumns, includeArchived).filter(
|
||||
// Empty status filter means "show all"; otherwise only render the lanes
|
||||
// whose status the user explicitly selected.
|
||||
const statusVisibleColumns =
|
||||
statusFilter.size === 0
|
||||
? filteredColumns
|
||||
: filteredColumns.filter((col) => statusFilter.has(col.id));
|
||||
const visibleColumns = placeArchivedColumnLast(statusVisibleColumns, includeArchived).filter(
|
||||
(col) => col.id !== "pending" || col.items.length > 0,
|
||||
);
|
||||
|
||||
|
|
@ -839,6 +847,7 @@ export default function Runs() {
|
|||
repoFilter={repoFilter}
|
||||
workflowFilter={workflowFilter}
|
||||
createdFilter={createdFilter}
|
||||
statusFilter={statusFilter}
|
||||
includeArchived={includeArchived}
|
||||
view={view}
|
||||
hiddenColumns={hiddenColumns}
|
||||
|
|
@ -848,6 +857,7 @@ export default function Runs() {
|
|||
onRepoFilterChange={setRepoFilter}
|
||||
onWorkflowFilterChange={setWorkflowFilter}
|
||||
onCreatedFilterChange={setCreatedFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
onIncludeArchivedChange={setIncludeArchived}
|
||||
onViewChange={setView}
|
||||
onHiddenColumnsChange={setHiddenColumns}
|
||||
|
|
@ -894,6 +904,7 @@ export default function Runs() {
|
|||
query={lowerQuery}
|
||||
repoFilter={repoFilter}
|
||||
workflowFilter={workflowFilter}
|
||||
statusFilter={statusFilter}
|
||||
createdCutoffMs={createdCutoffMs}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ArchiveBoxIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import type { BoardColumn } from "@qltysh/fabro-api-client";
|
||||
|
||||
import { ColumnPickerButton } from "../../components/runs-list/column-picker-button";
|
||||
import { FilterButton } from "../../components/runs-list/filter-button";
|
||||
|
|
@ -7,6 +8,7 @@ import {
|
|||
type CreatedFilter,
|
||||
type ViewMode,
|
||||
} from "../../components/runs-list/preferences";
|
||||
import { StatusFilterButton } from "../../components/runs-list/status-filter-button";
|
||||
import type { ToggleableColumn } from "../../components/runs-list/toggleable-column";
|
||||
|
||||
interface RunsToolbarProps {
|
||||
|
|
@ -14,6 +16,7 @@ interface RunsToolbarProps {
|
|||
repoFilter: string;
|
||||
workflowFilter: string;
|
||||
createdFilter: CreatedFilter;
|
||||
statusFilter: Set<BoardColumn>;
|
||||
includeArchived: boolean;
|
||||
view: ViewMode;
|
||||
hiddenColumns: Set<ToggleableColumn>;
|
||||
|
|
@ -23,6 +26,7 @@ interface RunsToolbarProps {
|
|||
onRepoFilterChange: (value: string) => void;
|
||||
onWorkflowFilterChange: (value: string) => void;
|
||||
onCreatedFilterChange: (value: CreatedFilter) => void;
|
||||
onStatusFilterChange: (value: Set<BoardColumn>) => void;
|
||||
onIncludeArchivedChange: (value: boolean) => void;
|
||||
onViewChange: (value: ViewMode) => void;
|
||||
onHiddenColumnsChange: (value: Set<ToggleableColumn>) => void;
|
||||
|
|
@ -33,6 +37,7 @@ export function RunsToolbar({
|
|||
repoFilter,
|
||||
workflowFilter,
|
||||
createdFilter,
|
||||
statusFilter,
|
||||
includeArchived,
|
||||
view,
|
||||
hiddenColumns,
|
||||
|
|
@ -42,6 +47,7 @@ export function RunsToolbar({
|
|||
onRepoFilterChange,
|
||||
onWorkflowFilterChange,
|
||||
onCreatedFilterChange,
|
||||
onStatusFilterChange,
|
||||
onIncludeArchivedChange,
|
||||
onViewChange,
|
||||
onHiddenColumnsChange,
|
||||
|
|
@ -61,6 +67,7 @@ export function RunsToolbar({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<StatusFilterButton value={statusFilter} onChange={onStatusFilterChange} />
|
||||
<FilterButton
|
||||
label="Time"
|
||||
value={createdFilter}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
useRef,
|
||||
} from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import type { ListRunsSortEnum } from "@qltysh/fabro-api-client";
|
||||
import type { BoardColumn, ListRunsSortEnum } from "@qltysh/fabro-api-client";
|
||||
|
||||
import {
|
||||
hiddenColumnsFromSearchParams,
|
||||
|
|
@ -32,11 +32,16 @@ export function useRunsWorkspacePreferences() {
|
|||
() => resolveRunsWorkspaceSearchParams(urlSearchParams),
|
||||
[urlSearchParams],
|
||||
);
|
||||
const query = searchParams.get("search") ?? "";
|
||||
const repoFilter = searchParams.get("repo") ?? "all";
|
||||
const workflowFilter = searchParams.get("workflow") ?? "all";
|
||||
const createdFilter = parseCreatedFilter(searchParams.get("created"));
|
||||
const includeArchived = searchParams.get("archived") === "1";
|
||||
const preferences = useMemo(
|
||||
() => runsWorkspacePreferencesFromSearchParams(searchParams),
|
||||
[searchParams],
|
||||
);
|
||||
const query = preferences.search;
|
||||
const repoFilter = preferences.repo;
|
||||
const workflowFilter = preferences.workflow;
|
||||
const createdFilter = preferences.created;
|
||||
const statusFilter = preferences.status;
|
||||
const includeArchived = preferences.archived;
|
||||
const view = parseView(searchParams.get("view"));
|
||||
const sort = parseSort(searchParams.get("sort"));
|
||||
const direction = parseDirection(searchParams.get("direction"));
|
||||
|
|
@ -69,6 +74,8 @@ export function useRunsWorkspacePreferences() {
|
|||
updatePreferences((prev) => ({ ...prev, workflow: value }));
|
||||
const setCreatedFilter = (value: CreatedFilter) =>
|
||||
updatePreferences((prev) => ({ ...prev, created: value }));
|
||||
const setStatusFilter = (value: Set<BoardColumn>) =>
|
||||
updatePreferences((prev) => ({ ...prev, status: value }));
|
||||
const setIncludeArchived = (value: boolean) =>
|
||||
updatePreferences((prev) => ({ ...prev, archived: value }));
|
||||
const setView = (value: ViewMode) =>
|
||||
|
|
@ -109,6 +116,7 @@ export function useRunsWorkspacePreferences() {
|
|||
repoFilter,
|
||||
workflowFilter,
|
||||
createdFilter,
|
||||
statusFilter,
|
||||
includeArchived,
|
||||
view,
|
||||
sort,
|
||||
|
|
@ -120,6 +128,7 @@ export function useRunsWorkspacePreferences() {
|
|||
setRepoFilter,
|
||||
setWorkflowFilter,
|
||||
setCreatedFilter,
|
||||
setStatusFilter,
|
||||
setIncludeArchived,
|
||||
setView,
|
||||
setPage,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue