Introduce LabeledOption and RangeValues named types
Replace all tuple[str, str] annotations with purpose-specific names: - LabeledOption = tuple[str, str] for (value, label) pairs used in FilterChoice, FilterSelect params, _modifier_options, _find_label, and _extract_labeled. - RangeValues(min, max) NamedTuple for _parse_range return values, making the two fields self-documenting at every call site. Export LabeledOption from common.components alongside SearchSelectOption. Document the "name compound types explicitly" convention in CLAUDE.md. https://claude.ai/code/session_01EyAJcMoDktLrY9tSbdHViA
This commit is contained in:
@@ -42,6 +42,7 @@ from common.components.primitives import (
|
||||
)
|
||||
from common.components.search_select import (
|
||||
FilterSelect,
|
||||
LabeledOption,
|
||||
SearchSelect,
|
||||
SearchSelectOption,
|
||||
searchselect_selected,
|
||||
@@ -87,6 +88,7 @@ __all__ = [
|
||||
"PopoverTruncated",
|
||||
"SearchField",
|
||||
"FilterSelect",
|
||||
"LabeledOption",
|
||||
"SearchSelect",
|
||||
"SearchSelectOption",
|
||||
"searchselect_selected",
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.utils.safestring import SafeText, mark_safe
|
||||
|
||||
from common.components.core import Component
|
||||
from common.components.primitives import Label, Span
|
||||
from common.components.search_select import FilterSelect
|
||||
from common.components.search_select import FilterSelect, LabeledOption
|
||||
|
||||
|
||||
class FilterChoice(NamedTuple):
|
||||
@@ -19,11 +19,18 @@ class FilterChoice(NamedTuple):
|
||||
for enum fields the label is resolved from the fixed option list.
|
||||
"""
|
||||
|
||||
selected: list[tuple[str, str]]
|
||||
excluded: list[tuple[str, str]]
|
||||
selected: list[LabeledOption]
|
||||
excluded: list[LabeledOption]
|
||||
modifier: str
|
||||
|
||||
|
||||
class RangeValues(NamedTuple):
|
||||
"""A (min, max) string pair parsed from a range filter criterion."""
|
||||
|
||||
min: str
|
||||
max: str
|
||||
|
||||
|
||||
_FILTER_LABEL_CLASS = "text-xs font-medium text-body uppercase tracking-wide"
|
||||
|
||||
|
||||
@@ -55,7 +62,7 @@ def _filter_parse(filter_json: str) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def _extract_labeled(items: list) -> list[tuple[str, str]]:
|
||||
def _extract_labeled(items: list) -> list[LabeledOption]:
|
||||
"""Convert a list of bare values or ``{id, label}`` dicts to ``(value, label)`` pairs."""
|
||||
result = []
|
||||
for item in items:
|
||||
@@ -84,12 +91,12 @@ def _filter_get_choice(existing: dict, field: str) -> FilterChoice:
|
||||
)
|
||||
|
||||
|
||||
def _parse_range(existing: dict, key: str) -> tuple[str, str]:
|
||||
"""Extract (value, value2) from a filter criterion, defaulting to ("", "")."""
|
||||
def _parse_range(existing: dict, key: str) -> RangeValues:
|
||||
"""Extract (min, max) from a range filter criterion, defaulting to ("", "")."""
|
||||
field = existing.get(key, {})
|
||||
if not isinstance(field, dict):
|
||||
return "", ""
|
||||
return str(field.get("value", "")), str(field.get("value2", ""))
|
||||
return RangeValues("", "")
|
||||
return RangeValues(str(field.get("value", "")), str(field.get("value2", "")))
|
||||
|
||||
|
||||
def _parse_bool(existing: dict, key: str) -> bool:
|
||||
@@ -108,7 +115,7 @@ def _parse_bool(existing: dict, key: str) -> bool:
|
||||
_FILTER_PREFETCH = 20
|
||||
|
||||
|
||||
def _modifier_options(nullable: bool) -> list[tuple[str, str]]:
|
||||
def _modifier_options(nullable: bool) -> list[LabeledOption]:
|
||||
"""Pinned (Any)/(None) pseudo-options; (None) only when the field is nullable."""
|
||||
options = [("NOT_NULL", "(Any)")]
|
||||
if nullable:
|
||||
@@ -618,7 +625,7 @@ def _filter_bar(fields, filter_json, preset_list_url, preset_save_url) -> SafeTe
|
||||
|
||||
def FilterBar(
|
||||
filter_json: str = "",
|
||||
status_options: list[tuple[str, str]] | None = None,
|
||||
status_options: list[LabeledOption] | None = None,
|
||||
preset_list_url: str = "",
|
||||
preset_save_url: str = "",
|
||||
) -> SafeText:
|
||||
@@ -717,7 +724,7 @@ def FilterBar(
|
||||
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
|
||||
|
||||
|
||||
def _find_label(options: list[tuple[str, str]], value: str) -> str:
|
||||
def _find_label(options: list[LabeledOption], value: str) -> str:
|
||||
for v, label in options:
|
||||
if str(v) == str(value):
|
||||
return label
|
||||
|
||||
@@ -33,6 +33,12 @@ class SearchSelectOption(TypedDict):
|
||||
data: dict[str, str] # becomes data-* attrs on the row / pill
|
||||
|
||||
|
||||
# A lightweight (value, label) pair used wherever only those two fields are
|
||||
# needed — e.g. filter pill lists and modifier pseudo-options. The richer
|
||||
# SearchSelectOption adds a ``data`` dict for extra row attributes.
|
||||
LabeledOption = tuple[str, str]
|
||||
|
||||
|
||||
# The pills and the search box share one flex-wrap row (with padding) so the
|
||||
# widget reads as a single clickable field; the pills wrapper uses `contents`
|
||||
# so its pills/hidden inputs flow as direct participants of that row, inline
|
||||
@@ -394,11 +400,11 @@ def _filter_modifier_row(modifier_value: str, label: str) -> SafeText:
|
||||
def FilterSelect(
|
||||
*,
|
||||
field_name: str,
|
||||
options: list | None = None,
|
||||
included: list | None = None,
|
||||
excluded: list | None = None,
|
||||
options: list[LabeledOption | SearchSelectOption] | None = None,
|
||||
included: list[LabeledOption | SearchSelectOption] | None = None,
|
||||
excluded: list[LabeledOption | SearchSelectOption] | None = None,
|
||||
modifier: str = "",
|
||||
modifier_options: list[tuple[str, str]] | None = None,
|
||||
modifier_options: list[LabeledOption] | None = None,
|
||||
search_url: str = "",
|
||||
prefetch: int = 0,
|
||||
items_visible: int = 6,
|
||||
|
||||
Reference in New Issue
Block a user