Make component return types honest; drop str/mark_safe leftovers

Cleanup of hacky leftovers from the node-tree migration (no behaviour
change):

- Return annotations: the component builders return Node subtrees, not
  SafeText strings, but ~40 functions still declared `-> SafeText`. Correct
  them to `-> Node` across filters / search_select / date_range_picker /
  domain. The genuine string returners keep `-> SafeText`: the Alpine
  selectors (GameStatusSelector / SessionDeviceSelector, which build f-string
  markup) and the script-tag helpers (CsrfInput / ModuleScript /
  ExternalScript / StaticScript).
- layout.render_page / layout.Page / AddForm now accept `Node` in their
  `content` / `scripts` / `fields` parameters (TYPE_CHECKING import in
  layout to avoid the components import cycle), matching what views already
  pass.
- session._session_fields builds a `Fragment(*rows, separator="\n")` instead
  of `mark_safe("\n".join(str(row) ...))` — keeps the tree intact so media
  could bubble, per the Fragment convention.
- Inline SVG icon children use `Safe(...)` nodes instead of `mark_safe(...)`
  strings (filters mode-toggle + collapse icons, date_range_picker calendar
  icon).
- _filter_field reads the widget's own id from its node `.attributes`
  (`_widget_id`) for the label's `for`, dropping the superfluous `for_widget`
  argument that always rendered `for="None"`. Removes the two TODOs whose
  premise ("the Component function can't expose the id") the class/node
  refactor retired, plus RangeSlider's dead commented-out Label block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 15:12:52 +02:00
parent 1c5bff8651
commit 022d43a5a5
7 changed files with 86 additions and 80 deletions
+7 -7
View File
@@ -6,7 +6,7 @@ from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.safestring import SafeText, mark_safe
from common.components.core import HTMLTag
from common.components.core import HTMLTag, Node
from common.components.primitives import (
A,
Div,
@@ -22,7 +22,7 @@ def GameLink(
game_id: int,
name: str = "",
children: list[HTMLTag] | HTMLTag | None = None,
) -> SafeText:
) -> Node:
"""Link to a game's detail page. Uses children (slot) if provided, otherwise name."""
from django.urls import reverse
@@ -58,7 +58,7 @@ def GameStatus(
status: str = "u",
display: str = "",
class_: str = "",
) -> SafeText:
) -> Node:
"""Colored status dot with label. Status codes: u/p/f/a/r."""
children = children or []
outer_class = (
@@ -82,7 +82,7 @@ def GameStatus(
def PriceConverted(
children: list[HTMLTag] | HTMLTag | None = None,
) -> SafeText:
) -> Node:
"""Wrap content in a span that indicates the price was converted."""
children = children or []
return Span(
@@ -94,7 +94,7 @@ def PriceConverted(
)
def LinkedPurchase(purchase: Purchase) -> SafeText:
def LinkedPurchase(purchase: Purchase) -> Node:
link = reverse("games:view_purchase", args=[int(purchase.id)])
link_content = ""
popover_content = ""
@@ -145,7 +145,7 @@ def NameWithIcon(
session: Session | None = None,
linkify: bool = True,
emulated: bool = False,
) -> SafeText:
) -> Node:
_name, platform, final_emulated, create_link, link = _resolve_name_with_icon(
name, game, session, linkify
)
@@ -203,7 +203,7 @@ def _resolve_name_with_icon(
return _name, platform, final_emulated, create_link, link
def PurchasePrice(purchase) -> SafeText:
def PurchasePrice(purchase) -> Node:
return Popover(
popover_content=f"{floatformat(purchase.price)} {purchase.price_currency}",
wrapped_content=f"{floatformat(purchase.converted_price)} {purchase.converted_currency}",