Clicking a column header could open that column's filter, instead of a filter row that is always there #253

Closed
opened 2026-09-17 14:01:42 +00:00 by tiagoagueda · 0 comments
Owner

Every column of the companies table already filters — that landed with #173 — but the way
in is a second header row of inputs that is on screen whether or not anybody is filtering.
On Companies that is fourteen boxes above the first company, permanently, and the table
they belong to is the thing they push down.

The proposal is the one Dispatcharr uses: the header cell is the filter. Click a column's
header and it becomes an input for that column; leave it and it goes back to being a label,
carrying a mark if a filter is still in force. Nothing is on screen that is not being used.

What exists now

templates/partials/table/head.html renders two rows:

  • the labels, each a sort link (<a id="sort-{{ header.key }}">) with an arrow for the
    state, plus a resize handle app.js adds where data-col-settings is;
  • <tr data-filter-row>, one control per column — text, choice, number (a least and
    a most) and date (a from and a to), every one associated with the filter form by
    form= so the plain Apply button works with no script at all, and carrying htmx so
    typing narrows the table after 300 ms.

That row is hidden … md:table-row. Below md it is not there, and narrow.html repeats
the same filters inside a folded <details> labelled Narrow, because — as the comment
puts it — a row of inputs inside a table at 390 pixels is not a control anybody can use.

So this is not a missing feature. It is a change of gesture, and the question is whether
the gesture is better, not whether filtering exists.

What it has to solve

The header click is taken — sorting moves to an icon. Today the label is the sort
link, so the label cannot also be the filter. Sorting is the one of the two that survives
being a glyph: it has three states and each of them has a picture. The label goes back to
being a label that opens the filter, and beside it a small icon-only control sorts.

We already have the machinery. {% icon %} inlines from a curated set — assets/icons.txt,
47 icons, npm run sync:icons — and arrow-up and arrow-down are the two the header
draws now. Two notes on doing it:

  • The third state has no picture yet. #136 made the unsorted state draw nothing,
    which reads fine when the label beside it is the control but leaves an icon-only button
    with nothing to be. It needs a neutral glyph — lucide's arrow-up-down or
    chevrons-up-down — which is one line in icons.txt and a sync.
  • filter and search are already vendored, so whatever marks a filtered column, or
    opens the filter where the label cannot be clicked, costs nothing to add.

What has to come with it, because an icon is smaller than a word:

  • the sr-only {{ header.hint }} becomes the button's own name — {% icon "arrow-up" label=header.hint %}
    gives it role="img" and a label, so an icon-only control still says which way the next
    click sorts;
  • tap-target is 24 pixels and #115 put it there for 12-pixel type; an icon-only control
    is the case that rule was written for, so it keeps it or grows;
  • id="sort-{{ header.key }}" moves with the control, for #227's reason unchanged: it
    swaps itself away and has to hand focus to its replacement.

The scriptless path. Today every filter is a real form control with form= and Apply,
so the table filters with JavaScript switched off. A header that becomes an input is a
script doing it. Either the inputs stay in the markup and the script folds them away — the
<details> trick, one column at a time — or the no-script case needs a stated answer. The
first is the one that matches how #134 and #136 handled the same problem: the script adds a
control, it never animates a dead one.

A filter you cannot see is a filter you forget. The row's real virtue is that every
narrowing is visible at once. If the inputs fold away, a column with a filter in force has
to say so in its header — the count of active filters, a mark on the column, or both — or
somebody will wonder for ten minutes where their companies went.

Below md nothing changes. The filter row is already not there and Narrow already
serves; this proposal is about the wide layout only. Worth saying so explicitly so the
<details> does not get rebuilt by accident.

Worth deciding

Whether the permanent row goes away entirely or becomes a preference. Somebody filtering
five columns at once wants them all open; somebody reading a long list wants the space.
table_settings already stores per-table, per-user column widths, so there is a place to
keep the choice, and "show the filter row" is a smaller change than either extreme.

Reference: https://github.com/Dispatcharr/Dispatcharr.

Every column of the companies table already filters — that landed with #173 — but the way in is a second header row of inputs that is on screen whether or not anybody is filtering. On *Companies* that is fourteen boxes above the first company, permanently, and the table they belong to is the thing they push down. The proposal is the one Dispatcharr uses: the header cell *is* the filter. Click a column's header and it becomes an input for that column; leave it and it goes back to being a label, carrying a mark if a filter is still in force. Nothing is on screen that is not being used. ## What exists now `templates/partials/table/head.html` renders two rows: - the labels, each a sort link (`<a id="sort-{{ header.key }}">`) with an arrow for the state, plus a resize handle `app.js` adds where `data-col-settings` is; - `<tr data-filter-row>`, one control per column — `text`, `choice`, `number` (a least and a most) and `date` (a from and a to), every one associated with the filter form by `form=` so the plain **Apply** button works with no script at all, and carrying htmx so typing narrows the table after 300 ms. That row is `hidden … md:table-row`. Below `md` it is not there, and `narrow.html` repeats the same filters inside a folded `<details>` labelled *Narrow*, because — as the comment puts it — a row of inputs inside a table at 390 pixels is not a control anybody can use. So this is not a missing feature. It is a change of gesture, and the question is whether the gesture is better, not whether filtering exists. ## What it has to solve **The header click is taken — sorting moves to an icon.** Today the label *is* the sort link, so the label cannot also be the filter. Sorting is the one of the two that survives being a glyph: it has three states and each of them has a picture. The label goes back to being a label that opens the filter, and beside it a small icon-only control sorts. We already have the machinery. `{% icon %}` inlines from a curated set — `assets/icons.txt`, 47 icons, `npm run sync:icons` — and `arrow-up` and `arrow-down` are the two the header draws now. Two notes on doing it: - **The third state has no picture yet.** #136 made the unsorted state draw *nothing*, which reads fine when the label beside it is the control but leaves an icon-only button with nothing to be. It needs a neutral glyph — lucide's `arrow-up-down` or `chevrons-up-down` — which is one line in `icons.txt` and a sync. - **`filter` and `search` are already vendored**, so whatever marks a filtered column, or opens the filter where the label cannot be clicked, costs nothing to add. What has to come with it, because an icon is smaller than a word: - the sr-only `{{ header.hint }}` becomes the button's own name — `{% icon "arrow-up" label=header.hint %}` gives it `role="img"` and a label, so an icon-only control still says which way the next click sorts; - `tap-target` is 24 pixels and #115 put it there for 12-pixel type; an icon-only control is the case that rule was written for, so it keeps it or grows; - `id="sort-{{ header.key }}"` moves with the control, for #227's reason unchanged: it swaps itself away and has to hand focus to its replacement. **The scriptless path.** Today every filter is a real form control with `form=` and Apply, so the table filters with JavaScript switched off. A header that *becomes* an input is a script doing it. Either the inputs stay in the markup and the script folds them away — the `<details>` trick, one column at a time — or the no-script case needs a stated answer. The first is the one that matches how #134 and #136 handled the same problem: the script adds a control, it never animates a dead one. **A filter you cannot see is a filter you forget.** The row's real virtue is that every narrowing is visible at once. If the inputs fold away, a column with a filter in force has to say so in its header — the count of active filters, a mark on the column, or both — or somebody will wonder for ten minutes where their companies went. **Below `md` nothing changes.** The filter row is already not there and *Narrow* already serves; this proposal is about the wide layout only. Worth saying so explicitly so the `<details>` does not get rebuilt by accident. ## Worth deciding Whether the permanent row goes away entirely or becomes a preference. Somebody filtering five columns at once wants them all open; somebody reading a long list wants the space. `table_settings` already stores per-table, per-user column widths, so there is a place to keep the choice, and "show the filter row" is a smaller change than either extreme. Reference: <https://github.com/Dispatcharr/Dispatcharr>.
tiagoagueda added this to the 0.4.0 milestone 2026-09-17 14:01:42 +00:00
tiagoagueda modified the milestone from 0.4.0 to 0.5.0 2026-09-19 10:31:13 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#253
No description provided.