Table filters: a filter the page was loaded with cannot be changed or cleared from its column header #622

Closed
opened 2026-10-01 10:48:27 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the table filter templates and Table.given, then driving Companies in headless Chromium with every request answered in-process by the test client.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

Every column filter is drawn twice and both copies are posted under the same name: once in the column header and once in the phone block Narrow. Table.given takes the first non-empty value, on the assumption that "the copy nobody typed in narrows nothing". That only holds while the untouched copy is empty. The Narrow block sits inside the filter form, outside the region a live filter swaps, so it keeps the value the page was rendered with, and it comes first in the form's element order. So once a page has been loaded with a filter in its address, the header box for that filter no longer does anything: clearing it or typing something else leaves the table filtered by the old value. Only Clear gets out.

  • Where:
    • core/tables.py:551-558, Table.given: return next((value.strip() for value in self.params.getlist(name) if value.strip()), "").
    • templates/partials/table/narrow.html:15-33, the second copy, included inside the filter form at templates/jobs/company_list.html:89 (form #company-filters, :77-90, before the swapped {% partial table %} at :92), templates/jobs/listing_list.html:154 and templates/applications/partials/filters.html:45.
    • templates/cotton/table/filter.html:31-41: the header copy, hx-include="#{{ form_id }}", so every live request posts the whole form, Narrow block included.
    • The assumption is written down in three comments that are now wrong: cotton/table/filter.html:16-18, partials/table/narrow.html:7-9, cotton/table/head.html:55-56.
  • What happens:
    1. Open Companies at /jobs/companies/?name=acme (a saved view, a bookmark, a reload after filtering, or Back, which reloads from the server since #226). Both copies are rendered with acme; in the form's elements the Narrow copy (filter-name-narrow) comes before the header copy (filter-name).
    2. Clear the Name box in the header. htmx sends ?q=&name=acme&…&name=&…: the Narrow copy first. given("name") answers acme, and the table still shows only Acme while the box says nothing.
    3. Type glo in the same box. The request is ?…&name=acme&…&name=glo&…, and the table still shows Acme.
    4. The pushed address carries every filter twice, stale copy first, so reloading it, bookmarking it or keeping it as a view keeps the stale filter too.
      The same happens with scripts off (the Apply button posts both copies in the same order), and the other way round on a phone: a field changed in Narrow is overridden by the header copy if that one was filled when the page loaded. Companies, Listings and Applications are all affected.
  • Why it matters: the table shows a different filter from the one on screen, and the person cannot change a filter without first throwing all of them away with Clear.
  • Fix: post one copy per filter. The simplest is to stop drawing the controls twice: since #253 the header disclosures are usable at every width (cotton/table/head.html:47-56), so narrow.html can become a list of the filterable columns that opens the matching header disclosure, rather than a second set of inputs. If two sets of inputs must stay, they cannot share a name: give the Narrow copies their own names (for example narrow-<name>), read them only when the request came from the Narrow block's own Apply button, and read the header names otherwise. Either way, correct the three comments above. #313 and #314 rework these forms; this should land with them or before them.

Done when

  • A test in tests/test_tables.py renders Companies, Listings and Applications with a filter in the address and asserts that each filter name is posted by one control only.
  • A browser test (beside tests/e2e/test_saved_views.py) loads /jobs/companies/?name=…, clears the Name header box and sees the whole list, then types another name and sees that company.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the table filter templates and `Table.given`, then driving *Companies* in headless Chromium with every request answered in-process by the test client. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. Every column filter is drawn twice and both copies are posted under the same name: once in the column header and once in the phone block *Narrow*. `Table.given` takes the first non-empty value, on the assumption that "the copy nobody typed in narrows nothing". That only holds while the untouched copy is empty. The *Narrow* block sits inside the filter form, outside the region a live filter swaps, so it keeps the value the page was rendered with, and it comes first in the form's element order. So once a page has been loaded with a filter in its address, the header box for that filter no longer does anything: clearing it or typing something else leaves the table filtered by the old value. Only *Clear* gets out. - **Where:** - `core/tables.py:551-558`, `Table.given`: `return next((value.strip() for value in self.params.getlist(name) if value.strip()), "")`. - `templates/partials/table/narrow.html:15-33`, the second copy, included inside the filter form at `templates/jobs/company_list.html:89` (form `#company-filters`, `:77-90`, before the swapped `{% partial table %}` at `:92`), `templates/jobs/listing_list.html:154` and `templates/applications/partials/filters.html:45`. - `templates/cotton/table/filter.html:31-41`: the header copy, `hx-include="#{{ form_id }}"`, so every live request posts the whole form, *Narrow* block included. - The assumption is written down in three comments that are now wrong: `cotton/table/filter.html:16-18`, `partials/table/narrow.html:7-9`, `cotton/table/head.html:55-56`. - **What happens:** 1. Open *Companies* at `/jobs/companies/?name=acme` (a saved view, a bookmark, a reload after filtering, or Back, which reloads from the server since #226). Both copies are rendered with `acme`; in the form's elements the *Narrow* copy (`filter-name-narrow`) comes before the header copy (`filter-name`). 2. Clear the *Name* box in the header. htmx sends `?q=&name=acme&…&name=&…`: the *Narrow* copy first. `given("name")` answers `acme`, and the table still shows only Acme while the box says nothing. 3. Type `glo` in the same box. The request is `?…&name=acme&…&name=glo&…`, and the table still shows Acme. 4. The pushed address carries every filter twice, stale copy first, so reloading it, bookmarking it or keeping it as a view keeps the stale filter too. The same happens with scripts off (the *Apply* button posts both copies in the same order), and the other way round on a phone: a field changed in *Narrow* is overridden by the header copy if that one was filled when the page loaded. *Companies*, *Listings* and *Applications* are all affected. - **Why it matters:** the table shows a different filter from the one on screen, and the person cannot change a filter without first throwing all of them away with *Clear*. - **Fix:** post one copy per filter. The simplest is to stop drawing the controls twice: since #253 the header disclosures are usable at every width (`cotton/table/head.html:47-56`), so `narrow.html` can become a list of the filterable columns that opens the matching header disclosure, rather than a second set of inputs. If two sets of inputs must stay, they cannot share a name: give the *Narrow* copies their own names (for example `narrow-<name>`), read them only when the request came from the *Narrow* block's own *Apply* button, and read the header names otherwise. Either way, correct the three comments above. #313 and #314 rework these forms; this should land with them or before them. ## Done when - A test in `tests/test_tables.py` renders *Companies*, *Listings* and *Applications* with a filter in the address and asserts that each filter name is posted by one control only. - A browser test (beside `tests/e2e/test_saved_views.py`) loads `/jobs/companies/?name=…`, clears the *Name* header box and sees the whole list, then types another name and sees that company.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:48:27 +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#622
No description provided.