feat(filter-builder): the group header is the ALL/ANY switch
The group header ("MATCH"/"GROUP") was a dead label while the engine
honored group modes all along — buildGroupClause parenthesizes nested
groups and joins each by its own mode, and validation accepted
all/any from day one. The only thing missing was the UI: + Group
birthed ALL groups and no control anywhere could flip one, so the
user's target structures (GROUP(GROUP(... or ...) ...) — 3-level
mixed-mode nesting) were constructible only through the invisible
row-level chips, and standalone ANY-groups not at all (2026-11-01
user report: "MATCH cannot be clicked").
- The header is now the control: MATCH ALL ⇄ MATCH ANY (root — a
synthetic header row, since the root was never projected and its
mode was invisible too), GROUP ALL ⇄ GROUP ANY (nested). ANY is
accented; aria-pressed carries the state. Labels are path-based
(root reads Match, explicit groups read Group) instead of
depth-based, which previously mislabeled top-level groups.
- The row-level chips are untouched — they mix AND/OR inside a group
(A AND (B OR C)), which a global mode toggle cannot express; the
header is the discoverable entry point, the chips the refinement.
- Dropdown audit (the "would it bug out" check): suggestTree is
mode-generic — root-ANY drops group siblings, sibling ANY-groups
survive into the constraint with their mode intact, empty groups
never emit (no 400s) — now pinned. Deep-nesting Go pin: a 3-level
mixed-mode tree evaluated through composed search + the suggestion
endpoint (200, never the empty-group 400). 192 web tests green.
This commit is contained in:
1 parent
5fb80f82ed
commit
2344f5686a
7 files changed
+254
-5
No files matched your search
@@ -11,11 +11,20 @@ templ FilterBuilderRows(countNoun string) {
|
||||
<div>
|
||||
<template x-if="row.type === 'groupOpen'">
|
||||
<div :class="row.depth > 0 ? 'fb-nested' : ''" class="flex items-center gap-1.5 flex-wrap py-1" :style="'margin-left:' + (row.depth * 14) + 'px'">
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wide" style="color: var(--text-secondary)" x-text="row.depth === 0 ? 'Match' : 'Group'"></span>
|
||||
<!-- THE group mode control (2026-11-01): the header was a dead label
|
||||
while the engine honored group modes all along — clicking it now
|
||||
flips ALL ⇄ ANY for that group. Root (path=[]) is the whole-query
|
||||
switch; the remove × hides there (nothing to remove). -->
|
||||
<button type="button" class="fb-group-mode" :class="groupMode(row.path) === 'any' ? 'fb-group-any' : ''" :aria-pressed="groupMode(row.path) === 'any'" :title="groupMode(row.path) === 'any' ? 'ANY — at least one rule in this group matches. Click for ALL.' : 'ALL — every rule in this group must match. Click for ANY.'" @click="toggleGroupMode(row.path)">
|
||||
<span x-text="row.path.length === 0 ? 'Match' : 'Group'"></span>
|
||||
<span x-text="groupMode(row.path) === 'any' ? 'ANY' : 'ALL'"></span>
|
||||
</button>
|
||||
<span class="flex-1"></span>
|
||||
<template x-if="row.path.length > 0">
|
||||
<button type="button" class="fb-mini-btn fb-danger" @click="removeNode(row.path)" aria-label="Remove group">×</button>
|
||||
</template>
|
||||
<button type="button" class="fb-mini-btn" @click="addRule(row.path)">+ rule</button>
|
||||
<button type="button" class="fb-mini-btn" @click="addGroup(row.path)">+ group</button>
|
||||
<button type="button" class="fb-mini-btn fb-danger" @click="removeNode(row.path)" aria-label="Remove group">×</button>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="row.type === 'rule'">
|
||||
|
||||
Reference in new issue
Block a user