feat(filters): per-row AND/OR (Excel-style chips) + browse-on-click value menus (user report)

- Per-rule connectives: the chip before each rule flips that rule
  between AND and OR — OR-joined neighbors merge into an invisible
  OR-block, AND pulls them back out. The tree stays the single source
  of truth (implicit blocks are a client marker the server ignores),
  so deep links, saved filters, and the composed engine are untouched.
  Group-mode pills are gone; explicit groups remain as pure structure
  (+ group). Compile limit, same as Excel: flat chips express
  AND-of-OR-groups; a top-level OR of AND-blocks still needs nesting.
- Browse-on-click: clicking an empty value box now fetches the field's
  top values UNDER the other rules (blank typed = browse, not a
  similarity filter) — 'assign an author, click Publisher, see their
  publishers' without typing. Server: the composed suggestion engine
  trims the typed query; client: the >=2-char guard no longer blocks
  the empty query, browse limit 15 vs 8 while typing.
- Tests: the browse case pinned (blank sentinel + rules → the field's
  values under those rules, zero-match values absent).
This commit is contained in:
John O'Keefe committed 2026-10-20 15:25:57 -04:00
1 parent 6f98ca4c22
commit c747f0a76c
7 files changed
+138 -36

No files matched your search

+10
View File
@@ -516,6 +516,16 @@ func TestFilterSuggestionsUnderRules(t *testing.T) {
require.Equal(t, http.StatusOK, status)
assert.NotContains(t, valuesOf(results), "Amara Voss", "zero-match values never offered")
// BROWSE (the user's click-the-box case): a blank typed query lists
// the field's top values UNDER the other rules — "assign an author,
// click publisher, see their publishers" without typing anything.
// The wire sends a single space as the sentinel.
status, results = suggest("genre", " ", qfAll(qfRule("author", "contains", "Quill")))
require.Equal(t, http.StatusOK, status)
assert.Contains(t, valuesOf(results), "fantasy", "browse shows the field's values under the rules")
assert.NotContains(t, valuesOf(results), "spacewestern", "zero-match values never offered")
assert.NotContains(t, valuesOf(results), "history", "values outside the rules never offered")
// Library field menu: visibility-scoped, id values + name display
// (this test's fixture libraries are named "Test Builder Sug …").
status, results = suggest("library", "Test Builder", "")