Move events/market/blog composition from Python to .sx defcomps (Phase 9)
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m33s

Continues the pattern of eliminating Python sx_call tree-building in favour
of data-driven .sx defcomps. POST/PUT/DELETE routes now pass plain data
(dicts, lists, scalars) and let .sx handle iteration, conditionals, and
layout via map/let/when/if. Single response components wrap OOB swaps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 08:17:09 +00:00
parent 877e776977
commit 51ebf347ba
23 changed files with 1841 additions and 1423 deletions

View File

@@ -43,3 +43,17 @@
(defcomp ~market-cart-add-oob (&key id content inner)
(div :id id :sx-swap-oob "outerHTML"
(if content content (when inner inner))))
;; Cart added response — composes cart mini + add/remove OOB in sx
(defcomp ~market-cart-added-response (&key has-count cart-href blog-href logo
slug action csrf quantity minus-val plus-val)
(<>
(if has-count
(~market-cart-mini-count :href cart-href :count (str has-count))
(~market-cart-mini-empty :href blog-href :logo logo))
(~market-cart-add-oob :id (str "cart-add-" slug)
:inner (if (= (or quantity "0") "0")
(~market-cart-add-empty :cart-id (str "cart-" slug) :action action :csrf csrf)
(~market-cart-add-quantity :cart-id (str "cart-" slug) :action action :csrf csrf
:minus-val minus-val :plus-val plus-val
:quantity quantity :cart-href cart-href)))))

View File

@@ -271,3 +271,18 @@
(~market-filter-stickers-from-data :items sticker-data :hx-select hx-select))
(when brand-data
(~market-filter-brands-from-data :items brand-data :hx-select hx-select))))
;; Composite mobile filter — eliminates SxExpr nesting in Python (M2)
(defcomp ~market-mobile-filter-from-data (&key search-bar
sort-chip liked-chip label-chips sticker-chips brand-chips
sort-data like-data label-data sticker-data brand-data
clear-href hx-select)
(~market-mobile-filter-summary
:search-bar search-bar
:chips (~market-mobile-chips-from-data
:sort-chip sort-chip :liked-chip liked-chip
:label-chips label-chips :sticker-chips sticker-chips :brand-chips brand-chips)
:filter (~market-mobile-filter-content-from-data
:sort-data sort-data :like-data like-data
:label-data label-data :sticker-data sticker-data :brand-data brand-data
:clear-href clear-href :hx-select hx-select)))

View File

@@ -96,10 +96,37 @@
(<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header product-header admin-header))))
;; OOB wrappers
;; OOB wrappers — compose headers + clear divs in sx (no Python concatenation)
(defcomp ~market-oob-wrap (&key parts)
(<> parts))
(defcomp ~market-clear-product-oob ()
"Clear admin-level OOB divs when rendering product detail."
(<>
(~clear-oob-div :id "product-admin-row")
(~clear-oob-div :id "product-admin-header-child")
(~clear-oob-div :id "market-admin-row")
(~clear-oob-div :id "market-admin-header-child")
(~clear-oob-div :id "post-admin-row")
(~clear-oob-div :id "post-admin-header-child")))
(defcomp ~market-clear-product-admin-oob ()
"Clear deeper OOB divs when rendering product admin."
(<>
(~clear-oob-div :id "market-admin-row")
(~clear-oob-div :id "market-admin-header-child")
(~clear-oob-div :id "post-admin-row")
(~clear-oob-div :id "post-admin-header-child")))
(defcomp ~market-product-oob (&key market-header oob-header)
"Product detail OOB: market header + product header + clear deeper."
(<> market-header oob-header (~market-clear-product-oob)))
(defcomp ~market-product-admin-oob (&key product-header oob-header)
"Product admin OOB: product header + admin header + clear deeper."
(<> product-header oob-header (~market-clear-product-admin-oob)))
;; Content wrappers
(defcomp ~market-content-padded (&key content)
(<> content (div :class "pb-8")))

View File

@@ -197,12 +197,6 @@ def _market_cards_sx(markets: list, page_info: dict, page: int, has_more: bool,
next_url=next_url)
def _markets_grid(cards_sx: str) -> str:
"""Wrap market cards in a grid as sx."""
from shared.sx.parser import SxExpr
return sx_call("market-markets-grid", cards=SxExpr(cards_sx))
def _no_markets_sx(message: str = "No markets available") -> str:
"""Empty state for markets as sx."""
return sx_call("empty-state", icon="fa fa-store", message=message,

View File

@@ -366,19 +366,23 @@ def _mobile_filter_content_data(ctx: dict) -> dict:
async def _mobile_filter_summary_sx(ctx: dict) -> str:
"""Build mobile filter summary — delegates to .sx defcomps."""
# Search bar (still uses render_to_sx for shared component)
"""Build mobile filter summary — single sx_call with data kwargs."""
search_bar = await search_mobile_sx(ctx)
# Chips data
chips_data = _mobile_chips_data(ctx)
chips = sx_call("market-mobile-chips-from-data", **chips_data)
# Expanded filter content data
filter_data = _mobile_filter_content_data(ctx)
filter_content = sx_call("market-mobile-filter-content-from-data", **filter_data)
return sx_call("market-mobile-filter-summary",
search_bar=SxExpr(search_bar),
chips=SxExpr(chips),
filter=SxExpr(filter_content))
return sx_call("market-mobile-filter-from-data",
search_bar=SxExpr(search_bar),
sort_chip=chips_data.get("sort-chip"),
liked_chip=chips_data.get("liked-chip"),
label_chips=chips_data.get("label-chips"),
sticker_chips=chips_data.get("sticker-chips"),
brand_chips=chips_data.get("brand-chips"),
sort_data=filter_data.get("sort-data"),
like_data=filter_data.get("like-data"),
label_data=filter_data.get("label-data"),
sticker_data=filter_data.get("sticker-data"),
brand_data=filter_data.get("brand-data"),
clear_href=filter_data.get("clear-href"),
hx_select=filter_data.get("hx-select"),
)

View File

@@ -11,7 +11,7 @@ from shared.sx.helpers import (
full_page_sx, oob_page_sx,
)
from .utils import _clear_deeper_oob, _product_detail_sx, _product_meta_sx
from .utils import _product_detail_sx, _product_meta_sx
from .cards import _product_cards_sx, _market_cards_sx
from .filters import _desktop_filter_sx, _mobile_filter_summary_sx
from .layouts import (
@@ -25,15 +25,9 @@ from .helpers import _markets_admin_panel_sx
# Browse page
# ---------------------------------------------------------------------------
def _product_grid(cards_sx: str) -> str:
"""Wrap product cards in a grid as sx."""
return sx_call("market-product-grid", cards=SxExpr(cards_sx))
async def render_browse_page(ctx: dict) -> str:
"""Full page: product browse with filters."""
cards = _product_cards_sx(ctx)
content = _product_grid(cards)
content = sx_call("market-product-grid", cards=SxExpr(_product_cards_sx(ctx)))
from shared.sx.helpers import render_to_sx_with_env
hdr = await render_to_sx_with_env("market-browse-layout-full", {})
@@ -47,8 +41,7 @@ async def render_browse_page(ctx: dict) -> str:
async def render_browse_oob(ctx: dict) -> str:
"""OOB response: product browse."""
cards = _product_cards_sx(ctx)
content = _product_grid(cards)
content = sx_call("market-product-grid", cards=SxExpr(_product_cards_sx(ctx)))
# Layout handles all OOB headers via auto-fetch macros
oobs = sx_call("market-browse-layout-oob")
@@ -86,13 +79,10 @@ async def render_product_oob(ctx: dict, d: dict) -> str:
"""OOB response: product detail."""
content = _product_detail_sx(d, ctx)
oobs = sx_call("market-oob-wrap",
parts=SxExpr("(<> " + _market_header_sx(ctx, oob=True) + " "
+ await _oob_header_sx("market-header-child", "product-header-child",
_product_header_sx(ctx, d)) + " "
+ _clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child",
"product-row", "product-header-child") + ")"))
oobs = sx_call("market-product-oob",
market_header=SxExpr(_market_header_sx(ctx, oob=True)),
oob_header=SxExpr(await _oob_header_sx("market-header-child", "product-header-child",
_product_header_sx(ctx, d))))
menu = _mobile_nav_panel_sx(ctx)
return await oob_page_sx(oobs=oobs, content=content, menu=menu)
@@ -118,14 +108,10 @@ async def render_product_admin_oob(ctx: dict, d: dict) -> str:
"""OOB response: product admin."""
content = _product_detail_sx(d, ctx)
oobs = sx_call("market-oob-wrap",
parts=SxExpr("(<> " + _product_header_sx(ctx, d, oob=True) + " "
+ await _oob_header_sx("product-header-child", "product-admin-header-child",
_product_admin_header_sx(ctx, d)) + " "
+ _clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child",
"product-row", "product-header-child",
"product-admin-row", "product-admin-header-child") + ")"))
oobs = sx_call("market-product-admin-oob",
product_header=SxExpr(_product_header_sx(ctx, d, oob=True)),
oob_header=SxExpr(await _oob_header_sx("product-header-child", "product-admin-header-child",
_product_admin_header_sx(ctx, d))))
return await oob_page_sx(oobs=oobs, content=content)
@@ -195,10 +181,7 @@ def render_like_toggle_button(slug: str, liked: bool, *,
def render_cart_added_response(cart: list, item: Any, d: dict) -> str:
"""Render the HTMX response after add-to-cart.
Returns OOB fragments: cart-mini icon + product add/remove buttons + cart item row.
"""
"""Render the HTMX response after add-to-cart via ~market-cart-added-response."""
from shared.browser.app.csrf import generate_csrf_token
from quart import url_for
from shared.infrastructure.urls import cart_url as _cart_url
@@ -206,37 +189,28 @@ def render_cart_added_response(cart: list, item: Any, d: dict) -> str:
csrf = generate_csrf_token()
slug = d.get("slug", "")
count = sum(getattr(ci, "quantity", 0) for ci in cart)
quantity = getattr(item, "quantity", 0) if item else 0
action = url_for("market.browse.product.cart", product_slug=slug)
# 1. Cart mini icon OOB
if count > 0:
cart_href = _cart_url("/")
cart_mini = sx_call("market-cart-mini-count", href=cart_href, count=str(count))
blog_href = ""
logo = ""
else:
from shared.config import config
cart_href = ""
blog_href = config().get("blog_url", "/")
logo = config().get("logo", "")
cart_mini = sx_call("market-cart-mini-empty", href=blog_href, logo=logo)
# 2. Add/remove buttons OOB
action = url_for("market.browse.product.cart", product_slug=slug)
quantity = getattr(item, "quantity", 0) if item else 0
if not quantity:
cart_add = sx_call(
"market-cart-add-empty",
cart_id=f"cart-{slug}", action=action, csrf=csrf,
)
else:
cart_href = _cart_url("/") if callable(_cart_url) else "/"
cart_add = sx_call(
"market-cart-add-quantity",
cart_id=f"cart-{slug}", action=action, csrf=csrf,
minus_val=str(quantity - 1), plus_val=str(quantity + 1),
quantity=str(quantity), cart_href=cart_href,
)
add_sx = sx_call(
"market-cart-add-oob",
id=f"cart-add-{slug}",
inner=cart_add,
return sx_call("market-cart-added-response",
has_count=str(count) if count > 0 else None,
cart_href=cart_href,
blog_href=blog_href,
logo=logo,
slug=slug,
action=action,
csrf=csrf,
quantity=str(quantity),
minus_val=str(quantity - 1) if quantity > 0 else "0",
plus_val=str(quantity + 1),
)
return "(<> " + cart_mini + " " + add_sx + ")"

View File

@@ -1,28 +1,9 @@
"""Price helpers, OOB helpers, product detail/meta data builders."""
"""Price helpers, product detail/meta data builders."""
from __future__ import annotations
from shared.sx.helpers import sx_call
# ---------------------------------------------------------------------------
# OOB orphan cleanup
# ---------------------------------------------------------------------------
_MARKET_DEEP_IDS = [
"product-admin-row", "product-admin-header-child",
"product-row", "product-header-child",
"market-admin-row", "market-admin-header-child",
"market-row", "market-header-child",
"post-admin-row", "post-admin-header-child",
]
def _clear_deeper_oob(*keep_ids: str) -> str:
"""Clear all market header rows/children NOT in keep_ids."""
to_clear = [i for i in _MARKET_DEEP_IDS if i not in keep_ids]
return " ".join(sx_call("clear-oob-div", id=i) for i in to_clear)
# ---------------------------------------------------------------------------
# Price helpers
# ---------------------------------------------------------------------------