Replace JSON sx-headers with SX dict expressions, fix blog like component
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m6s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m6s
sx-headers attributes now use native SX dict format {:key val} instead of
JSON strings. Eliminates manual JSON string construction in both .sx files
and Python callers.
- sx.js: parse sx-headers/sx-vals as SX dict ({: prefix) with JSON fallback,
add _serializeDict for dict→attribute serialization, fix verbInfo scope in
_doFetch error handler
- html.py: serialize dict attribute values via SX serialize() not str()
- All .sx files: {:X-CSRFToken csrf} replaces (str "{\"X-CSRFToken\": ...}")
- All Python callers: {"X-CSRFToken": csrf} dict replaces f-string JSON
- Blog like: extract ~blog-like-toggle, fix POST returning wrong component,
fix emoji escapes in .sx (parser has no \U support), fix card :hx-headers
keyword mismatch, wrap sx_content in SxExpr for evaluation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,7 @@ def register(url_prefix="/"):
|
|||||||
return sx_response(sx_call(
|
return sx_response(sx_call(
|
||||||
"account-newsletter-toggle",
|
"account-newsletter-toggle",
|
||||||
id=f"nl-{nid}", url=toggle_url,
|
id=f"nl-{nid}", url=toggle_url,
|
||||||
hdrs=f'{{"X-CSRFToken": "{csrf}"}}',
|
hdrs={"X-CSRFToken": csrf},
|
||||||
target=f"#nl-{nid}",
|
target=f"#nl-{nid}",
|
||||||
cls=f"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 {bg}",
|
cls=f"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 {bg}",
|
||||||
checked=checked,
|
checked=checked,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
:toggle (~account-newsletter-toggle
|
:toggle (~account-newsletter-toggle
|
||||||
:id (str "nl-" nid)
|
:id (str "nl-" nid)
|
||||||
:url toggle-url
|
:url toggle-url
|
||||||
:hdrs (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
:hdrs {:X-CSRFToken csrf}
|
||||||
:target (str "#nl-" nid)
|
:target (str "#nl-" nid)
|
||||||
:cls (str "relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 " bg)
|
:cls (str "relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 " bg)
|
||||||
:checked checked
|
:checked checked
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ def _render_calendar_view(
|
|||||||
e_id = getattr(e, "id", None)
|
e_id = getattr(e, "id", None)
|
||||||
e_name = esc(getattr(e, "name", ""))
|
e_name = esc(getattr(e, "name", ""))
|
||||||
t_url = toggle_url_fn(e_id)
|
t_url = toggle_url_fn(e_id)
|
||||||
hx_hdrs = f'{{"X-CSRFToken": "{csrf}"}}'
|
hx_hdrs = '{:X-CSRFToken "' + csrf + '"}'
|
||||||
|
|
||||||
if e_id in associated_entry_ids:
|
if e_id in associated_entry_ids:
|
||||||
entry_btns.append(
|
entry_btns.append(
|
||||||
|
|||||||
@@ -156,14 +156,10 @@ def register():
|
|||||||
csrf = generate_csrf_token()
|
csrf = generate_csrf_token()
|
||||||
|
|
||||||
def _like_btn(liked):
|
def _like_btn(liked):
|
||||||
if liked:
|
return sx_call("blog-like-toggle",
|
||||||
colour, icon, label = "text-red-600", "fa-solid fa-heart", "Unlike this post"
|
like_url=like_url,
|
||||||
else:
|
hx_headers={"X-CSRFToken": csrf},
|
||||||
colour, icon, label = "text-stone-300", "fa-regular fa-heart", "Like this post"
|
heart="\u2764\ufe0f" if liked else "\U0001f90d")
|
||||||
return sx_call("market-like-toggle-button",
|
|
||||||
colour=colour, action=like_url,
|
|
||||||
hx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
|
||||||
label=label, icon_cls=icon)
|
|
||||||
|
|
||||||
if not g.user:
|
if not g.user:
|
||||||
return sx_response(_like_btn(False), status=403)
|
return sx_response(_like_btn(False), status=403)
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
"""Blog page data service — provides serialized dicts for .sx defpages."""
|
"""Blog page data service — provides serialized dicts for .sx defpages."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from shared.sx.parser import SxExpr
|
||||||
|
|
||||||
|
|
||||||
|
def _sx_content_expr(raw: str) -> SxExpr | None:
|
||||||
|
"""Wrap non-empty sx_content as SxExpr so it serializes unquoted."""
|
||||||
|
return SxExpr(raw) if raw else None
|
||||||
|
|
||||||
|
|
||||||
class BlogPageService:
|
class BlogPageService:
|
||||||
"""Service for blog page data, callable via (service "blog-page" ...)."""
|
"""Service for blog page data, callable via (service "blog-page" ...)."""
|
||||||
@@ -424,7 +431,7 @@ class BlogPageService:
|
|||||||
"authors": authors,
|
"authors": authors,
|
||||||
"feature_image": post.get("feature_image"),
|
"feature_image": post.get("feature_image"),
|
||||||
"html_content": post.get("html", ""),
|
"html_content": post.get("html", ""),
|
||||||
"sx_content": post.get("sx_content", ""),
|
"sx_content": _sx_content_expr(post.get("sx_content", "")),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def preview_data(self, session, *, slug=None, **kw):
|
async def preview_data(self, session, *, slug=None, **kw):
|
||||||
|
|||||||
@@ -206,7 +206,7 @@
|
|||||||
(when is-admin
|
(when is-admin
|
||||||
(~blog-snippet-visibility-select
|
(~blog-snippet-visibility-select
|
||||||
:patch-url (get s "patch_url")
|
:patch-url (get s "patch_url")
|
||||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
:hx-headers {:X-CSRFToken csrf}
|
||||||
:options (<>
|
:options (<>
|
||||||
(~blog-snippet-option :value "private" :selected (= vis "private") :label "private")
|
(~blog-snippet-option :value "private" :selected (= vis "private") :label "private")
|
||||||
(~blog-snippet-option :value "shared" :selected (= vis "shared") :label "shared")
|
(~blog-snippet-option :value "shared" :selected (= vis "shared") :label "shared")
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
:trigger-target "#snippets-list"
|
:trigger-target "#snippets-list"
|
||||||
:title "Delete snippet?"
|
:title "Delete snippet?"
|
||||||
:text (str "Delete \u201c" name "\u201d?")
|
:text (str "Delete \u201c" name "\u201d?")
|
||||||
:sx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
:sx-headers {:X-CSRFToken csrf}
|
||||||
:cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0"))))))
|
:cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0"))))))
|
||||||
(or snippets (list)))))))
|
(or snippets (list)))))))
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
:edit-url (get mi "edit_url")
|
:edit-url (get mi "edit_url")
|
||||||
:delete-url (get mi "delete_url")
|
:delete-url (get mi "delete_url")
|
||||||
:confirm-text (str "Remove " (get mi "label") " from the menu?")
|
:confirm-text (str "Remove " (get mi "label") " from the menu?")
|
||||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")))
|
:hx-headers {:X-CSRFToken csrf}))
|
||||||
(or menu-items (list)))))))
|
(or menu-items (list)))))))
|
||||||
|
|
||||||
;; Tag Groups — receives serialized tag group data from service
|
;; Tag Groups — receives serialized tag group data from service
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
(defcomp ~blog-like-button (&key like-url hx-headers heart)
|
(defcomp ~blog-like-button (&key like-url hx-headers heart)
|
||||||
(div :class "absolute top-20 right-2 z-10 text-6xl md:text-4xl"
|
(div :class "absolute top-20 right-2 z-10 text-6xl md:text-4xl"
|
||||||
(button :sx-post like-url :sx-swap "outerHTML"
|
(~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart)))
|
||||||
:sx-headers hx-headers :class "cursor-pointer" heart)))
|
|
||||||
|
|
||||||
(defcomp ~blog-draft-status (&key publish-requested timestamp)
|
(defcomp ~blog-draft-status (&key publish-requested timestamp)
|
||||||
(<> (div :class "flex justify-center gap-2 mt-1"
|
(<> (div :class "flex justify-center gap-2 mt-1"
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
(when has-like
|
(when has-like
|
||||||
(~blog-like-button
|
(~blog-like-button
|
||||||
:like-url like-url
|
:like-url like-url
|
||||||
:sx-headers (str "{\"X-CSRFToken\": \"" csrf-token "\"}")
|
:hx-headers {:X-CSRFToken csrf-token}
|
||||||
:heart (if liked "❤️" "🤍")))
|
:heart (if liked "❤️" "🤍")))
|
||||||
(a :href href :sx-get href :sx-target "#main-panel"
|
(a :href href :sx-get href :sx-target "#main-panel"
|
||||||
:sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true"
|
:sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true"
|
||||||
|
|||||||
@@ -12,10 +12,13 @@
|
|||||||
(when publish-requested (span :class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-blue-100 text-blue-800" "Publish requested"))
|
(when publish-requested (span :class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-blue-100 text-blue-800" "Publish requested"))
|
||||||
edit))
|
edit))
|
||||||
|
|
||||||
|
(defcomp ~blog-like-toggle (&key like-url hx-headers heart)
|
||||||
|
(button :sx-post like-url :sx-swap "outerHTML"
|
||||||
|
:sx-headers hx-headers :class "cursor-pointer" heart))
|
||||||
|
|
||||||
(defcomp ~blog-detail-like (&key like-url hx-headers heart)
|
(defcomp ~blog-detail-like (&key like-url hx-headers heart)
|
||||||
(div :class "absolute top-2 right-2 z-10 text-8xl md:text-6xl"
|
(div :class "absolute top-2 right-2 z-10 text-8xl md:text-6xl"
|
||||||
(button :sx-post like-url :sx-swap "outerHTML"
|
(~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart)))
|
||||||
:sx-headers hx-headers :class "cursor-pointer" heart)))
|
|
||||||
|
|
||||||
(defcomp ~blog-detail-excerpt (&key excerpt)
|
(defcomp ~blog-detail-excerpt (&key excerpt)
|
||||||
(div :class "w-full text-center italic text-3xl p-2" excerpt))
|
(div :class "w-full text-center italic text-3xl p-2" excerpt))
|
||||||
@@ -55,8 +58,8 @@
|
|||||||
:like (when has-user
|
:like (when has-user
|
||||||
(~blog-detail-like
|
(~blog-detail-like
|
||||||
:like-url like-url
|
:like-url like-url
|
||||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
:hx-headers {:X-CSRFToken csrf}
|
||||||
:heart (if liked "\u2764\ufe0f" "\U0001f90d")))
|
:heart (if liked "❤️" "🤍")))
|
||||||
:excerpt (when (not (= custom-excerpt ""))
|
:excerpt (when (not (= custom-excerpt ""))
|
||||||
(~blog-detail-excerpt :excerpt custom-excerpt))
|
(~blog-detail-excerpt :excerpt custom-excerpt))
|
||||||
:at-bar (~blog-at-bar :tags tags :authors authors)))))
|
:at-bar (~blog-at-bar :tags tags :authors authors)))))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
(defcomp ~blog-features-form (&key features-url calendar-checked market-checked hs-trigger)
|
(defcomp ~blog-features-form (&key features-url calendar-checked market-checked hs-trigger)
|
||||||
(form :sx-put features-url :sx-target "#features-panel" :sx-swap "outerHTML"
|
(form :sx-put features-url :sx-target "#features-panel" :sx-swap "outerHTML"
|
||||||
:sx-headers "{\"Content-Type\": \"application/json\"}" :sx-encoding "json" :class "space-y-3"
|
:sx-headers {:Content-Type "application/json"} :sx-encoding "json" :class "space-y-3"
|
||||||
(label :class "flex items-center gap-3 cursor-pointer"
|
(label :class "flex items-center gap-3 cursor-pointer"
|
||||||
(input :type "checkbox" :name "calendar" :value "true" :checked calendar-checked
|
(input :type "checkbox" :name "calendar" :value "true" :checked calendar-checked
|
||||||
:class "h-5 w-5 rounded border-stone-300 text-blue-600 focus:ring-blue-500"
|
:class "h-5 w-5 rounded border-stone-300 text-blue-600 focus:ring-blue-500"
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
(~blog-associated-entry
|
(~blog-associated-entry
|
||||||
:confirm-text (get e "confirm_text")
|
:confirm-text (get e "confirm_text")
|
||||||
:toggle-url (get e "toggle_url")
|
:toggle-url (get e "toggle_url")
|
||||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
:hx-headers {:X-CSRFToken csrf}
|
||||||
:img (~blog-entry-image :src (get e "cal_image") :title (get e "cal_title"))
|
:img (~blog-entry-image :src (get e "cal_image") :title (get e "cal_title"))
|
||||||
:name (get e "name")
|
:name (get e "name")
|
||||||
:date-str (get e "date_str")))
|
:date-str (get e "date_str")))
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str:
|
|||||||
cal_name = getattr(cal, "name", "")
|
cal_name = getattr(cal, "name", "")
|
||||||
href = prefix + url_for("calendar.get", calendar_slug=cal_slug)
|
href = prefix + url_for("calendar.get", calendar_slug=cal_slug)
|
||||||
del_url = url_for("calendar.delete", calendar_slug=cal_slug)
|
del_url = url_for("calendar.delete", calendar_slug=cal_slug)
|
||||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
csrf_hdr = {"X-CSRFToken": csrf}
|
||||||
parts.append(sx_call("crud-item",
|
parts.append(sx_call("crud-item",
|
||||||
href=href, name=cal_name, slug=cal_slug,
|
href=href, name=cal_name, slug=cal_slug,
|
||||||
del_url=del_url, csrf_hdr=csrf_hdr,
|
del_url=del_url, csrf_hdr=csrf_hdr,
|
||||||
@@ -656,7 +656,7 @@ def _markets_list_html(ctx: dict, markets: list) -> str:
|
|||||||
m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "")
|
m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "")
|
||||||
market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
|
market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
|
||||||
del_url = url_for("markets.delete_market", market_slug=m_slug)
|
del_url = url_for("markets.delete_market", market_slug=m_slug)
|
||||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
csrf_hdr = {"X-CSRFToken": csrf}
|
||||||
parts.append(sx_call("crud-item",
|
parts.append(sx_call("crud-item",
|
||||||
href=market_href, name=m_name,
|
href=market_href, name=m_name,
|
||||||
slug=m_slug, del_url=del_url,
|
slug=m_slug, del_url=del_url,
|
||||||
|
|||||||
@@ -552,7 +552,7 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) ->
|
|||||||
items += sx_call("events-entry-post-item",
|
items += sx_call("events-entry-post-item",
|
||||||
img=img_html, title=ep_title,
|
img=img_html, title=ep_title,
|
||||||
del_url=del_url, entry_id=eid_s,
|
del_url=del_url, entry_id=eid_s,
|
||||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
csrf_hdr={"X-CSRFToken": csrf})
|
||||||
posts_html = sx_call("events-entry-posts-list", items=SxExpr(items))
|
posts_html = sx_call("events-entry-posts-list", items=SxExpr(items))
|
||||||
else:
|
else:
|
||||||
posts_html = sx_call("events-entry-posts-none")
|
posts_html = sx_call("events-entry-posts-none")
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ async def _h_slots_data(calendar_slug=None, **kw) -> dict:
|
|||||||
csrf = generate_csrf_token()
|
csrf = generate_csrf_token()
|
||||||
cal_slug = getattr(calendar, "slug", "")
|
cal_slug = getattr(calendar, "slug", "")
|
||||||
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
||||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
csrf_hdr = {"X-CSRFToken": csrf}
|
||||||
add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)
|
add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)
|
||||||
|
|
||||||
slots_list = []
|
slots_list = []
|
||||||
@@ -624,7 +624,7 @@ async def _h_ticket_types_data(calendar_slug=None, entry_id=None,
|
|||||||
cal_slug = getattr(calendar, "slug", "")
|
cal_slug = getattr(calendar, "slug", "")
|
||||||
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
||||||
eid = entry.id if entry else 0
|
eid = entry.id if entry else 0
|
||||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
csrf_hdr = {"X-CSRFToken": csrf}
|
||||||
|
|
||||||
types_list = []
|
types_list = []
|
||||||
for tt in (ticket_types or []):
|
for tt in (ticket_types or []):
|
||||||
@@ -964,7 +964,7 @@ async def _h_markets_data(**kw) -> dict:
|
|||||||
|
|
||||||
post = ctx.get("post") or {}
|
post = ctx.get("post") or {}
|
||||||
slug = post.get("slug", "")
|
slug = post.get("slug", "")
|
||||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
csrf_hdr = {"X-CSRFToken": csrf}
|
||||||
|
|
||||||
markets_list = []
|
markets_list = []
|
||||||
for m in markets_raw:
|
for m in markets_raw:
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ def render_slots_table(slots, calendar) -> str:
|
|||||||
time_str=f"{time_start} - {time_end}",
|
time_str=f"{time_start} - {time_end}",
|
||||||
cost_str=cost_str, action_btn=action_btn,
|
cost_str=cost_str, action_btn=action_btn,
|
||||||
del_url=del_url,
|
del_url=del_url,
|
||||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
csrf_hdr={"X-CSRFToken": csrf})
|
||||||
else:
|
else:
|
||||||
rows_html = sx_call("events-slots-empty-row")
|
rows_html = sx_call("events-slots-empty-row")
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ def render_slot_add_form(calendar) -> str:
|
|||||||
|
|
||||||
post_url = url_for("calendar.slots.post", calendar_slug=cal_slug)
|
post_url = url_for("calendar.slots.post", calendar_slug=cal_slug)
|
||||||
cancel_url = url_for("calendar.slots.add_button", calendar_slug=cal_slug)
|
cancel_url = url_for("calendar.slots.add_button", calendar_slug=cal_slug)
|
||||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
csrf_hdr = {"X-CSRFToken": csrf}
|
||||||
|
|
||||||
# Days checkboxes (all unchecked for add)
|
# Days checkboxes (all unchecked for add)
|
||||||
day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
|
day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -
|
|||||||
tt_name=tt.name, cost_str=cost_str,
|
tt_name=tt.name, cost_str=cost_str,
|
||||||
count=str(tt.count), action_btn=action_btn,
|
count=str(tt.count), action_btn=action_btn,
|
||||||
del_url=del_url,
|
del_url=del_url,
|
||||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
csrf_hdr={"X-CSRFToken": csrf})
|
||||||
else:
|
else:
|
||||||
rows_html = sx_call("events-ticket-types-empty-row")
|
rows_html = sx_call("events-ticket-types-empty-row")
|
||||||
|
|
||||||
@@ -699,7 +699,7 @@ def render_ticket_type_add_form(entry, calendar, day, month, year) -> str:
|
|||||||
cancel_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.add_button",
|
cancel_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.add_button",
|
||||||
calendar_slug=cal_slug, entry_id=entry.id,
|
calendar_slug=cal_slug, entry_id=entry.id,
|
||||||
year=year, month=month, day=day)
|
year=year, month=month, day=day)
|
||||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
csrf_hdr = {"X-CSRFToken": csrf}
|
||||||
|
|
||||||
return sx_call("events-ticket-type-add-form",
|
return sx_call("events-ticket-type-add-form",
|
||||||
post_url=post_url, csrf=csrf_hdr,
|
post_url=post_url, csrf=csrf_hdr,
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ async def _h_page_admin_data(slug=None, **kw) -> dict:
|
|||||||
m_name = getattr(m, "name", "") or (m.get("name", "") if isinstance(m, dict) else "")
|
m_name = getattr(m, "name", "") or (m.get("name", "") if isinstance(m, dict) else "")
|
||||||
href = prefix + f"/{post_slug}/{m_slug}/"
|
href = prefix + f"/{post_slug}/{m_slug}/"
|
||||||
del_url = url_for("page_admin.delete_market", market_slug=m_slug)
|
del_url = url_for("page_admin.delete_market", market_slug=m_slug)
|
||||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
csrf_hdr = {"X-CSRFToken": csrf}
|
||||||
markets.append({
|
markets.append({
|
||||||
"href": href, "name": m_name, "slug": m_slug,
|
"href": href, "name": m_name, "slug": m_slug,
|
||||||
"del-url": del_url, "csrf-hdr": csrf_hdr,
|
"del-url": del_url, "csrf-hdr": csrf_hdr,
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ def render_like_toggle_button(slug: str, liked: bool, *,
|
|||||||
return sx_call(
|
return sx_call(
|
||||||
"market-like-toggle-button",
|
"market-like-toggle-button",
|
||||||
colour=colour, action=like_url,
|
colour=colour, action=like_url,
|
||||||
hx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
hx_headers={"X-CSRFToken": csrf},
|
||||||
label=label, icon_cls=icon,
|
label=label, icon_cls=icon,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,18 @@
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Serialize a JS object as SX dict {:key "val" ...} for attribute values. */
|
||||||
|
function _serializeDict(obj) {
|
||||||
|
var parts = [];
|
||||||
|
for (var k in obj) {
|
||||||
|
if (!obj.hasOwnProperty(k)) continue;
|
||||||
|
var v = obj[k];
|
||||||
|
var vs = typeof v === "string" ? '"' + v.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"' : String(v);
|
||||||
|
parts.push(":" + k + " " + vs);
|
||||||
|
}
|
||||||
|
return "{" + parts.join(" ") + "}";
|
||||||
|
}
|
||||||
|
|
||||||
// --- Primitives ---
|
// --- Primitives ---
|
||||||
|
|
||||||
var PRIMITIVES = {};
|
var PRIMITIVES = {};
|
||||||
@@ -1420,7 +1432,7 @@
|
|||||||
} else if (attrVal === true) {
|
} else if (attrVal === true) {
|
||||||
el.setAttribute(attrName, "");
|
el.setAttribute(attrName, "");
|
||||||
} else {
|
} else {
|
||||||
el.setAttribute(attrName, String(attrVal));
|
el.setAttribute(attrName, typeof attrVal === "object" && attrVal !== null && !Array.isArray(attrVal) ? _serializeDict(attrVal) : String(attrVal));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Child
|
// Child
|
||||||
@@ -1851,7 +1863,7 @@
|
|||||||
cancelButtonText: "Cancel"
|
cancelButtonText: "Cancel"
|
||||||
}).then(function (result) {
|
}).then(function (result) {
|
||||||
if (!result.isConfirmed) return;
|
if (!result.isConfirmed) return;
|
||||||
return _doFetch(el, method, url, extraParams);
|
return _doFetch(el, verbInfo, method, url, extraParams);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!window.confirm(confirmMsg)) return Promise.resolve();
|
if (!window.confirm(confirmMsg)) return Promise.resolve();
|
||||||
@@ -1866,10 +1878,10 @@
|
|||||||
extraParams.promptValue = promptVal;
|
extraParams.promptValue = promptVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _doFetch(el, method, url, extraParams);
|
return _doFetch(el, verbInfo, method, url, extraParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _doFetch(el, method, url, extraParams) {
|
function _doFetch(el, verbInfo, method, url, extraParams) {
|
||||||
// sx-sync: abort previous
|
// sx-sync: abort previous
|
||||||
var sync = el.getAttribute("sx-sync");
|
var sync = el.getAttribute("sx-sync");
|
||||||
if (sync && sync.indexOf("replace") >= 0) abortPrevious(el);
|
if (sync && sync.indexOf("replace") >= 0) abortPrevious(el);
|
||||||
@@ -1895,12 +1907,12 @@
|
|||||||
var cssHeader = _getSxCssHeader();
|
var cssHeader = _getSxCssHeader();
|
||||||
if (cssHeader) headers["SX-Css"] = cssHeader;
|
if (cssHeader) headers["SX-Css"] = cssHeader;
|
||||||
|
|
||||||
// Extra headers from sx-headers
|
// Extra headers from sx-headers (SX dict {:key "val"} or JSON)
|
||||||
var extraH = el.getAttribute("sx-headers");
|
var extraH = el.getAttribute("sx-headers");
|
||||||
if (extraH) {
|
if (extraH) {
|
||||||
try {
|
try {
|
||||||
var parsed = JSON.parse(extraH);
|
var parsed = extraH.charAt(0) === "{" && extraH.charAt(1) === ":" ? parse(extraH) : JSON.parse(extraH);
|
||||||
for (var k in parsed) headers[k] = parsed[k];
|
for (var k in parsed) headers[k] = String(parsed[k]);
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1974,7 +1986,7 @@
|
|||||||
var valsAttr = el.getAttribute("sx-vals");
|
var valsAttr = el.getAttribute("sx-vals");
|
||||||
if (valsAttr) {
|
if (valsAttr) {
|
||||||
try {
|
try {
|
||||||
var vals = JSON.parse(valsAttr);
|
var vals = valsAttr.charAt(0) === "{" && valsAttr.charAt(1) === ":" ? parse(valsAttr) : JSON.parse(valsAttr);
|
||||||
if (method === "GET") {
|
if (method === "GET") {
|
||||||
for (var vk in vals) {
|
for (var vk in vals) {
|
||||||
url += (url.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(vk) + "=" + encodeURIComponent(vals[vk]);
|
url += (url.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(vk) + "=" + encodeURIComponent(vals[vk]);
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ def _render(expr: Any, env: dict[str, Any]) -> str:
|
|||||||
return ""
|
return ""
|
||||||
return _render_list(expr, env)
|
return _render_list(expr, env)
|
||||||
|
|
||||||
# --- dict → skip (data, not renderable) -------------------------------
|
# --- dict → skip (data, not renderable as HTML content) -----------------
|
||||||
if isinstance(expr, dict):
|
if isinstance(expr, dict):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -540,6 +540,9 @@ def _render_element(tag: str, args: list, env: dict[str, Any]) -> str:
|
|||||||
parts.append(f" {attr_name}")
|
parts.append(f" {attr_name}")
|
||||||
elif attr_val is True:
|
elif attr_val is True:
|
||||||
parts.append(f" {attr_name}")
|
parts.append(f" {attr_name}")
|
||||||
|
elif isinstance(attr_val, dict):
|
||||||
|
from .parser import serialize as _sx_serialize
|
||||||
|
parts.append(f' {attr_name}="{escape_attr(_sx_serialize(attr_val))}"')
|
||||||
else:
|
else:
|
||||||
parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
|
parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
|
||||||
parts.append(">")
|
parts.append(">")
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ from typing import Any
|
|||||||
|
|
||||||
from .jinja_bridge import sx
|
from .jinja_bridge import sx
|
||||||
|
|
||||||
SEARCH_HEADERS_MOBILE = '{"X-Origin":"search-mobile","X-Search":"true"}'
|
SEARCH_HEADERS_MOBILE = {"X-Origin": "search-mobile", "X-Search": "true"}
|
||||||
SEARCH_HEADERS_DESKTOP = '{"X-Origin":"search-desktop","X-Search":"true"}'
|
SEARCH_HEADERS_DESKTOP = {"X-Origin": "search-desktop", "X-Search": "true"}
|
||||||
|
|
||||||
|
|
||||||
def render_page(source: str, **kwargs: Any) -> str:
|
def render_page(source: str, **kwargs: Any) -> str:
|
||||||
|
|||||||
@@ -277,7 +277,7 @@
|
|||||||
:description "sx-vals adds extra key/value pairs to the request parameters. sx-headers adds custom HTTP headers. The server echoes back what it received."
|
:description "sx-vals adds extra key/value pairs to the request parameters. sx-headers adds custom HTTP headers. The server echoes back what it received."
|
||||||
:demo-description "Click each button to see what the server receives."
|
:demo-description "Click each button to see what the server receives."
|
||||||
:demo (~vals-headers-demo)
|
:demo (~vals-headers-demo)
|
||||||
:sx-code ";; Send extra values with the request\n(button\n :sx-get \"/examples/api/echo-vals\"\n :sx-vals \"{\\\"source\\\": \\\"button\\\"}\"\n \"Send with vals\")\n\n;; Send custom headers\n(button\n :sx-get \"/examples/api/echo-headers\"\n :sx-headers \"{\\\"X-Custom-Token\\\": \\\"abc123\\\"}\"\n \"Send with headers\")"
|
:sx-code ";; Send extra values with the request\n(button\n :sx-get \"/examples/api/echo-vals\"\n :sx-vals \"{\\\"source\\\": \\\"button\\\"}\"\n \"Send with vals\")\n\n;; Send custom headers\n(button\n :sx-get \"/examples/api/echo-headers\"\n :sx-headers {:X-Custom-Token \"abc123\"}\n \"Send with headers\")"
|
||||||
:handler-code "@bp.get(\"/examples/api/echo-vals\")\nasync def api_echo_vals():\n vals = dict(request.args)\n return sx_response(\n f'(~echo-result :label \"values\" :items (...))')\n\n@bp.get(\"/examples/api/echo-headers\")\nasync def api_echo_headers():\n custom = {k: v for k, v in request.headers\n if k.startswith(\"X-\")}\n return sx_response(\n f'(~echo-result :label \"headers\" :items (...))')"
|
:handler-code "@bp.get(\"/examples/api/echo-vals\")\nasync def api_echo_vals():\n vals = dict(request.args)\n return sx_response(\n f'(~echo-result :label \"values\" :items (...))')\n\n@bp.get(\"/examples/api/echo-headers\")\nasync def api_echo_headers():\n custom = {k: v for k, v in request.headers\n if k.startswith(\"X-\")}\n return sx_response(\n f'(~echo-result :label \"headers\" :items (...))')"
|
||||||
:comp-placeholder-id "vals-comp"
|
:comp-placeholder-id "vals-comp"
|
||||||
:wire-placeholder-id "vals-wire"))
|
:wire-placeholder-id "vals-wire"))
|
||||||
|
|||||||
@@ -705,7 +705,7 @@
|
|||||||
:sx-get "/examples/api/echo-headers"
|
:sx-get "/examples/api/echo-headers"
|
||||||
:sx-target "#headers-result"
|
:sx-target "#headers-result"
|
||||||
:sx-swap "innerHTML"
|
:sx-swap "innerHTML"
|
||||||
:sx-headers "{\"X-Custom-Token\": \"abc123\", \"X-Request-Source\": \"demo\"}"
|
:sx-headers {:X-Custom-Token "abc123" :X-Request-Source "demo"}
|
||||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||||
"Send with headers")
|
"Send with headers")
|
||||||
(div :id "headers-result" :class "p-3 rounded bg-stone-50 text-sm text-stone-400"
|
(div :id "headers-result" :class "p-3 rounded bg-stone-50 text-sm text-stone-400"
|
||||||
|
|||||||
@@ -272,7 +272,7 @@
|
|||||||
(defcomp ~ref-headers-demo ()
|
(defcomp ~ref-headers-demo ()
|
||||||
(div :class "space-y-3"
|
(div :class "space-y-3"
|
||||||
(button :sx-get "/reference/api/echo-headers"
|
(button :sx-get "/reference/api/echo-headers"
|
||||||
:sx-headers "{\"X-Custom-Token\": \"abc123\", \"X-Request-Source\": \"demo\"}"
|
:sx-headers {:X-Custom-Token "abc123" :X-Request-Source "demo"}
|
||||||
:sx-target "#ref-headers-result"
|
:sx-target "#ref-headers-result"
|
||||||
:sx-swap "innerHTML"
|
:sx-swap "innerHTML"
|
||||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||||
|
|||||||
Reference in New Issue
Block a user