diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx index d46129dd..350876ed 100644 --- a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx @@ -318,13 +318,11 @@ export function AgentsPage() { header: "hive", render: (a) => a.hive ?? "—", sortBy: (a) => a.hive ?? "", - // "—" (not "") so the missing-hive option in the filter list reads - // the same as the cell itself, rather than showing a blank choice. - // Multiselect, not single-value `"select"` — an operator narrowing - // to a handful of hives at once shouldn't need to filter one at a - // time. - filterValues: (a) => [a.hive ?? "—"], - filterMode: "multiselect", + // "—" (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", @@ -335,9 +333,8 @@ export function AgentsPage() { // label and blew the row out (mara: "looks messy"). That string // now lives in its own "message" column below. sortBy: (a) => FRESHNESS[a.freshness].label, - // Multiselect, same reasoning as the hive column above. - filterValues: (a) => [FRESHNESS[a.freshness].label], - filterMode: "multiselect", + filterValue: (a) => FRESHNESS[a.freshness].label, + filterMode: "select", render: (a) => { const { tone, label } = FRESHNESS[a.freshness]; return ( @@ -382,9 +379,8 @@ export function AgentsPage() { // real per-badge logic lives there, this column just wires its // callbacks to the page's own state/handlers. sortBy: (a) => a.wanted ?? "", - // Multiselect, same reasoning as the hive column above. - filterValues: (a) => [a.wanted ?? "no declaration"], - filterMode: "multiselect", + filterValue: (a) => a.wanted ?? "no declaration", + filterMode: "select", render: (a) => { const err = actionErrors.get(a.name); return ( diff --git a/frontend/packages/swarm-ui/src/pages/HivesPage.tsx b/frontend/packages/swarm-ui/src/pages/HivesPage.tsx index 518aa0af..4d0deda4 100644 --- a/frontend/packages/swarm-ui/src/pages/HivesPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/HivesPage.tsx @@ -91,11 +91,8 @@ const COLUMNS: TableColumn[] = [ ); }, sortBy: (h) => FRESHNESS[h.freshness].label, - // Multiselect (not the single-value `"select"` mode) so the operator - // can filter for e.g. "stale or offline" in one pass rather than one - // freshness state at a time. - filterValues: (h) => [FRESHNESS[h.freshness].label], - filterMode: "multiselect", + filterValue: (h) => FRESHNESS[h.freshness].label, + filterMode: "select", }, ]; diff --git a/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx b/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx index 95f84da9..1c95de8d 100644 --- a/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx @@ -312,12 +312,14 @@ export function IssueReportPage() { ), ariaSort: ariaSortFor("assignees"), render: (r) => (r.assignees.length ? r.assignees.join(", ") : "—"), - // Multiselect, same machinery the "labels" column above already - // uses — `r.assignees` is already the per-row array this mode - // wants, so there's no reason to fall back to a joined-string - // substring search here. - filterValues: (r) => r.assignees, - filterMode: "multiselect", + // Text, not `"select"` — a row can carry more than one assignee, + // and `Table`'s select mode matches one whole string per row + // exactly, so a real per-assignee dropdown would need `Table` + // itself to grow multi-value filtering. Substring search over + // the joined string still finds "iris" inside "damocles, iris" + // correctly, which covers the actual gap (no way to filter by + // assignee at all today). + filterValue: (r) => r.assignees.join(", "), }, { key: "blocked", @@ -335,11 +337,11 @@ export function IssueReportPage() { r.blocked ? : "—", // A distinct gap from the existing "hide blocked" toggle above: // that one only *hides* blocked issues, there was no way to see - // *only* the blocked ones. Synthesized two-value set (not the raw - // boolean) — multiselect, same as every other enum-shaped column, - // rather than the single-value `"select"` mode. - filterValues: (r) => [r.blocked ? "blocked" : "not blocked"], - filterMode: "multiselect", + // *only* the blocked ones. Synthesized two-value string (not the + // raw boolean) — `Table`'s select mode needs a string to match, + // same as `AgentsPage`'s `wanted ?? "no declaration"` pattern. + filterValue: (r) => (r.blocked ? "blocked" : "not blocked"), + filterMode: "select", }, { key: "depended_on_by_count", diff --git a/frontend/packages/swarm-ui/src/ui/table/Table.css b/frontend/packages/swarm-ui/src/ui/table/Table.css index 6be32e7a..6a0a8d1b 100644 --- a/frontend/packages/swarm-ui/src/ui/table/Table.css +++ b/frontend/packages/swarm-ui/src/ui/table/Table.css @@ -129,7 +129,8 @@ border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); } -.ui-table-filter-input { +.ui-table-filter-input, +.ui-table-filter-select { width: 100%; box-sizing: border-box; background: var(--bg); diff --git a/frontend/packages/swarm-ui/src/ui/table/Table.tsx b/frontend/packages/swarm-ui/src/ui/table/Table.tsx index 7683a05b..18e4bb0f 100644 --- a/frontend/packages/swarm-ui/src/ui/table/Table.tsx +++ b/frontend/packages/swarm-ui/src/ui/table/Table.tsx @@ -77,24 +77,22 @@ export interface TableColumn { /** * `"text"` (default): free substring, case-insensitive `` — * today's only behavior, unaffected if you don't set this. - * `"multiselect"`: a checkbox list, populated from the *distinct* - * `filterValues` results across the currently-loaded rows (deduped, - * case-insensitive sort), matching on any overlap between the row's - * values and the operator's current selection. 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. Works the same whether a row naturally carries a - * *set* of values (e.g. issue labels — pass the real array) or just - * one (wrap it in a 1-element array). Deriving options from live rows - * rather than a hardcoded enum means the list never offers a choice - * that would match zero rows. Ignored unless `filterValues` is set. - * There's deliberately no separate single-value `"select"` mode — - * multiselect subsumes it (mara: "why have a non multi select? the - * user can always just choose 1"), so a single-choice picker never - * needs distinct handling from an N-choice one. + * `"select"`: a ` { + const v = (e.target as HTMLSelectElement).value; + updateFilter(c.key, { value: v }); + }} + > + + {selectOptionsFor(c).map((v) => ( + + ))} + + {renderNegateToggle(c)} + + ); + } return ( <>