swarm-ui: bounded-domain table filters become dropdowns
Table columns whose value only ever comes from a small closed set (freshness, wanted, hive) get a <select> in their filter-row cell instead of a free-text input, populated from the distinct values present in the currently-loaded rows plus an "any" option, matched by exact equality instead of substring. Free-text columns (name, the agent's own status message, config PR, hive domain) are unchanged.
This commit is contained in:
parent
a204b5f457
commit
4544c458eb
4 changed files with 63 additions and 8 deletions
|
|
@ -310,7 +310,11 @@ export function AgentsPage() {
|
|||
header: "hive",
|
||||
render: (a) => a.hive ?? "—",
|
||||
sortBy: (a) => a.hive ?? "",
|
||||
filterValue: (a) => a.hive ?? "",
|
||||
// "—" (not "") so the missing-hive option in the filter dropdown
|
||||
// reads the same as the cell itself, rather than showing a blank
|
||||
// choice.
|
||||
filterValue: (a) => a.hive ?? "—",
|
||||
filterMode: "select",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
|
|
@ -322,6 +326,7 @@ export function AgentsPage() {
|
|||
// now lives in its own "message" column below.
|
||||
sortBy: (a) => FRESHNESS[a.freshness].label,
|
||||
filterValue: (a) => FRESHNESS[a.freshness].label,
|
||||
filterMode: "select",
|
||||
render: (a) => {
|
||||
const { tone, label } = FRESHNESS[a.freshness];
|
||||
return (
|
||||
|
|
@ -367,6 +372,7 @@ export function AgentsPage() {
|
|||
// callbacks to the page's own state/handlers.
|
||||
sortBy: (a) => a.wanted ?? "",
|
||||
filterValue: (a) => a.wanted ?? "no declaration",
|
||||
filterMode: "select",
|
||||
render: (a) => {
|
||||
const err = actionErrors.get(a.name);
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ const COLUMNS: TableColumn<HiveStatus>[] = [
|
|||
},
|
||||
sortBy: (h) => FRESHNESS[h.freshness].label,
|
||||
filterValue: (h) => FRESHNESS[h.freshness].label,
|
||||
filterMode: "select",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@
|
|||
padding-bottom: 0.5em;
|
||||
font-weight: 400;
|
||||
}
|
||||
.ui-table-filter-input {
|
||||
.ui-table-filter-input,
|
||||
.ui-table-filter-select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--bg);
|
||||
|
|
|
|||
|
|
@ -51,13 +51,28 @@ export interface TableColumn<T> {
|
|||
*/
|
||||
sortBy?: (row: T) => string | number;
|
||||
/**
|
||||
* Column is filterable (a text input in its own row under the
|
||||
* headers) when present — extracts the substring to match a case-
|
||||
* insensitive filter query against. Same one-signal reasoning as
|
||||
* `sortBy`: no separate `filterable` flag, the extractor's presence
|
||||
* is the flag.
|
||||
* Column is filterable (a control in its own row under the headers)
|
||||
* when present — extracts the string to filter this column against.
|
||||
* Same one-signal reasoning as `sortBy`: no separate `filterable`
|
||||
* flag, the extractor's presence is the flag. How the extracted
|
||||
* string is matched against the operator's input is `filterMode`.
|
||||
*/
|
||||
filterValue?: (row: T) => string;
|
||||
/**
|
||||
* `"text"` (default): free substring, case-insensitive `<input>` —
|
||||
* today's only behavior, unaffected if you don't set this.
|
||||
* `"select"`: a `<select>` populated from the *distinct* `filterValue`
|
||||
* results across the currently-loaded rows (deduped, case-insensitive
|
||||
* sort) plus a leading "any" option, matched by exact equality. Pick
|
||||
* this for a column whose value only ever comes from a small
|
||||
* bounded/closed set (an enum label, a hive name) — typing an exact
|
||||
* substring is friction a picker removes, and a substring query can
|
||||
* accidentally straddle two distinct values. Deriving options from
|
||||
* live rows rather than a hardcoded enum means the list never offers
|
||||
* a choice that would match zero rows. Ignored unless `filterValue`
|
||||
* is also set.
|
||||
*/
|
||||
filterMode?: "text" | "select";
|
||||
}
|
||||
|
||||
type SortDir = "asc" | "desc";
|
||||
|
|
@ -104,6 +119,7 @@ export function Table<T>({
|
|||
activeFilters.every(([key, query]) => {
|
||||
const col = columns.find((c) => c.key === key);
|
||||
const value = col?.filterValue?.(row) ?? "";
|
||||
if (col?.filterMode === "select") return value === query;
|
||||
return value.toLowerCase().includes(query.toLowerCase());
|
||||
}),
|
||||
);
|
||||
|
|
@ -141,6 +157,20 @@ export function Table<T>({
|
|||
return sort.dir === "asc" ? "ascending" : "descending";
|
||||
}
|
||||
|
||||
// Distinct values currently present for a `"select"`-mode column, so
|
||||
// the dropdown never offers an option that would match zero rows.
|
||||
// Computed off `rows` (pre-filter) — every column's own option list
|
||||
// stays stable while a sibling column's filter narrows `visibleRows`,
|
||||
// matching how a spreadsheet's column filters don't hide each other's
|
||||
// choices.
|
||||
function selectOptionsFor(c: TableColumn<T>): string[] {
|
||||
if (!c.filterValue) return [];
|
||||
const values = new Set(rows.map(c.filterValue));
|
||||
return Array.from(values).sort((a, b) =>
|
||||
a.localeCompare(b, undefined, { sensitivity: "base", numeric: true }),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="ui-table-scroll">
|
||||
<table class="ui-table">
|
||||
|
|
@ -175,7 +205,24 @@ export function Table<T>({
|
|||
<tr class="ui-table-filter-row">
|
||||
{columns.map((c) => (
|
||||
<th key={c.key}>
|
||||
{c.filterValue ? (
|
||||
{c.filterValue && c.filterMode === "select" ? (
|
||||
<select
|
||||
class="ui-table-filter-select"
|
||||
aria-label={`filter by ${typeof c.header === "string" ? c.header : c.key}`}
|
||||
value={filters[c.key] ?? ""}
|
||||
onChange={(e) => {
|
||||
const v = (e.target as HTMLSelectElement).value;
|
||||
setFilters((prev) => ({ ...prev, [c.key]: v }));
|
||||
}}
|
||||
>
|
||||
<option value="">any</option>
|
||||
{selectOptionsFor(c).map((v) => (
|
||||
<option key={v} value={v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : c.filterValue ? (
|
||||
<input
|
||||
type="text"
|
||||
class="ui-table-filter-input"
|
||||
|
|
|
|||
Loading…
Reference in a new issue