Replace Python sx_call loops with data-driven SX defcomps using map
Move rendering logic from Python for-loops building sx_call strings into SX defcomp components that use map/lambda over data dicts. Python now serializes display data into plain dicts and passes them via a single sx_call; the SX layer handles iteration and conditional rendering. Covers orders (rows, items, calendar, tickets), federation (timeline, search, actors, profile activities), and blog (cards, pages, filters, snippets, menu items, tag groups, page search, nav OOB). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -143,6 +143,80 @@
|
||||
(div :class "max-w-2xl mx-auto px-4 py-6 space-y-6"
|
||||
edit-form delete-form))
|
||||
|
||||
;; Data-driven snippets list (replaces Python _snippets_sx loop)
|
||||
(defcomp ~blog-snippets-from-data (&key snippets user-id is-admin csrf badge-colours)
|
||||
(~blog-snippets-list
|
||||
:rows (<> (map (lambda (s)
|
||||
(let* ((s-id (get s "id"))
|
||||
(s-name (get s "name"))
|
||||
(s-uid (get s "user_id"))
|
||||
(s-vis (get s "visibility"))
|
||||
(owner (if (= s-uid user-id) "You" (str "User #" s-uid)))
|
||||
(badge-cls (or (get badge-colours s-vis) "bg-stone-200 text-stone-700"))
|
||||
(extra (<>
|
||||
(when is-admin
|
||||
(~blog-snippet-visibility-select
|
||||
:patch-url (get s "patch_url")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:options (<>
|
||||
(~blog-snippet-option :value "private" :selected (= s-vis "private") :label "private")
|
||||
(~blog-snippet-option :value "shared" :selected (= s-vis "shared") :label "shared")
|
||||
(~blog-snippet-option :value "admin" :selected (= s-vis "admin") :label "admin"))
|
||||
:cls "text-sm border border-stone-300 rounded px-2 py-1"))
|
||||
(when (or (= s-uid user-id) is-admin)
|
||||
(~delete-btn :url (get s "delete_url") :trigger-target "#snippets-list"
|
||||
:title "Delete snippet?"
|
||||
:text (str "Delete \u201c" s-name "\u201d?")
|
||||
:sx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0")))))
|
||||
(~blog-snippet-row :name s-name :owner owner :badge-cls badge-cls
|
||||
:visibility s-vis :extra extra)))
|
||||
(or snippets (list))))))
|
||||
|
||||
;; Data-driven menu items list (replaces Python _menu_items_list_sx loop)
|
||||
(defcomp ~blog-menu-items-from-data (&key items csrf)
|
||||
(~blog-menu-items-list
|
||||
:rows (<> (map (lambda (item)
|
||||
(let* ((img (~img-or-placeholder :src (get item "feature_image") :alt (get item "label")
|
||||
:size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")))
|
||||
(~blog-menu-item-row
|
||||
:img img :label (get item "label") :slug (get item "slug")
|
||||
:sort-order (get item "sort_order") :edit-url (get item "edit_url")
|
||||
:delete-url (get item "delete_url")
|
||||
:confirm-text (str "Remove " (get item "label") " from the menu?")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}"))))
|
||||
(or items (list))))))
|
||||
|
||||
;; Data-driven tag groups main (replaces Python _tag_groups_main_panel_sx loops)
|
||||
(defcomp ~blog-tag-groups-from-data (&key groups unassigned-tags csrf create-url)
|
||||
(~blog-tag-groups-main
|
||||
:form (~blog-tag-groups-create-form :create-url create-url :csrf csrf)
|
||||
:groups (if (empty? (or groups (list)))
|
||||
(~empty-state :message "No tag groups yet." :cls "text-stone-500 text-sm")
|
||||
(~blog-tag-groups-list
|
||||
:items (<> (map (lambda (g)
|
||||
(let* ((icon (if (get g "feature_image")
|
||||
(~blog-tag-group-icon-image :src (get g "feature_image") :name (get g "name"))
|
||||
(~blog-tag-group-icon-color :style (get g "style") :initial (get g "initial")))))
|
||||
(~blog-tag-group-li :icon icon :edit-href (get g "edit_href")
|
||||
:name (get g "name") :slug (get g "slug") :sort-order (get g "sort_order"))))
|
||||
groups))))
|
||||
:unassigned (when (not (empty? (or unassigned-tags (list))))
|
||||
(~blog-unassigned-tags
|
||||
:heading (str "Unassigned Tags (" (len unassigned-tags) ")")
|
||||
:spans (<> (map (lambda (t)
|
||||
(~blog-unassigned-tag :name (get t "name")))
|
||||
unassigned-tags))))))
|
||||
|
||||
;; Data-driven tag group edit (replaces Python _tag_groups_edit_main_panel_sx loop)
|
||||
(defcomp ~blog-tag-checkboxes-from-data (&key tags)
|
||||
(<> (map (lambda (t)
|
||||
(~blog-tag-checkbox
|
||||
:tag-id (get t "tag_id") :checked (get t "checked")
|
||||
:img (when (get t "feature_image") (~blog-tag-checkbox-image :src (get t "feature_image")))
|
||||
:name (get t "name")))
|
||||
(or tags (list)))))
|
||||
|
||||
;; Preview panel components
|
||||
|
||||
(defcomp ~blog-preview-panel (&key sections)
|
||||
|
||||
@@ -107,6 +107,43 @@
|
||||
(ul :class "flex flex-wrap gap-2 text-sm"
|
||||
(map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors))))))))
|
||||
|
||||
;; Data-driven blog cards list (replaces Python _blog_cards_sx loop)
|
||||
(defcomp ~blog-cards-from-data (&key posts view sentinel)
|
||||
(<>
|
||||
(map (lambda (p)
|
||||
(if (= view "tile")
|
||||
(~blog-card-tile
|
||||
:href (get p "href") :hx-select (get p "hx_select")
|
||||
:feature-image (get p "feature_image") :title (get p "title")
|
||||
:is-draft (get p "is_draft") :publish-requested (get p "publish_requested")
|
||||
:status-timestamp (get p "status_timestamp")
|
||||
:excerpt (get p "excerpt") :tags (get p "tags") :authors (get p "authors"))
|
||||
(~blog-card
|
||||
:slug (get p "slug") :href (get p "href") :hx-select (get p "hx_select")
|
||||
:title (get p "title") :feature-image (get p "feature_image")
|
||||
:excerpt (get p "excerpt") :is-draft (get p "is_draft")
|
||||
:publish-requested (get p "publish_requested")
|
||||
:status-timestamp (get p "status_timestamp")
|
||||
:has-like (get p "has_like") :liked (get p "liked")
|
||||
:like-url (get p "like_url") :csrf-token (get p "csrf_token")
|
||||
:tags (get p "tags") :authors (get p "authors")
|
||||
:widget (when (get p "widget") (~rich-text :html (get p "widget"))))))
|
||||
(or posts (list)))
|
||||
sentinel))
|
||||
|
||||
;; Data-driven page cards list (replaces Python _page_cards_sx loop)
|
||||
(defcomp ~page-cards-from-data (&key pages sentinel)
|
||||
(<>
|
||||
(map (lambda (pg)
|
||||
(~blog-page-card
|
||||
:href (get pg "href") :hx-select (get pg "hx_select")
|
||||
:title (get pg "title")
|
||||
:has-calendar (get pg "has_calendar") :has-market (get pg "has_market")
|
||||
:pub-timestamp (get pg "pub_timestamp")
|
||||
:feature-image (get pg "feature_image") :excerpt (get pg "excerpt")))
|
||||
(or pages (list)))
|
||||
sentinel))
|
||||
|
||||
(defcomp ~blog-page-badges (&key has-calendar has-market)
|
||||
(div :class "flex justify-center gap-2 mt-2"
|
||||
(when has-calendar (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800"
|
||||
|
||||
@@ -63,3 +63,39 @@
|
||||
|
||||
(defcomp ~blog-filter-summary (&key text)
|
||||
(span :class "text-sm text-stone-600" text))
|
||||
|
||||
;; Data-driven tag groups filter (replaces Python _tag_groups_filter_sx loop)
|
||||
(defcomp ~blog-tag-groups-filter-from-data (&key groups selected-groups hx-select)
|
||||
(let* ((is-any (empty? (or selected-groups (list))))
|
||||
(any-cls (if is-any "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50")))
|
||||
(~blog-filter-nav
|
||||
:items (<>
|
||||
(~blog-filter-any-topic :cls any-cls :hx-select hx-select)
|
||||
(map (lambda (g)
|
||||
(let* ((slug (get g "slug"))
|
||||
(name (get g "name"))
|
||||
(is-on (contains? selected-groups slug))
|
||||
(cls (if is-on "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"))
|
||||
(icon (if (get g "feature_image")
|
||||
(~blog-filter-group-icon-image :src (get g "feature_image") :name name)
|
||||
(~blog-filter-group-icon-color :style (get g "style") :initial (get g "initial")))))
|
||||
(~blog-filter-group-li :cls cls :hx-get (str "?group=" slug "&page=1") :hx-select hx-select
|
||||
:icon icon :name name :count (get g "count"))))
|
||||
(or groups (list)))))))
|
||||
|
||||
;; Data-driven authors filter (replaces Python _authors_filter_sx loop)
|
||||
(defcomp ~blog-authors-filter-from-data (&key authors selected-authors hx-select)
|
||||
(let* ((is-any (empty? (or selected-authors (list))))
|
||||
(any-cls (if is-any "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50")))
|
||||
(~blog-filter-nav
|
||||
:items (<>
|
||||
(~blog-filter-any-author :cls any-cls :hx-select hx-select)
|
||||
(map (lambda (a)
|
||||
(let* ((slug (get a "slug"))
|
||||
(is-on (contains? selected-authors slug))
|
||||
(cls (if is-on "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"))
|
||||
(icon (when (get a "profile_image")
|
||||
(~blog-filter-author-icon :src (get a "profile_image") :name (get a "name")))))
|
||||
(~blog-filter-author-li :cls cls :hx-get (str "?author=" slug "&page=1") :hx-select hx-select
|
||||
:icon icon :name (get a "name") :count (get a "count"))))
|
||||
(or authors (list)))))))
|
||||
|
||||
@@ -24,3 +24,37 @@
|
||||
(defcomp ~page-search-empty (&key query)
|
||||
(div :class "p-3 text-center text-stone-400 border border-stone-200 rounded-md"
|
||||
(str "No pages found matching \"" query "\"")))
|
||||
|
||||
;; Data-driven page search results (replaces Python render_page_search_results loop)
|
||||
(defcomp ~page-search-results-from-data (&key pages query has-more search-url next-page)
|
||||
(if (and (not pages) query)
|
||||
(~page-search-empty :query query)
|
||||
(when pages
|
||||
(~page-search-results
|
||||
:items (<> (map (lambda (p)
|
||||
(~page-search-item
|
||||
:id (get p "id") :title (get p "title")
|
||||
:slug (get p "slug") :feature-image (get p "feature_image")))
|
||||
pages))
|
||||
:sentinel (when has-more
|
||||
(~page-search-sentinel :url search-url :query query :next-page next-page))))))
|
||||
|
||||
;; Data-driven menu nav items (replaces Python render_menu_items_nav_oob loop)
|
||||
(defcomp ~blog-menu-nav-from-data (&key items nav-cls container-id arrow-cls scroll-hs)
|
||||
(if (not items)
|
||||
(~blog-nav-empty :wrapper-id "menu-items-nav-wrapper")
|
||||
(~scroll-nav-wrapper :wrapper-id "menu-items-nav-wrapper" :container-id container-id
|
||||
:arrow-cls arrow-cls
|
||||
:left-hs (str "on click set #" container-id ".scrollLeft to #" container-id ".scrollLeft - 200")
|
||||
:scroll-hs scroll-hs
|
||||
:right-hs (str "on click set #" container-id ".scrollLeft to #" container-id ".scrollLeft + 200")
|
||||
:items (<> (map (lambda (item)
|
||||
(let* ((img (~img-or-placeholder :src (get item "feature_image") :alt (get item "label")
|
||||
:size-cls "w-8 h-8 rounded-full object-cover flex-shrink-0")))
|
||||
(if (= (get item "slug") "cart")
|
||||
(~blog-nav-item-plain :href (get item "href") :selected (get item "selected")
|
||||
:nav-cls nav-cls :img img :label (get item "label"))
|
||||
(~blog-nav-item-link :href (get item "href") :hx-get (get item "hx_get")
|
||||
:selected (get item "selected") :nav-cls nav-cls :img img :label (get item "label")))))
|
||||
items))
|
||||
:oob true)))
|
||||
|
||||
@@ -212,14 +212,9 @@ def _blog_cards_sx(ctx: dict) -> str:
|
||||
"""S-expression wire format for blog cards (client renders)."""
|
||||
posts = ctx.get("posts") or []
|
||||
view = ctx.get("view")
|
||||
parts = []
|
||||
for p in posts:
|
||||
if view == "tile":
|
||||
parts.append(_blog_card_tile_sx(p, ctx))
|
||||
else:
|
||||
parts.append(_blog_card_sx(p, ctx))
|
||||
parts.append(_blog_sentinel_sx(ctx))
|
||||
return "(<> " + " ".join(parts) + ")"
|
||||
post_dicts = [_blog_card_data(p, ctx) for p in posts]
|
||||
sentinel = SxExpr(_blog_sentinel_sx(ctx))
|
||||
return sx_call("blog-cards-from-data", posts=post_dicts, view=view, sentinel=sentinel)
|
||||
|
||||
|
||||
def _format_ts(dt) -> str:
|
||||
@@ -249,8 +244,8 @@ def _author_data(authors: list) -> list[dict]:
|
||||
return result
|
||||
|
||||
|
||||
def _blog_card_sx(post: dict, ctx: dict) -> str:
|
||||
"""Single blog card as sx call (wire format) — pure data, no HTML."""
|
||||
def _blog_card_data(post: dict, ctx: dict) -> dict:
|
||||
"""Serialize a blog post to a display-data dict for sx rendering."""
|
||||
from quart import g
|
||||
|
||||
slug = post.get("slug", "")
|
||||
@@ -274,7 +269,7 @@ def _blog_card_sx(post: dict, ctx: dict) -> str:
|
||||
tags = _tag_data(post.get("tags") or [])
|
||||
authors = _author_data(post.get("authors") or [])
|
||||
|
||||
kwargs = dict(
|
||||
d: dict[str, Any] = dict(
|
||||
slug=slug, href=href, hx_select=hx_select,
|
||||
title=post.get("title", ""),
|
||||
feature_image=fi, excerpt=excerpt,
|
||||
@@ -285,54 +280,18 @@ def _blog_card_sx(post: dict, ctx: dict) -> str:
|
||||
)
|
||||
|
||||
if user:
|
||||
kwargs["liked"] = post.get("is_liked", False)
|
||||
kwargs["like_url"] = call_url(ctx, "blog_url", f"/{slug}/like/toggle/")
|
||||
kwargs["csrf_token"] = _ctx_csrf(ctx)
|
||||
d["liked"] = post.get("is_liked", False)
|
||||
d["like_url"] = call_url(ctx, "blog_url", f"/{slug}/like/toggle/")
|
||||
d["csrf_token"] = _ctx_csrf(ctx)
|
||||
|
||||
if tags:
|
||||
kwargs["tags"] = tags
|
||||
d["tags"] = tags
|
||||
if authors:
|
||||
kwargs["authors"] = authors
|
||||
d["authors"] = authors
|
||||
if widget:
|
||||
kwargs["widget"] = SxExpr(widget) if widget else None
|
||||
d["widget"] = widget
|
||||
|
||||
return sx_call("blog-card", **kwargs)
|
||||
|
||||
|
||||
def _blog_card_tile_sx(post: dict, ctx: dict) -> str:
|
||||
"""Single blog card tile as sx call (wire format) — pure data."""
|
||||
slug = post.get("slug", "")
|
||||
href = call_url(ctx, "blog_url", f"/{slug}/")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
fi = post.get("feature_image")
|
||||
status = post.get("status", "published")
|
||||
is_draft = status == "draft"
|
||||
|
||||
if is_draft:
|
||||
status_timestamp = _format_ts(post.get("updated_at"))
|
||||
else:
|
||||
status_timestamp = _format_ts(post.get("published_at"))
|
||||
|
||||
excerpt = post.get("custom_excerpt") or post.get("excerpt", "")
|
||||
tags = _tag_data(post.get("tags") or [])
|
||||
authors = _author_data(post.get("authors") or [])
|
||||
|
||||
kwargs = dict(
|
||||
href=href, hx_select=hx_select, feature_image=fi,
|
||||
title=post.get("title", ""),
|
||||
is_draft=is_draft,
|
||||
publish_requested=post.get("publish_requested", False) if is_draft else False,
|
||||
status_timestamp=status_timestamp,
|
||||
excerpt=excerpt,
|
||||
)
|
||||
|
||||
if tags:
|
||||
kwargs["tags"] = tags
|
||||
if authors:
|
||||
kwargs["authors"] = authors
|
||||
|
||||
return sx_call("blog-card-tile", **kwargs)
|
||||
return d
|
||||
|
||||
|
||||
def _at_bar_sx(post: dict, ctx: dict) -> str:
|
||||
@@ -359,6 +318,22 @@ def _at_bar_sx(post: dict, ctx: dict) -> str:
|
||||
|
||||
|
||||
|
||||
def _page_card_data(page: dict, ctx: dict) -> dict:
|
||||
"""Serialize a page to a display-data dict for sx rendering."""
|
||||
slug = page.get("slug", "")
|
||||
href = call_url(ctx, "blog_url", f"/{slug}/")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
features = page.get("features") or {}
|
||||
return dict(
|
||||
href=href, hx_select=hx_select, title=page.get("title", ""),
|
||||
has_calendar=features.get("calendar", False),
|
||||
has_market=features.get("market", False),
|
||||
pub_timestamp=_format_ts(page.get("published_at")),
|
||||
feature_image=page.get("feature_image"),
|
||||
excerpt=page.get("custom_excerpt") or page.get("excerpt", ""),
|
||||
)
|
||||
|
||||
|
||||
def _page_cards_sx(ctx: dict) -> str:
|
||||
"""Render page cards with sentinel (sx)."""
|
||||
pages = ctx.get("pages") or ctx.get("posts") or []
|
||||
@@ -366,45 +341,22 @@ def _page_cards_sx(ctx: dict) -> str:
|
||||
total_pages = ctx.get("total_pages", 1)
|
||||
if isinstance(total_pages, str):
|
||||
total_pages = int(total_pages)
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
parts = []
|
||||
for pg in pages:
|
||||
parts.append(_page_card_sx(pg, ctx))
|
||||
page_dicts = [_page_card_data(pg, ctx) for pg in pages]
|
||||
|
||||
if page_num < total_pages:
|
||||
current_local_href = ctx.get("current_local_href", "/index?type=pages")
|
||||
next_url = f"{current_local_href}&page={page_num + 1}" if "?" in current_local_href else f"{current_local_href}?page={page_num + 1}"
|
||||
parts.append(sx_call("sentinel-simple",
|
||||
id=f"sentinel-{page_num}-d", next_url=next_url,
|
||||
))
|
||||
sentinel = SxExpr(sx_call("sentinel-simple",
|
||||
id=f"sentinel-{page_num}-d", next_url=next_url))
|
||||
elif pages:
|
||||
parts.append(sx_call("end-of-results"))
|
||||
sentinel = SxExpr(sx_call("end-of-results"))
|
||||
elif not pages:
|
||||
sentinel = SxExpr(sx_call("blog-no-pages"))
|
||||
else:
|
||||
parts.append(sx_call("blog-no-pages"))
|
||||
sentinel = None
|
||||
|
||||
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||
|
||||
|
||||
def _page_card_sx(page: dict, ctx: dict) -> str:
|
||||
"""Single page card as sx."""
|
||||
slug = page.get("slug", "")
|
||||
href = call_url(ctx, "blog_url", f"/{slug}/")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
features = page.get("features") or {}
|
||||
pub_timestamp = _format_ts(page.get("published_at"))
|
||||
|
||||
fi = page.get("feature_image")
|
||||
excerpt = page.get("custom_excerpt") or page.get("excerpt", "")
|
||||
|
||||
return sx_call("blog-page-card",
|
||||
href=href, hx_select=hx_select, title=page.get("title", ""),
|
||||
has_calendar=features.get("calendar", False),
|
||||
has_market=features.get("market", False),
|
||||
pub_timestamp=pub_timestamp, feature_image=fi,
|
||||
excerpt=excerpt,
|
||||
)
|
||||
return sx_call("page-cards-from-data", pages=page_dicts, sentinel=sentinel)
|
||||
|
||||
|
||||
def _view_toggle_sx(ctx: dict) -> str:
|
||||
@@ -566,15 +518,10 @@ def _action_buttons_sx(ctx: dict) -> str:
|
||||
def _tag_groups_filter_sx(ctx: dict) -> str:
|
||||
"""Tag group filter bar as sx."""
|
||||
tag_groups = ctx.get("tag_groups") or []
|
||||
selected_groups = ctx.get("selected_groups") or ()
|
||||
selected_tags = ctx.get("selected_tags") or ()
|
||||
selected_groups = list(ctx.get("selected_groups") or ())
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
is_any = len(selected_groups) == 0 and len(selected_tags) == 0
|
||||
any_cls = "bg-stone-900 text-white border-stone-900" if is_any else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"
|
||||
|
||||
li_parts = [sx_call("blog-filter-any-topic", cls=any_cls, hx_select=hx_select)]
|
||||
|
||||
group_dicts = []
|
||||
for group in tag_groups:
|
||||
g_slug = getattr(group, "slug", "") if hasattr(group, "slug") else group.get("slug", "")
|
||||
g_name = getattr(group, "name", "") if hasattr(group, "name") else group.get("name", "")
|
||||
@@ -585,55 +532,35 @@ def _tag_groups_filter_sx(ctx: dict) -> str:
|
||||
if g_count <= 0 and g_slug not in selected_groups:
|
||||
continue
|
||||
|
||||
is_on = g_slug in selected_groups
|
||||
cls = "bg-stone-900 text-white border-stone-900" if is_on else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"
|
||||
|
||||
if g_fi:
|
||||
icon = sx_call("blog-filter-group-icon-image", src=g_fi, name=g_name)
|
||||
else:
|
||||
style = f"background-color: {g_colour}; color: white;" if g_colour else "background-color: #e7e5e4; color: #57534e;"
|
||||
icon = sx_call("blog-filter-group-icon-color", style=style, initial=g_name[:1])
|
||||
|
||||
li_parts.append(sx_call("blog-filter-group-li",
|
||||
cls=cls, hx_get=f"?group={g_slug}&page=1", hx_select=hx_select,
|
||||
icon=SxExpr(icon), name=g_name, count=str(g_count),
|
||||
style = (f"background-color: {g_colour}; color: white;" if g_colour
|
||||
else "background-color: #e7e5e4; color: #57534e;") if not g_fi else None
|
||||
group_dicts.append(dict(
|
||||
slug=g_slug, name=g_name, feature_image=g_fi,
|
||||
style=style, initial=g_name[:1], count=str(g_count),
|
||||
))
|
||||
|
||||
items = "(<> " + " ".join(li_parts) + ")"
|
||||
return sx_call("blog-filter-nav", items=SxExpr(items))
|
||||
return sx_call("blog-tag-groups-filter-from-data",
|
||||
groups=group_dicts, selected_groups=selected_groups, hx_select=hx_select)
|
||||
|
||||
|
||||
def _authors_filter_sx(ctx: dict) -> str:
|
||||
"""Author filter bar as sx."""
|
||||
authors = ctx.get("authors") or []
|
||||
selected_authors = ctx.get("selected_authors") or ()
|
||||
selected_authors = list(ctx.get("selected_authors") or ())
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
is_any = len(selected_authors) == 0
|
||||
any_cls = "bg-stone-900 text-white border-stone-900" if is_any else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"
|
||||
|
||||
li_parts = [sx_call("blog-filter-any-author", cls=any_cls, hx_select=hx_select)]
|
||||
|
||||
author_dicts = []
|
||||
for author in authors:
|
||||
a_slug = getattr(author, "slug", "") if hasattr(author, "slug") else author.get("slug", "")
|
||||
a_name = getattr(author, "name", "") if hasattr(author, "name") else author.get("name", "")
|
||||
a_img = getattr(author, "profile_image", None) if hasattr(author, "profile_image") else author.get("profile_image")
|
||||
a_count = getattr(author, "published_post_count", 0) if hasattr(author, "published_post_count") else author.get("published_post_count", 0)
|
||||
|
||||
is_on = a_slug in selected_authors
|
||||
cls = "bg-stone-900 text-white border-stone-900" if is_on else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"
|
||||
|
||||
icon_sx = None
|
||||
if a_img:
|
||||
icon_sx = sx_call("blog-filter-author-icon", src=a_img, name=a_name)
|
||||
|
||||
li_parts.append(sx_call("blog-filter-author-li",
|
||||
cls=cls, hx_get=f"?author={a_slug}&page=1", hx_select=hx_select,
|
||||
icon=SxExpr(icon_sx) if icon_sx else None, name=a_name, count=str(a_count),
|
||||
author_dicts.append(dict(
|
||||
slug=a_slug, name=a_name, profile_image=a_img, count=str(a_count),
|
||||
))
|
||||
|
||||
items = "(<> " + " ".join(li_parts) + ")"
|
||||
return sx_call("blog-filter-nav", items=SxExpr(items))
|
||||
return sx_call("blog-authors-filter-from-data",
|
||||
authors=author_dicts, selected_authors=selected_authors, hx_select=hx_select)
|
||||
|
||||
|
||||
def _tag_groups_filter_summary_sx(ctx: dict) -> str:
|
||||
@@ -846,48 +773,21 @@ def _snippets_list_sx(ctx: dict) -> str:
|
||||
"admin": "bg-amber-100 text-amber-700",
|
||||
}
|
||||
|
||||
row_parts = []
|
||||
snippet_dicts = []
|
||||
for s in snippets:
|
||||
s_id = getattr(s, "id", None) or s.get("id")
|
||||
s_name = getattr(s, "name", "") if hasattr(s, "name") else s.get("name", "")
|
||||
s_uid = getattr(s, "user_id", None) if hasattr(s, "user_id") else s.get("user_id")
|
||||
s_vis = getattr(s, "visibility", "private") if hasattr(s, "visibility") else s.get("visibility", "private")
|
||||
|
||||
owner = "You" if s_uid == user_id else f"User #{s_uid}"
|
||||
badge_cls = badge_colours.get(s_vis, "bg-stone-200 text-stone-700")
|
||||
|
||||
extra = ""
|
||||
if is_admin:
|
||||
patch_url = qurl("snippets.patch_visibility", snippet_id=s_id)
|
||||
opts = ""
|
||||
for v in ["private", "shared", "admin"]:
|
||||
opts += sx_call("blog-snippet-option",
|
||||
value=v, selected=(s_vis == v), label=v,
|
||||
)
|
||||
extra += sx_call("blog-snippet-visibility-select",
|
||||
patch_url=patch_url,
|
||||
hx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
options=SxExpr("(<> " + opts + ")") if opts else None,
|
||||
cls="text-sm border border-stone-300 rounded px-2 py-1",
|
||||
)
|
||||
|
||||
if s_uid == user_id or is_admin:
|
||||
del_url = qurl("snippets.delete_snippet", snippet_id=s_id)
|
||||
extra += sx_call("delete-btn",
|
||||
url=del_url, trigger_target="#snippets-list",
|
||||
title="Delete snippet?",
|
||||
text=f'Delete \u201c{s_name}\u201d?',
|
||||
sx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
cls="px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0",
|
||||
)
|
||||
|
||||
row_parts.append(sx_call("blog-snippet-row",
|
||||
name=s_name, owner=owner, badge_cls=badge_cls,
|
||||
visibility=s_vis, extra=SxExpr("(<> " + extra + ")") if extra else None,
|
||||
snippet_dicts.append(dict(
|
||||
id=s_id, name=s_name, user_id=s_uid, visibility=s_vis,
|
||||
patch_url=qurl("snippets.patch_visibility", snippet_id=s_id),
|
||||
delete_url=qurl("snippets.delete_snippet", snippet_id=s_id),
|
||||
))
|
||||
|
||||
rows = "(<> " + " ".join(row_parts) + ")"
|
||||
return sx_call("blog-snippets-list", rows=SxExpr(rows))
|
||||
return sx_call("blog-snippets-from-data",
|
||||
snippets=snippet_dicts, user_id=user_id, is_admin=is_admin,
|
||||
csrf=csrf, badge_colours=badge_colours)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -911,29 +811,21 @@ def _menu_items_list_sx(ctx: dict) -> str:
|
||||
if not menu_items:
|
||||
return sx_call("empty-state", icon="fa fa-inbox", message="No menu items yet. Add one to get started!")
|
||||
|
||||
row_parts = []
|
||||
item_dicts = []
|
||||
for item in menu_items:
|
||||
i_id = getattr(item, "id", None) or item.get("id")
|
||||
label = getattr(item, "label", "") if hasattr(item, "label") else item.get("label", "")
|
||||
slug = getattr(item, "slug", "") if hasattr(item, "slug") else item.get("slug", "")
|
||||
fi = getattr(item, "feature_image", None) if hasattr(item, "feature_image") else item.get("feature_image")
|
||||
sort = getattr(item, "sort_order", 0) if hasattr(item, "sort_order") else item.get("sort_order", 0)
|
||||
|
||||
edit_url = qurl("menu_items.edit_menu_item", item_id=i_id)
|
||||
del_url = qurl("menu_items.delete_menu_item_route", item_id=i_id)
|
||||
|
||||
img_sx = sx_call("img-or-placeholder", src=fi, alt=label,
|
||||
size_cls="w-12 h-12 rounded-full object-cover flex-shrink-0")
|
||||
|
||||
row_parts.append(sx_call("blog-menu-item-row",
|
||||
img=SxExpr(img_sx), label=label, slug=slug,
|
||||
sort_order=str(sort), edit_url=edit_url, delete_url=del_url,
|
||||
confirm_text=f"Remove {label} from the menu?",
|
||||
hx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
item_dicts.append(dict(
|
||||
label=label, slug=slug, feature_image=fi,
|
||||
sort_order=str(sort),
|
||||
edit_url=qurl("menu_items.edit_menu_item", item_id=i_id),
|
||||
delete_url=qurl("menu_items.delete_menu_item_route", item_id=i_id),
|
||||
))
|
||||
|
||||
rows = "(<> " + " ".join(row_parts) + ")"
|
||||
return sx_call("blog-menu-items-list", rows=SxExpr(rows))
|
||||
return sx_call("blog-menu-items-from-data", items=item_dicts, csrf=csrf)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -946,57 +838,32 @@ def _tag_groups_main_panel_sx(ctx: dict) -> str:
|
||||
groups = ctx.get("groups") or []
|
||||
unassigned_tags = ctx.get("unassigned_tags") or []
|
||||
csrf = _ctx_csrf(ctx)
|
||||
|
||||
create_url = qurl("blog.tag_groups_admin.create")
|
||||
form_sx = sx_call("blog-tag-groups-create-form",
|
||||
create_url=create_url, csrf=csrf,
|
||||
)
|
||||
|
||||
# Groups list
|
||||
groups_html = ""
|
||||
if groups:
|
||||
li_parts = []
|
||||
for group in groups:
|
||||
g_id = getattr(group, "id", None) or group.get("id")
|
||||
g_name = getattr(group, "name", "") if hasattr(group, "name") else group.get("name", "")
|
||||
g_slug = getattr(group, "slug", "") if hasattr(group, "slug") else group.get("slug", "")
|
||||
g_fi = getattr(group, "feature_image", None) if hasattr(group, "feature_image") else group.get("feature_image")
|
||||
g_colour = getattr(group, "colour", None) if hasattr(group, "colour") else group.get("colour")
|
||||
g_sort = getattr(group, "sort_order", 0) if hasattr(group, "sort_order") else group.get("sort_order", 0)
|
||||
group_dicts = []
|
||||
for group in groups:
|
||||
g_id = getattr(group, "id", None) or group.get("id")
|
||||
g_name = getattr(group, "name", "") if hasattr(group, "name") else group.get("name", "")
|
||||
g_slug = getattr(group, "slug", "") if hasattr(group, "slug") else group.get("slug", "")
|
||||
g_fi = getattr(group, "feature_image", None) if hasattr(group, "feature_image") else group.get("feature_image")
|
||||
g_colour = getattr(group, "colour", None) if hasattr(group, "colour") else group.get("colour")
|
||||
g_sort = getattr(group, "sort_order", 0) if hasattr(group, "sort_order") else group.get("sort_order", 0)
|
||||
style = (f"background-color: {g_colour}; color: white;" if g_colour
|
||||
else "background-color: #e7e5e4; color: #57534e;") if not g_fi else None
|
||||
group_dicts.append(dict(
|
||||
name=g_name, slug=g_slug, feature_image=g_fi,
|
||||
style=style, initial=g_name[:1], sort_order=str(g_sort),
|
||||
edit_href=qurl("blog.tag_groups_admin.defpage_tag_group_edit", id=g_id),
|
||||
))
|
||||
|
||||
edit_href = qurl("blog.tag_groups_admin.defpage_tag_group_edit", id=g_id)
|
||||
unassigned_dicts = []
|
||||
for tag in unassigned_tags:
|
||||
t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "")
|
||||
unassigned_dicts.append(dict(name=t_name))
|
||||
|
||||
if g_fi:
|
||||
icon = sx_call("blog-tag-group-icon-image", src=g_fi, name=g_name)
|
||||
else:
|
||||
style = f"background-color: {g_colour}; color: white;" if g_colour else "background-color: #e7e5e4; color: #57534e;"
|
||||
icon = sx_call("blog-tag-group-icon-color", style=style, initial=g_name[:1])
|
||||
|
||||
li_parts.append(sx_call("blog-tag-group-li",
|
||||
icon=SxExpr(icon), edit_href=edit_href, name=g_name,
|
||||
slug=g_slug, sort_order=str(g_sort),
|
||||
))
|
||||
groups_sx = sx_call("blog-tag-groups-list", items=SxExpr("(<> " + " ".join(li_parts) + ")"))
|
||||
else:
|
||||
groups_sx = sx_call("empty-state", message="No tag groups yet.", cls="text-stone-500 text-sm")
|
||||
|
||||
# Unassigned tags
|
||||
unassigned_sx = ""
|
||||
if unassigned_tags:
|
||||
tag_spans = []
|
||||
for tag in unassigned_tags:
|
||||
t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "")
|
||||
tag_spans.append(sx_call("blog-unassigned-tag", name=t_name))
|
||||
unassigned_sx = sx_call("blog-unassigned-tags",
|
||||
heading=f"Unassigned Tags ({len(unassigned_tags)})",
|
||||
spans=SxExpr("(<> " + " ".join(tag_spans) + ")"),
|
||||
)
|
||||
|
||||
return sx_call("blog-tag-groups-main",
|
||||
form=SxExpr(form_sx),
|
||||
groups=SxExpr(groups_sx),
|
||||
unassigned=SxExpr(unassigned_sx) if unassigned_sx else None,
|
||||
)
|
||||
return sx_call("blog-tag-groups-from-data",
|
||||
groups=group_dicts, unassigned_tags=unassigned_dicts,
|
||||
csrf=csrf, create_url=create_url)
|
||||
|
||||
|
||||
def _tag_groups_edit_main_panel_sx(ctx: dict) -> str:
|
||||
@@ -1016,24 +883,24 @@ def _tag_groups_edit_main_panel_sx(ctx: dict) -> str:
|
||||
save_url = qurl("blog.tag_groups_admin.save", id=g_id)
|
||||
del_url = qurl("blog.tag_groups_admin.delete_group", id=g_id)
|
||||
|
||||
# Tag checkboxes
|
||||
tag_items = []
|
||||
# Tag checkboxes — pass data dicts to defcomp
|
||||
tag_dicts = []
|
||||
for tag in all_tags:
|
||||
t_id = getattr(tag, "id", None) or tag.get("id")
|
||||
t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "")
|
||||
t_fi = getattr(tag, "feature_image", None) if hasattr(tag, "feature_image") else tag.get("feature_image")
|
||||
checked = t_id in assigned_tag_ids
|
||||
img = sx_call("blog-tag-checkbox-image", src=t_fi) if t_fi else ""
|
||||
tag_items.append(sx_call("blog-tag-checkbox",
|
||||
tag_id=str(t_id), checked=checked,
|
||||
img=SxExpr(img) if img else None, name=t_name,
|
||||
tag_dicts.append(dict(
|
||||
tag_id=str(t_id), name=t_name, feature_image=t_fi,
|
||||
checked=t_id in assigned_tag_ids,
|
||||
))
|
||||
|
||||
tags_sx = sx_call("blog-tag-checkboxes-from-data", tags=tag_dicts)
|
||||
|
||||
edit_form = sx_call("blog-tag-group-edit-form",
|
||||
save_url=save_url, csrf=csrf,
|
||||
name=g_name, colour=g_colour or "", sort_order=str(g_sort),
|
||||
feature_image=g_fi or "",
|
||||
tags=SxExpr("(<> " + " ".join(tag_items) + ")"),
|
||||
tags=SxExpr(tags_sx),
|
||||
)
|
||||
|
||||
del_form = sx_call("blog-tag-group-delete-form",
|
||||
@@ -2171,24 +2038,17 @@ def render_page_search_results(pages, query, page, has_more) -> str:
|
||||
if not pages:
|
||||
return ""
|
||||
|
||||
items = []
|
||||
for post in pages:
|
||||
items.append(sx_call("page-search-item",
|
||||
id=post.id, title=post.title,
|
||||
slug=post.slug,
|
||||
feature_image=post.feature_image or None))
|
||||
page_dicts = [
|
||||
dict(id=post.id, title=post.title, slug=post.slug,
|
||||
feature_image=post.feature_image or None)
|
||||
for post in pages
|
||||
]
|
||||
|
||||
sentinel = ""
|
||||
if has_more:
|
||||
search_url = qurl("menu_items.search_pages_route")
|
||||
sentinel = sx_call("page-search-sentinel",
|
||||
url=search_url, query=query,
|
||||
next_page=page + 1)
|
||||
search_url = qurl("menu_items.search_pages_route") if has_more else None
|
||||
|
||||
items_sx = "(<> " + " ".join(items) + ")"
|
||||
return sx_call("page-search-results",
|
||||
items=SxExpr(items_sx),
|
||||
sentinel=SxExpr(sentinel) if sentinel else None)
|
||||
return sx_call("page-search-results-from-data",
|
||||
pages=page_dicts, query=query, has_more=has_more,
|
||||
search_url=search_url, next_page=page + 1)
|
||||
|
||||
|
||||
def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str:
|
||||
@@ -2203,26 +2063,23 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str:
|
||||
if not menu_items:
|
||||
return sx_call("blog-nav-empty", wrapper_id="menu-items-nav-wrapper")
|
||||
|
||||
# Resolve URL helpers from context or fall back to template globals
|
||||
if ctx is None:
|
||||
ctx = {}
|
||||
|
||||
first_seg = qrequest.path.strip("/").split("/")[0] if qrequest else ""
|
||||
|
||||
# nav_button style (matches shared/infrastructure/jinja_setup.py)
|
||||
select_colours = (
|
||||
"[.hover-capable_&]:hover:bg-yellow-300"
|
||||
" aria-selected:bg-stone-500 aria-selected:text-white"
|
||||
" [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500"
|
||||
)
|
||||
nav_button_cls = (
|
||||
nav_cls = (
|
||||
f"justify-center cursor-pointer flex flex-row items-center gap-2"
|
||||
f" rounded bg-stone-200 text-black {select_colours} p-3"
|
||||
)
|
||||
|
||||
container_id = "menu-items-container"
|
||||
arrow_cls = f"scrolling-menu-arrow-{container_id}"
|
||||
|
||||
scroll_hs = (
|
||||
f"on load or scroll"
|
||||
f" if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth"
|
||||
@@ -2234,7 +2091,7 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str:
|
||||
cart_url_fn = ctx.get("cart_url")
|
||||
app_name = ctx.get("app_name", "")
|
||||
|
||||
item_parts = []
|
||||
item_dicts = []
|
||||
for item in menu_items:
|
||||
item_slug = getattr(item, "slug", "") if hasattr(item, "slug") else item.get("slug", "")
|
||||
label = getattr(item, "label", "") if hasattr(item, "label") else item.get("label", "")
|
||||
@@ -2248,31 +2105,16 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str:
|
||||
href = f"/{item_slug}/"
|
||||
|
||||
selected = "true" if (item_slug == first_seg or item_slug == app_name) else "false"
|
||||
hx_get = f"/{item_slug}/" if item_slug != "cart" else None
|
||||
|
||||
img_sx = sx_call("img-or-placeholder", src=fi, alt=label,
|
||||
size_cls="w-8 h-8 rounded-full object-cover flex-shrink-0")
|
||||
item_dicts.append(dict(
|
||||
slug=item_slug, label=label, feature_image=fi,
|
||||
href=href, hx_get=hx_get, selected=selected,
|
||||
))
|
||||
|
||||
if item_slug != "cart":
|
||||
item_parts.append(sx_call("blog-nav-item-link",
|
||||
href=href, hx_get=f"/{item_slug}/", selected=selected,
|
||||
nav_cls=nav_button_cls, img=SxExpr(img_sx), label=label,
|
||||
))
|
||||
else:
|
||||
item_parts.append(sx_call("blog-nav-item-plain",
|
||||
href=href, selected=selected, nav_cls=nav_button_cls,
|
||||
img=SxExpr(img_sx), label=label,
|
||||
))
|
||||
|
||||
items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else ""
|
||||
|
||||
return sx_call("scroll-nav-wrapper",
|
||||
wrapper_id="menu-items-nav-wrapper", container_id=container_id,
|
||||
arrow_cls=arrow_cls,
|
||||
left_hs=f"on click set #{container_id}.scrollLeft to #{container_id}.scrollLeft - 200",
|
||||
scroll_hs=scroll_hs,
|
||||
right_hs=f"on click set #{container_id}.scrollLeft to #{container_id}.scrollLeft + 200",
|
||||
items=SxExpr(items_sx) if items_sx else None, oob=True,
|
||||
)
|
||||
return sx_call("blog-menu-nav-from-data",
|
||||
items=item_dicts, nav_cls=nav_cls, container_id=container_id,
|
||||
arrow_cls=arrow_cls, scroll_hs=scroll_hs)
|
||||
|
||||
|
||||
# ---- Features panel ----
|
||||
|
||||
Reference in New Issue
Block a user