Migrate filter bars to FilterSelect

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
This commit is contained in:
Claude
2026-06-07 22:20:44 +00:00
committed by Lukáš Kucharczyk
parent a6532807cb
commit 1a206d719b
5 changed files with 198 additions and 139 deletions
+3 -22
View File
@@ -15,7 +15,6 @@ from django.test import TestCase
from common.components import (
FilterBar,
PurchaseFilterBar,
SelectableFilter,
SessionFilterBar,
)
from games.models import Device, Game, Platform
@@ -94,14 +93,15 @@ class FilterBarRenderingTest(TestCase):
self._assert_range_slider(html)
def test_game_filter_bar_roundtrips_selected_status(self):
"""A status in filter_json renders as a selected tag in the widget."""
"""A status in filter_json renders as an include pill in the widget."""
filter_json = json.dumps({"status": {"value": ["f"], "modifier": ""}})
html = str(
FilterBar(
filter_json=filter_json, preset_list_url="/l", preset_save_url="/s"
)
)
self.assertIn("sf-tag", html)
self.assertIn('data-ss-mode="filter"', html)
self.assertIn('data-ss-type="include"', html) # rendered as an include pill
self.assertIn('data-value="f"', html) # selected status reflected in widget
self.assertIn("Finished", html) # ...with its label
self.assertNoEscapedTags(html)
@@ -110,22 +110,3 @@ class FilterBarRenderingTest(TestCase):
# for the double-escape bug the dedup fixed.
self.assertIn(""status"", html)
self.assertNotIn(""", html)
class SelectableFilterTest(TestCase):
"""The shared widget the deduped FilterBar will be built on."""
OPTIONS = [("f", "Finished"), ("a", "Abandoned"), ("u", "Unplayed")]
def test_plain_widget_has_no_tags(self):
html = str(SelectableFilter("status", self.OPTIONS))
self.assertNotIn("sf-tag", html)
def test_include_and_exclude_tags(self):
html = str(
SelectableFilter("status", self.OPTIONS, selected=["f"], excluded=["a"])
)
self.assertIn('data-type="include"', html)
self.assertIn('data-type="exclude"', html)
self.assertIn("Finished", html)
self.assertIn("Abandoned", html)
+16 -17
View File
@@ -235,20 +235,20 @@ class TestGameFilterToQ:
class TestFilterBarRendering:
"""Tests for FilterBar with SelectableFilter widgets."""
"""Tests for FilterBar with FilterSelect widgets."""
def test_status_uses_selectable_filter(self):
html = str(FilterBar(platform_options=[]))
assert "data-selectable-filter" in html
def test_status_uses_filter_select(self):
html = str(FilterBar())
assert 'data-ss-mode="filter"' in html
assert 'data-name="status"' in html
def test_mastered_not_checked_by_default(self):
html = str(FilterBar(filter_json="", platform_options=[]))
html = str(FilterBar(filter_json=""))
assert 'checked="true"' not in html
def test_mastered_checked_when_filtered(self):
html = str(
FilterBar(
platform_options=[],
filter_json=json.dumps(
{"mastered": {"value": True, "modifier": "EQUALS"}}
),
@@ -259,7 +259,6 @@ class TestFilterBarRendering:
def test_status_prefilled(self):
html = str(
FilterBar(
platform_options=[],
filter_json=json.dumps(
{"status": {"value": ["f"], "modifier": "INCLUDES"}}
),
@@ -269,19 +268,19 @@ class TestFilterBarRendering:
assert "Finished" in html
def test_no_hx_get(self):
html = str(FilterBar(platform_options=[]))
html = str(FilterBar())
assert "hx-get" not in html
def test_platform_options_rendered(self):
html = str(FilterBar(platform_options=[(1, "Steam"), (2, "Switch")]))
assert "Steam" in html
assert "Switch" in html
def test_platform_uses_search_url(self):
"""Platform is model-backed: rows are fetched, not pre-rendered."""
html = str(FilterBar())
assert 'data-search-url="/api/platforms/search"' in html
def test_status_has_no_modifiers(self):
"""Non-nullable fields should not show (None) but MUST show (Any)."""
html = str(FilterBar(platform_options=[]))
status_start = html.find('data-selectable-filter="status"')
platform_start = html.find('data-selectable-filter="platform"')
html = str(FilterBar())
status_start = html.find('data-name="status"')
platform_start = html.find('data-name="platform"')
status_section = html[status_start:platform_start]
# Must have (Any) — always available
assert "(Any)" in status_section
@@ -290,8 +289,8 @@ class TestFilterBarRendering:
def test_platform_has_modifiers(self):
"""Nullable ForeignKey fields should show (Any)/(None)."""
html = str(FilterBar(platform_options=[(1, "Steam")]))
platform_start = html.find('data-selectable-filter="platform"')
html = str(FilterBar())
platform_start = html.find('data-name="platform"')
platform_section = html[platform_start:]
# Should have at least one modifier option
assert "(Any)" in platform_section or "(None)" in platform_section