From ba9b92d41923ec9407136088c7802eadc9e30a46 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 20:08:50 +0000 Subject: [PATCH] Align set-criterion modifiers with Stash (any/all/none) and harmonize EXCLUDES Closes #10. Backend (common/criteria.py): - Treat `excludes` as an always-orthogonal AND'd negative across both MultiCriterion and ChoiceCriterion; the modifier now governs only the `value` (include) set. This removes the prior divergence where MultiCriterion.EXCLUDES dropped the excludes list and ChoiceCriterion.EXCLUDES swapped include/exclude into a positive. - Fold INCLUDES / INCLUDES_ALL / EXCLUDES (+ EQUALS/NOT_EQUALS aliases) into the shared _SetCriterion base so the two subclasses cannot drift; remove _extra_q. M2M "has all" (games/filters.py): - PurchaseFilter._games_to_q builds a pk__in subquery with one join per value so INCLUDES_ALL on the many-to-many games field works in a single .filter() (a naive Q(games=a) & Q(games=b) collapses to one join and matches nothing). UI (FilterSelect + filter_bar.js): - Add an optional any/all/none match-mode (any/all/none → INCLUDES/INCLUDES_ALL/EXCLUDES). A +# native control so its value *is* its state — no class toggling in the JS. +# shrink-0 keeps it from collapsing as pills wrap; it sits before the pills. +_FILTER_MATCH_SELECT_CLASS = ( + "shrink-0 rounded border border-default-medium bg-neutral-secondary-medium " + "text-xs text-body px-2 py-0.5 cursor-pointer " + "focus:ring-brand focus:border-brand" +) def _normalize_option(option) -> SearchSelectOption: @@ -159,6 +167,7 @@ def _combobox_shell( always_visible: bool, items_visible: int, templates: list[SafeText] | None = None, + leading: SafeText | None = None, ) -> SafeText: """Assemble the shared, domain-agnostic combobox skeleton. @@ -169,8 +178,9 @@ def _combobox_shell( ``options_children`` (value rows plus any pinned pseudo-options), the ``container_attributes`` that carry the widget's identity and behaviour flags, and any ``templates`` (inert ``