fix(filters): preserve every value from <select multiple> filter strips

Symptom (m-reported): /calendar filters don't work.

Root cause: ParseTreeFilter and calendar's ?kind parser both used
`r.URL.Query().Get(key)` to read tag/mgmt/has/status/kind. `Get()`
returns ONLY the first value when a URL has the same key repeated, and
the HTMX filter-strip forms (calendar_section.tmpl, timeline_section,
dashboard_section, graph, bulk) all use `<select multiple name="tag">`
which the browser serialises as `?tag=foo&tag=bar` — repeated params,
not the comma-joined `?tag=foo,bar` the tree page emits from its hidden
input. Every second-and-beyond chip silently dropped on every filter
submission across every page with a multi-select strip; m happened to
catch it on /calendar.

Fix (single helper, four call-site swaps):

- web/server.go parseValues(q, key): collects q[key] (the full slice of
  values), joins on comma, runs parseCSV. Accepts both URL shapes:
    ?tag=foo,bar          → ["foo", "bar"]
    ?tag=foo&tag=bar      → ["foo", "bar"]
    ?tag=foo,bar&tag=baz  → ["foo", "bar", "baz"]

- web/tree_filter.go ParseTreeFilter: tag / mgmt / status / has all
  switch from `parseCSV(q.Get(...))` to `parseValues(q, ...)`. q / show-
  archived / public stay on `q.Get` — they're single-value by design.

- web/calendar.go parseCalendarQuery: ?kind handling drops the bespoke
  q.Get + strings.Split + dedup-map and uses `parseValues(..., "kind")`
  for the same reason. Behaviour preserved for legacy comma-joined
  `?kind=event,doc` AND new repeated-param submission.

Regression test:

- TestCalendarFilterMultiValueTagsFromForm seeds three items — one with
  both test tags (A+B), one with only A, one with only B — drops a
  dated link on each, then probes `/calendar?tag=A&tag=B`. Before the
  fix the A-only note leaked through (the parser kept just tag=A);
  after, only the A+B item appears per the AND-across-tags contract.

Full web suite green. Pre-existing db/TestBackfillTagsFromArea failure
unchanged (independent of this change).

Same fix transparently repairs /timeline, /dashboard, /graph, /bulk —
they all consume ParseTreeFilter and shared the bug.
This commit is contained in:
mAi
2026-05-26 11:56:42 +02:00
parent 69d872f7d2
commit 6f0a318979
4 changed files with 117 additions and 16 deletions

View File

@@ -10,6 +10,7 @@ import (
"log/slog"
"mime"
"net/http"
"net/url"
"sort"
"strings"
"time"
@@ -733,6 +734,23 @@ func parseTimelineExcludeList(raw []string) []string {
// parseCSV splits a comma/space-delimited chip input into a deduplicated,
// trimmed lowercase string slice. Empty input → []string{} (nil avoided so
// JSON/SQL writes get an explicit empty array).
// parseValues collects every value for `key` from a url.Values map and
// splits each on the same comma/whitespace separators parseCSV accepts.
// Handles both filter-strip styles:
// - `?tag=foo,bar` (tree page hidden-input chip pattern)
// - `?tag=foo&tag=bar` (HTMX multi-select form submission)
//
// Mixed shapes work too (`?tag=foo,bar&tag=baz` → [foo bar baz]).
// Without this, `q.Get(key)` returned only the first value, so the
// second tag/mgmt/has selection from any <select multiple> filter strip
// silently dropped.
func parseValues(q url.Values, key string) []string {
if vs, ok := q[key]; ok {
return parseCSV(strings.Join(vs, ","))
}
return []string{}
}
func parseCSV(raw string) []string {
if strings.TrimSpace(raw) == "" {
return []string{}