fix(filter-builder): multi-pick id values matched nothing; hint the multi boxes
Comma-joined multi-picks matched ZERO: clauseFeed/clauseLibrary/ clauseCollection parsed each rule value as ONE uuid, but the builder's pick loop stores several picks as a single comma-joined value (["id1,id2"]) — uuid.Parse failed on the joined string, the id list came back empty, and "feeds in [A, B]" matched no episodes (one pick worked, which is why the briefing recipe survived). parseUUIDValues now splits every value on commas (trimming) before parsing, accepting both the builder's joined shape and hand-written separate elements — same leniency the suggest-side id lookup already has. The multi-value cells now say so: id-kind value inputs get the placeholder "pick one or more" (the only boxes on the page that accept several values; every other cell replaces its value). Pins: comma_joined_multi_feed_matches_both — a tree collection built with the exact comma-joined wire the client sends, asserting BOTH feeds' episodes match. Filter-builder vitest 35/35, container filter + episode-tree slices green.
This commit is contained in:
1 parent
09f8f0875f
commit
cce488b76f
6 files changed
+78
-22
No files matched your search
@@ -379,6 +379,24 @@ func clauseFileAvailability(r FilterRule, b *filterBuilder) string {
|
||||
}
|
||||
|
||||
// clauseAddedBy — the admin who added the book.
|
||||
// parseUUIDValues — the wire shape for id-valued rules: the builder's
|
||||
// pick loop stores picked ids as ONE comma-joined value
|
||||
// (["id1,id2"]), while hand-written trees may carry separate elements.
|
||||
// Both are legal; split on commas, trim, and keep what parses. An
|
||||
// unsplit parse matched NOTHING for multi-picks (2026-11-01 user
|
||||
// report: two feeds in "feeds in" matched zero episodes).
|
||||
func parseUUIDValues(values []string) []uuid.UUID {
|
||||
ids := make([]uuid.UUID, 0, len(values))
|
||||
for _, v := range values {
|
||||
for _, part := range strings.Split(v, ",") {
|
||||
if id, err := uuid.Parse(strings.TrimSpace(part)); err == nil {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func clauseAddedBy(r FilterRule, b *filterBuilder) string {
|
||||
switch r.Op {
|
||||
case OpIs:
|
||||
@@ -610,12 +628,7 @@ func clauseCollection(r FilterRule, b *filterBuilder) string {
|
||||
}
|
||||
return "(NOT " + ex + ")"
|
||||
}
|
||||
ids := make([]uuid.UUID, 0, len(r.Values))
|
||||
for _, v := range r.Values {
|
||||
if id, err := uuid.Parse(v); err == nil {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
ids := parseUUIDValues(r.Values)
|
||||
ex := fmt.Sprintf(`(EXISTS (
|
||||
SELECT 1 FROM collection_items ci
|
||||
JOIN collections c ON c.id = ci.collection_id
|
||||
@@ -633,12 +646,7 @@ func clauseCollection(r FilterRule, b *filterBuilder) string {
|
||||
// library IDs; visibility is enforced by the mandatory head regardless
|
||||
// (a rule naming a hidden library simply matches nothing).
|
||||
func clauseLibrary(r FilterRule, b *filterBuilder) string {
|
||||
ids := make([]uuid.UUID, 0, len(r.Values))
|
||||
for _, v := range r.Values {
|
||||
if id, err := uuid.Parse(v); err == nil {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
ids := parseUUIDValues(r.Values)
|
||||
if r.Op == OpIs {
|
||||
return "(l.id = ANY(" + b.bind(ids) + "::uuid[]))"
|
||||
}
|
||||
@@ -653,12 +661,7 @@ func clauseLibrary(r FilterRule, b *filterBuilder) string {
|
||||
// naming an unsubscribed feed matches nothing).
|
||||
func clauseFeed(r FilterRule, b *filterBuilder) string {
|
||||
b.needJoin("pf", "LEFT JOIN podcast_feeds pf ON pf.id = mi.source_feed_id")
|
||||
ids := make([]uuid.UUID, 0, len(r.Values))
|
||||
for _, v := range r.Values {
|
||||
if id, err := uuid.Parse(v); err == nil {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
ids := parseUUIDValues(r.Values)
|
||||
in := "(pf.id = ANY(" + b.bind(ids) + "::uuid[]))"
|
||||
switch r.Op {
|
||||
case OpIs, OpIn:
|
||||
|
||||
Reference in new issue
Block a user