Refactor SX templates: shared components, Python migration, cleanup
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m0s

- Extract shared components (empty-state, delete-btn, sentinel, crud-*,
  view-toggle, img-or-placeholder, avatar, sumup-settings-form, auth
  forms, order tables/detail/checkout)
- Migrate all Python sx_call() callers to use shared components directly
- Remove 55+ thin wrapper defcomps from domain .sx files
- Remove trivial passthrough wrappers (blog-header-label, market-card-text, etc)
- Unify duplicate auth flows (account + federation) into shared/sx/templates/auth.sx
- Unify duplicate order views (cart + orders) into shared/sx/templates/orders.sx
- Disable static file caching in dev (SEND_FILE_MAX_AGE_DEFAULT=0)
- Add SX response validation and debug headers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 20:34:34 +00:00
parent 755313bd29
commit c0d369eb8e
58 changed files with 3473 additions and 1210 deletions

View File

@@ -37,7 +37,7 @@ load_service_components(os.path.dirname(os.path.dirname(__file__)))
def _clear_oob(*ids: str) -> str:
"""Generate OOB swaps to remove orphaned header rows/children."""
return "".join(f'<div id="{i}" hx-swap-oob="outerHTML"></div>' for i in ids)
return "".join(f'(div :id "{i}" :hx-swap-oob "outerHTML")' for i in ids)
# All possible header row/child IDs at each depth (deepest first)
@@ -68,11 +68,14 @@ async def _ensure_container_nav(ctx: dict) -> dict:
slug = post.get("slug", "")
if not post_id:
return ctx
from quart import g
from shared.infrastructure.fragments import fetch_fragments
current_cal = getattr(g, "calendar_slug", "") or ""
nav_params = {
"container_type": "page",
"container_id": str(post_id),
"post_slug": slug,
"current_calendar": current_cal,
}
events_nav, market_nav = await fetch_fragments([
("events", "container-nav", nav_params),
@@ -361,12 +364,15 @@ def _calendars_main_panel_sx(ctx: dict) -> str:
form_html = ""
if can_create:
create_url = url_for("calendars.create_calendar")
form_html = sx_call("events-calendars-create-form",
create_url=create_url, csrf=csrf)
form_html = sx_call("crud-create-form",
create_url=create_url, csrf=csrf,
errors_id="cal-create-errors", list_id="calendars-list",
placeholder="e.g. Events, Gigs, Meetings", btn_label="Add calendar")
list_html = _calendars_list_sx(ctx, calendars)
return sx_call("events-calendars-panel",
form=SxExpr(form_html), list=SxExpr(list_html))
return sx_call("crud-panel",
form=SxExpr(form_html), list=SxExpr(list_html),
list_id="calendars-list")
def _calendars_list_sx(ctx: dict, calendars: list) -> str:
@@ -378,7 +384,8 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str:
prefix = route_prefix()
if not calendars:
return sx_call("events-calendars-empty")
return sx_call("empty-state", message="No calendars yet. Create one above.",
cls="text-gray-500 mt-4")
parts = []
for cal in calendars:
@@ -387,9 +394,12 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str:
href = prefix + url_for("calendar.get", calendar_slug=cal_slug)
del_url = url_for("calendar.delete", calendar_slug=cal_slug)
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
parts.append(sx_call("events-calendars-item",
href=href, cal_name=cal_name, cal_slug=cal_slug,
del_url=del_url, csrf_hdr=csrf_hdr))
parts.append(sx_call("crud-item",
href=href, name=cal_name, slug=cal_slug,
del_url=del_url, csrf_hdr=csrf_hdr,
list_id="calendars-list",
confirm_title="Delete calendar?",
confirm_text="Entries will be hidden (soft delete)"))
return "".join(parts)
@@ -504,13 +514,15 @@ def _calendar_main_panel_html(ctx: dict) -> str:
bg_cls=bg_cls, name=e.name,
state_label=state_label))
badges_html = "".join(entry_badges)
badges_html = "(<> " + "".join(entry_badges) + ")" if entry_badges else ""
cells.append(sx_call("events-calendar-cell",
cell_cls=cell_cls, day_short=SxExpr(day_short_html),
day_num=SxExpr(day_num_html), badges=SxExpr(badges_html)))
day_num=SxExpr(day_num_html),
badges=SxExpr(badges_html) if badges_html else None))
cells_html = "".join(cells)
arrows_html = "".join(nav_arrows)
cells_html = "(<> " + "".join(cells) + ")"
arrows_html = "(<> " + "".join(nav_arrows) + ")"
wd_html = "(<> " + wd_html + ")"
return sx_call("events-calendar-grid",
arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html),
cells=SxExpr(cells_html))
@@ -632,7 +644,7 @@ def _entry_state_badge_html(state: str) -> str:
}
cls = state_classes.get(state, "bg-stone-100 text-stone-700")
label = state.replace("_", " ").capitalize()
return sx_call("events-state-badge", cls=cls, label=label)
return sx_call("badge", cls=cls, label=label)
# ---------------------------------------------------------------------------
@@ -693,12 +705,15 @@ def _markets_main_panel_html(ctx: dict) -> str:
form_html = ""
if can_create:
create_url = url_for("markets.create_market")
form_html = sx_call("events-markets-create-form",
create_url=create_url, csrf=csrf)
form_html = sx_call("crud-create-form",
create_url=create_url, csrf=csrf,
errors_id="market-create-errors", list_id="markets-list",
placeholder="e.g. Farm Shop, Bakery", btn_label="Add market")
list_html = _markets_list_html(ctx, markets)
return sx_call("events-markets-panel",
form=SxExpr(form_html), list=SxExpr(list_html))
return sx_call("crud-panel",
form=SxExpr(form_html), list=SxExpr(list_html),
list_id="markets-list")
def _markets_list_html(ctx: dict, markets: list) -> str:
@@ -710,7 +725,8 @@ def _markets_list_html(ctx: dict, markets: list) -> str:
slug = post.get("slug", "")
if not markets:
return sx_call("events-markets-empty")
return sx_call("empty-state", message="No markets yet. Create one above.",
cls="text-gray-500 mt-4")
parts = []
for m in markets:
@@ -719,10 +735,13 @@ def _markets_list_html(ctx: dict, markets: list) -> str:
market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
del_url = url_for("markets.delete_market", market_slug=m_slug)
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
parts.append(sx_call("events-markets-item",
href=market_href, market_name=m_name,
market_slug=m_slug, del_url=del_url,
csrf_hdr=csrf_hdr))
parts.append(sx_call("crud-item",
href=market_href, name=m_name,
slug=m_slug, del_url=del_url,
csrf_hdr=csrf_hdr,
list_id="markets-list",
confirm_title="Delete market?",
confirm_text="Products will be hidden (soft delete)"))
return "".join(parts)
@@ -740,7 +759,7 @@ def _ticket_state_badge_html(state: str) -> str:
}
cls = cls_map.get(state, "bg-stone-100 text-stone-700")
label = (state or "").replace("_", " ").capitalize()
return sx_call("events-state-badge", cls=cls, label=label)
return sx_call("badge", cls=cls, label=label)
# ---------------------------------------------------------------------------
@@ -1085,8 +1104,8 @@ def _entry_cards_html(entries, page_info, pending_tickets, ticket_url,
))
if has_more:
parts.append(sx_call("events-sentinel",
page=str(page), next_url=next_url))
parts.append(sx_call("sentinel-simple",
id=f"sentinel-{page}", next_url=next_url))
return "".join(parts)
@@ -1101,14 +1120,14 @@ _TILE_SVG = None
def _get_list_svg():
global _LIST_SVG
if _LIST_SVG is None:
_LIST_SVG = sx_call("events-list-svg")
_LIST_SVG = sx_call("list-svg")
return _LIST_SVG
def _get_tile_svg():
global _TILE_SVG
if _TILE_SVG is None:
_TILE_SVG = sx_call("events-tile-svg")
_TILE_SVG = sx_call("tile-svg")
return _TILE_SVG
@@ -1131,11 +1150,11 @@ def _view_toggle_html(ctx: dict, view: str) -> str:
list_active = 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600'
tile_active = 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600'
return sx_call("events-view-toggle",
return sx_call("view-toggle",
list_href=list_href, tile_href=tile_href,
hx_select=hx_select, list_active=list_active,
tile_active=tile_active, list_svg=_get_list_svg(),
tile_svg=_get_tile_svg())
hx_select=hx_select, list_cls=list_active,
tile_cls=tile_active, storage_key="events_view",
list_svg=_get_list_svg(), tile_svg=_get_tile_svg())
def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info,
@@ -1154,7 +1173,9 @@ def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_
if view == "tile" else "max-w-full px-3 py-3 space-y-3")
body = sx_call("events-grid", grid_cls=grid_cls, cards=SxExpr(cards))
else:
body = sx_call("events-empty")
body = sx_call("empty-state", icon="fa fa-calendar-xmark",
message="No upcoming events",
cls="px-3 py-12 text-center text-stone-400")
return sx_call("events-main-panel-body",
toggle=SxExpr(toggle), body=SxExpr(body))
@@ -1462,7 +1483,7 @@ async def render_slots_page(ctx: dict) -> str:
slots = ctx.get("slots") or []
calendar = ctx.get("calendar")
content = render_slots_table(slots, calendar)
ctx = await _ensure_container_nav(ctx)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
root_hdr = root_header_sx(ctx)
post_hdr = _post_header_sx(ctx)
@@ -1477,7 +1498,7 @@ async def render_slots_oob(ctx: dict) -> str:
slots = ctx.get("slots") or []
calendar = ctx.get("calendar")
content = render_slots_table(slots, calendar)
ctx = await _ensure_container_nav(ctx)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ _calendar_admin_header_sx(ctx, oob=True))
@@ -3012,7 +3033,7 @@ async def render_slot_page(ctx: dict) -> str:
if not slot or not calendar:
return ""
content = render_slot_main_panel(slot, calendar)
ctx = await _ensure_container_nav(ctx)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
root_hdr = root_header_sx(ctx)
post_hdr = _post_header_sx(ctx)
@@ -3030,7 +3051,7 @@ async def render_slot_oob(ctx: dict) -> str:
if not slot or not calendar:
return ""
content = render_slot_main_panel(slot, calendar)
ctx = await _ensure_container_nav(ctx)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ _calendar_admin_header_sx(ctx, oob=True))
@@ -3465,15 +3486,16 @@ def render_fragment_account_tickets(tickets) -> str:
type_name = ""
if getattr(ticket, "ticket_type_name", None):
type_name = f'<span>&middot; {escape(ticket.ticket_type_name)}</span>'
badge_html = sx_call("events-frag-ticket-badge",
state=getattr(ticket, "state", ""))
badge_html = sx_call("status-pill",
status=getattr(ticket, "state", ""))
items_html += sx_call("events-frag-ticket-item",
href=href, entry_name=ticket.entry_name,
date_str=date_str, calendar_name=cal_name,
type_name=type_name, badge=badge_html)
body = sx_call("events-frag-tickets-list", items=SxExpr(items_html))
else:
body = sx_call("events-frag-tickets-empty")
body = sx_call("empty-state", message="No tickets yet.",
cls="text-sm text-stone-500")
return sx_call("events-frag-tickets-panel", items=SxExpr(body))
@@ -3498,8 +3520,8 @@ def render_fragment_account_bookings(bookings) -> str:
cost_str = ""
if getattr(booking, "cost", None):
cost_str = f'<span>&middot; &pound;{escape(str(booking.cost))}</span>'
badge_html = sx_call("events-frag-booking-badge",
state=getattr(booking, "state", ""))
badge_html = sx_call("status-pill",
status=getattr(booking, "state", ""))
items_html += sx_call("events-frag-booking-item",
name=booking.name,
date_str=date_str + date_str_extra,
@@ -3507,6 +3529,7 @@ def render_fragment_account_bookings(bookings) -> str:
badge=badge_html)
body = sx_call("events-frag-bookings-list", items=SxExpr(items_html))
else:
body = sx_call("events-frag-bookings-empty")
body = sx_call("empty-state", message="No bookings yet.",
cls="text-sm text-stone-500")
return sx_call("events-frag-bookings-panel", items=SxExpr(body))