Implements the DateRangePicker design: a DateRangeField that looks like a
single input but splits each date into DD/MM/YYYY part inputs (ordered by
the new common.time.dateformat_hyphenated), and a DateRangeCalendar popup
with a preset column (today, yesterday, last 7/30 days, this/last month,
this year), anchor-style range picking with an outlined/filled/muted range
track, and a Cancel / Clear / Select footer.
Typing fills each part's placeholder from the right (YYYY -> YY19 -> 1987),
auto-advances between parts, and Backspace/Delete reverts the active part.
The committed value lives in hidden ISO {prefix}-min/{prefix}-max inputs --
the same contract as DateRangeFilter, so filter_bar.js needs no changes.
As a tryout, the Purchased filter in PurchaseFilterBar now uses the
DateRangePicker; Refunded keeps the native-date DateRangeFilter, and the
native-path e2e tests were repointed at it.
Includes unit tests for the component family and the filter-bar
integration, plus Playwright e2e tests for segment entry, calendar
picking, presets, and footer actions.
https://claude.ai/code/session_017b75KJAu4kNNpZPu9NAPBM
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 <select> (INCLUDES/INCLUDES_ALL/
EXCLUDES) rendered before the pills via a new `leading` slot on the shared
combobox shell. A native control so its value is its state. readSearchSelect
serialises it to data-match; filter_bar folds it into the criterion modifier.
Orthogonal to the (Any)/(None) presence pseudo-options and the exclude channel.
- Enable it for the M2M Purchase.games field (INCLUDES_ALL is only meaningful
for multi-valued relations). Styled with already-compiled utilities.
Tests: harmonized EXCLUDES + INCLUDES_ALL for both criterion types, a DB-backed
INCLUDES_ALL vs INCLUDES contrast on Purchase.games, and FilterSelect /
PurchaseFilterBar rendering + round-trip of the match mode.
https://claude.ai/code/session_01KwVrGFbq13mZdhDL9G6zhg
The JS always emits {id, label} objects now; the else branch was dead code
and the docstring was wrong. Update the remaining test that was still
passing bare strings.
https://claude.ai/code/session_01EyAJcMoDktLrY9tSbdHViA
- Move {id,label} stripping into _SetCriterion.from_json() so both
MultiCriterion and ChoiceCriterion normalise at the parse boundary;
the querying layer stays typed (list[int] / list[str]) and clean.
- Revert MultiCriterion to a thin _extra_q() override; _SetCriterion.to_q()
is no longer duplicated.
- JS: readSearchSelect always emits {id, label} objects — no conditional
mixed-type arrays. filter_bar.js stores them as-is for all fields,
removing the fragile isIdField hardcoded list.
- Update tests to use the {id, label} filter format.
https://claude.ai/code/session_01EyAJcMoDktLrY9tSbdHViA
MultiCriterion.to_q (used by SessionFilter for game/device) unconditionally added
field__in=value even when value was empty, and __in=[] matches no rows — so a
filter with only excludes (e.g. device excludes 11, no game/device includes)
returned zero results. Guard the empty value like ChoiceCriterion already does,
so an exclude-only criterion means 'all rows except the excluded ids'.
https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
Add a Template() primitive for the standard <template> tag and export it. Replace
inline Component(tag_name="div"/"span"/"input"/"template") in search_select.py
and Pill with Div/Span/Input/Template; drop the private _template helper in favour
of Template at the call sites. Bare custom-styled <button>s stay on Component
(the opinionated Button() would inject unwanted classes). Document the
prefer-primitives convention in CLAUDE.md.
https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
Spell out the abbreviated data-ss-* hook attributes (data-search-select-option,
-label, -mode, -template, -action, -type, -modifier, -modifier-option, -pills,
-search, -options, -no-results) and the JS expando properties (_searchSelectInit,
_searchSelectLabel, _searchSelectDirty, _searchSelectOption) across components,
JS, and tests — no abbreviations left in the widget's hooks.
https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
Eliminate the Python/JS class-string duplication: the server renders hidden
<template> prototypes (row, pill, include/exclude/modifier pills) using the same
component functions, and search_select.js clones them, filling only the
[data-ss-label] slot, value, and data-* attrs. All Tailwind class strings and DOM
structure now live solely in the Python components — the JS no longer hardcodes
any class. Pill gains an opt-in label_slot; the shell takes a templates list.
Companion issue #8 tracks the further HTMX-idiomatic step of returning rendered
row HTML from the search endpoint.
https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
Replace the bespoke SelectableFilter in all three bars with FilterSelect: enum
fields (status, type, ownership) pre-render their fixed options; model-backed
fields (game(s), platform, device) use the search endpoints with prefetch and
resolve only the selected ids to pill labels — dropping the per-page queries that
fetched every game/platform/device. filter_bar.js now reads filter-mode
SearchSelect widgets via readSearchSelect (data-included/excluded/modifier),
preserving the {value, excludes, modifier} JSON and id Number() coercion; the
redundant session game/device blocks are gone. Drop FilterBar's now-unused
platform_options param. Rebuild base.css for the inline filter-pill utilities and
update the bar tests to the new markup.
https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
FilterSelect renders value rows with +/- (include/exclude) buttons, check/cross
pills for the included/excluded sets, and an optional set of pinned modifier
pseudo-options (e.g. (Any)/(None)) that stay visible above the value rows. A
selected modifier is mutually exclusive with value pills. It delegates assembly
to _combobox_shell and supports both pre-rendered options (complete set) and
search_url + prefetch (windowed); included/excluded are passed as resolved
value+label so pills show labels even outside the fetched window. Styling is
inline (ported from the old SelectableFilter CSS) so nothing lives in input.css.
https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
Pull the domain-agnostic combobox skeleton (pills region, search box, options
panel with its no-results node, outer container) into a private _combobox_shell
helper. SearchSelect now builds its form-specific pills and option rows and
delegates assembly to the shell. Rendered markup is byte-identical; a structural
test guards the fixed region order so future builders (e.g. a filter variant)
can share the shell without drift.
https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
The `NameWithIcon()` function had a `platform` parameter that was immediately overwritten by `platform = None` and never used (dead code). The function mixed data lookup (database queries via IDs) with rendering, making it untestable.
**Fix**: Refactored `NameWithIcon()` to follow the `LinkedPurchase` pattern — accepts model objects (`Game`, `Session`) instead of IDs. Extracted `_resolve_name_with_icon()` helper for testable computation logic (name resolution, platform extraction, link creation). Fixed bug where `platform` was not extracted when `session` parameter was passed. Removed dead `platform` parameter from the public API. Updated all 3 production call sites (already using model objects). Added 10 unit tests for `_resolve_name_with_icon()` covering session override, custom names, linkify behavior, platform resolution, and edge cases. Updated 6 integration tests to use model-based parameters.