swarm-ui: drop Table's now-unused single-value select filter mode

This commit is contained in:
iris 2026-09-10 21:07:37 +02:00
commit a95a81bb12
2 changed files with 30 additions and 72 deletions

View file

@ -129,8 +129,7 @@
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
.ui-table-filter-input,
.ui-table-filter-select {
.ui-table-filter-input {
width: 100%;
box-sizing: border-box;
background: var(--bg);

View file

@ -77,22 +77,24 @@ export interface TableColumn<T> {
/**
* `"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.
* `"multiselect"`: a checkbox list, same live-rows-derived option
* source as `"select"` but over `filterValues` and matching on any
* overlap rather than one exact value for a column whose row value
* is itself a set (e.g. labels). Ignored unless `filterValues` is set.
* `"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.
*/
filterMode?: "text" | "select" | "multiselect";
filterMode?: "text" | "multiselect";
}
// One filter's full state: `value` for `"text"`/`"select"`, `values` for
@ -290,9 +292,6 @@ export function Table<T>({
if (col?.filterMode === "multiselect") {
const rowValues = col.filterValues?.(row) ?? [];
matches = f.values.some((v) => rowValues.includes(v));
} else if (col?.filterMode === "select") {
const value = col.filterValue?.(row) ?? "";
matches = value === f.value;
} else {
const value = col?.filterValue?.(row) ?? "";
matches = value.toLowerCase().includes(f.value.toLowerCase());
@ -334,22 +333,13 @@ 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 }),
);
}
// Same reasoning as `selectOptionsFor` above, over `filterValues`
// (a row contributes every one of its own values, not just one).
// Distinct values currently present for a `"multiselect"`-mode column,
// so the checkbox list 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. A row contributes every one of its own
// values, not just one.
function multiselectOptionsFor(c: TableColumn<T>): string[] {
if (!c.filterValues) return [];
const values = new Set<string>();
@ -362,17 +352,10 @@ export function Table<T>({
// Only one popover is ever open at a time, so one ref (rather than a
// per-column ref map) is enough to focus whichever control just
// mounted — typing immediately after the click that opened it,
// without a separate click into the field first. A callback ref
// (not `useRef`'s object form attached directly to the element)
// because the element is one of two different concrete types
// depending on `filterMode` — `RefObject<HTMLInputElement |
// HTMLSelectElement>` doesn't structurally satisfy either element's
// own narrower `ref` prop type, but a plain callback taking the
// union does.
const popoverControlRef = useRef<HTMLElement | null>(null);
function setPopoverControlRef(el: HTMLElement | null) {
popoverControlRef.current = el;
}
// without a separate click into the field first. (The multiselect
// checkbox list below doesn't use this — there's no single "the"
// control to focus in a list of several.)
const popoverControlRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (openFilterKey !== null) popoverControlRef.current?.focus();
}, [openFilterKey]);
@ -423,34 +406,10 @@ export function Table<T>({
</>
);
}
if (c.filterMode === "select") {
return (
<>
<select
ref={setPopoverControlRef}
class="ui-table-filter-select"
aria-label={filterLabel(c)}
value={getFilter(c.key).value}
onChange={(e) => {
const v = (e.target as HTMLSelectElement).value;
updateFilter(c.key, { value: v });
}}
>
<option value="">any</option>
{selectOptionsFor(c).map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
{renderNegateToggle(c)}
</>
);
}
return (
<>
<input
ref={setPopoverControlRef}
ref={popoverControlRef}
type="text"
class="ui-table-filter-input"
placeholder="filter…"