diff --git a/suite/apps/tableaux/utils/guestFilters.test.js b/suite/apps/tableaux/utils/guestFilters.test.js index e3ea9da8..574b8be4 100644 --- a/suite/apps/tableaux/utils/guestFilters.test.js +++ b/suite/apps/tableaux/utils/guestFilters.test.js @@ -9,3 +9,31 @@ describe('the Unassigned chip', () => { expect(matchesFilters(declined, ['unassigned'])).toBe(false) }) }) + +// Chips in one category widen the list; chips in different categories narrow +// it. A guest has one diet and one side, so ANDing two chips from the same +// category could only ever show nobody, or only the guests on both sides. +describe('combining chips', () => { + const veggie = { dietary: 'vegetarian', side: 'a', assignedTableId: null, rsvpStatus: 'confirmed' } + const vegan = { dietary: 'vegan', side: 'b', assignedTableId: 't1', rsvpStatus: 'confirmed' } + const neither = { dietary: '', side: 'both', assignedTableId: null, rsvpStatus: 'confirmed' } + + it('shows vegetarians and vegans when both diet chips are on', () => { + expect(matchesFilters(veggie, ['vegetarian', 'vegan'])).toBe(true) + expect(matchesFilters(vegan, ['vegetarian', 'vegan'])).toBe(true) + expect(matchesFilters(neither, ['vegetarian', 'vegan'])).toBe(false) + }) + + it("shows everyone from either side when both partners' chips are on", () => { + expect(matchesFilters(veggie, ['a', 'b'])).toBe(true) + expect(matchesFilters(vegan, ['a', 'b'])).toBe(true) + expect(matchesFilters(neither, ['a', 'b'])).toBe(true) + }) + + it('still narrows across categories', () => { + expect(matchesFilters(veggie, ['vegan', 'a'])).toBe(false) + expect(matchesFilters(vegan, ['vegan', 'b'])).toBe(true) + expect(matchesFilters(vegan, ['vegetarian', 'vegan', 'unassigned'])).toBe(false) + expect(matchesFilters(veggie, ['vegetarian', 'vegan', 'unassigned'])).toBe(true) + }) +}) diff --git a/suite/apps/tableaux/utils/guestFilters.ts b/suite/apps/tableaux/utils/guestFilters.ts index fc67d4dd..4d3f8f74 100644 --- a/suite/apps/tableaux/utils/guestFilters.ts +++ b/suite/apps/tableaux/utils/guestFilters.ts @@ -37,15 +37,32 @@ const PREDICATES: Record boolean> = { notes: (g) => !!(g.notes && g.notes.trim()), } -// TODO(ux-audit): matchesFilters ANDs every active filter (below), which -// produces silent, misleading results for two real combos: Vegetarian+Vegan -// ticked together is a guaranteed-empty result (dietary is a single value, -// can never match both); and both side chips together only matches -// side==='both', not the union a user would expect from ticking two side -// chips. See tmp/ux-audit.md #G7. +/** + * Chips in one category widen the list, and chips in different categories + * narrow it: Vegetarian + Vegan is everyone on either diet, and Vegan + Alex's + * is Alex's vegans. A guest has one diet and one side, so ANDing two chips of + * the same category could only ever show nobody, or just the guests on both + * sides. + */ +const CATEGORY: Record = { + unassigned: 'seat', + a: 'side', + b: 'side', + vegetarian: 'diet', + vegan: 'diet', + 'gluten-free': 'diet', + notes: 'notes', +} + export function matchesFilters(guest: Guest, filters: readonly string[] | null | undefined): boolean { if (!filters || filters.length === 0) return true - return filters.every((f) => (f in PREDICATES ? PREDICATES[f as FilterKey](guest) : true)) + const byCategory = new Map() + for (const f of filters) { + if (!(f in PREDICATES)) continue + const key = f as FilterKey + byCategory.set(CATEGORY[key], [...(byCategory.get(CATEGORY[key]) ?? []), key]) + } + return [...byCategory.values()].every((keys) => keys.some((key) => PREDICATES[key](guest))) } export function matchesSearch(guest: Guest, group: Pick | null | undefined, query: string): boolean {