Add SX editor to post edit page, prevent sx_content clearing on save
- Add sx_content to _post_to_edit_dict so edit page receives existing content - Add SX/Koenig editor tabs, sx-editor mount point, and SxEditor.mount init - Only pass sx_content to writer_update when form field is present (prevents accidental clearing when editing via Koenig-only path) - Add csrf_exempt to example API POST/DELETE/PUT demo endpoints - Add defpage infrastructure (pages.py, layouts.py) and sx docs page definitions - Add defhandler definitions for example API handlers (examples.sx) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ def _post_to_edit_dict(post) -> dict:
|
||||
d: dict = {}
|
||||
for col in (
|
||||
"id", "slug", "title", "html", "plaintext", "lexical", "mobiledoc",
|
||||
"sx_content",
|
||||
"feature_image", "feature_image_alt", "feature_image_caption",
|
||||
"excerpt", "custom_excerpt", "visibility", "status", "featured",
|
||||
"is_page", "email_only", "canonical_url",
|
||||
@@ -595,6 +596,10 @@ def register():
|
||||
effective_status = status
|
||||
|
||||
sx_content_raw = form.get("sx_content", "").strip() or None
|
||||
# Build optional kwargs — only pass sx_content if the form field was present
|
||||
extra_kw: dict = {}
|
||||
if "sx_content" in form:
|
||||
extra_kw["sx_content"] = sx_content_raw
|
||||
try:
|
||||
post = await writer_update(
|
||||
g.s,
|
||||
@@ -606,7 +611,7 @@ def register():
|
||||
custom_excerpt=custom_excerpt or None,
|
||||
feature_image_caption=feature_image_caption or None,
|
||||
status=effective_status,
|
||||
sx_content=sx_content_raw,
|
||||
**extra_kw,
|
||||
)
|
||||
except OptimisticLockError:
|
||||
return redirect(
|
||||
|
||||
@@ -1755,6 +1755,7 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
||||
asset_url_fn = current_app.jinja_env.globals.get("asset_url", lambda p: "")
|
||||
editor_css = asset_url_fn("scripts/editor.css")
|
||||
editor_js = asset_url_fn("scripts/editor.js")
|
||||
sx_editor_js = asset_url_fn("scripts/sx-editor.js")
|
||||
|
||||
upload_image_url = qurl("blog.editor_api.upload_image")
|
||||
upload_media_url = qurl("blog.editor_api.upload_media")
|
||||
@@ -1773,6 +1774,7 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
||||
updated_at = esc(ghost_post.get("updated_at") or "")
|
||||
status = ghost_post.get("status") or "draft"
|
||||
lexical_json = ghost_post.get("lexical") or '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'
|
||||
sx_content = ghost_post.get("sx_content") or ""
|
||||
|
||||
already_emailed = bool(ghost_post and ghost_post.get("email") and (ghost_post["email"] if isinstance(ghost_post["email"], dict) else {}).get("status"))
|
||||
# For ORM objects the email may be an object
|
||||
@@ -1799,6 +1801,7 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
||||
form_parts.append(f'<input type="hidden" name="csrf_token" value="{csrf}">')
|
||||
form_parts.append(f'<input type="hidden" name="updated_at" value="{updated_at}">')
|
||||
form_parts.append('<input type="hidden" id="lexical-json-input" name="lexical" value="">')
|
||||
form_parts.append(f'<input type="hidden" id="sx-content-input" name="sx_content" value="{esc(sx_content)}">')
|
||||
form_parts.append(f'<input type="hidden" id="feature-image-input" name="feature_image" value="{esc(feature_image)}">')
|
||||
form_parts.append(f'<input type="hidden" id="feature-image-caption-input" name="feature_image_caption" value="{esc(feature_image_caption)}">')
|
||||
|
||||
@@ -1830,8 +1833,24 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
||||
f' class="w-full text-[18px] text-stone-500 bg-transparent border-none outline-none placeholder:text-stone-300 resize-none mb-[24px] leading-relaxed">{excerpt_val}</textarea>'
|
||||
)
|
||||
|
||||
# Editor mount point
|
||||
form_parts.append('<div id="lexical-editor" class="relative w-full bg-transparent"></div>')
|
||||
# Editor tabs: SX (primary) and Koenig (legacy)
|
||||
has_sx = bool(sx_content)
|
||||
sx_active = 'text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent'
|
||||
sx_inactive = 'text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600'
|
||||
form_parts.append(
|
||||
'<div class="flex gap-[4px] mb-[8px] border-b border-stone-200">'
|
||||
f'<button type="button" id="editor-tab-sx" class="px-[12px] py-[6px] text-[13px] font-medium {sx_active if has_sx else sx_inactive}"'
|
||||
""" onclick="document.getElementById('sx-editor').style.display='block';document.getElementById('lexical-editor').style.display='none';this.className='px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent';document.getElementById('editor-tab-koenig').className='px-[12px] py-[6px] text-[13px] font-medium text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600'" """
|
||||
'>SX Editor</button>'
|
||||
f'<button type="button" id="editor-tab-koenig" class="px-[12px] py-[6px] text-[13px] font-medium {sx_inactive if has_sx else sx_active}"'
|
||||
""" onclick="document.getElementById('lexical-editor').style.display='block';document.getElementById('sx-editor').style.display='none';this.className='px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent';document.getElementById('editor-tab-sx').className='px-[12px] py-[6px] text-[13px] font-medium text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600'" """
|
||||
'>Koenig (Legacy)</button>'
|
||||
'</div>'
|
||||
)
|
||||
# SX editor mount point
|
||||
form_parts.append(f'<div id="sx-editor" class="relative w-full bg-transparent" style="{"" if has_sx else "display:none"}"></div>')
|
||||
# Koenig editor mount point
|
||||
form_parts.append(f'<div id="lexical-editor" class="relative w-full bg-transparent" style="{"display:none" if has_sx else ""}"></div>')
|
||||
|
||||
# Initial lexical JSON
|
||||
form_parts.append(f'<script id="lexical-initial-data" type="application/json">{lexical_json}</script>')
|
||||
@@ -1902,8 +1921,10 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
||||
'</script>'
|
||||
)
|
||||
|
||||
# Editor CSS + styles
|
||||
# Editor CSS + styles + SX editor styles
|
||||
from shared.sx.helpers import sx_call
|
||||
parts.append(f'<link rel="stylesheet" href="{esc(editor_css)}">')
|
||||
parts.append(sx_call("sx-editor-styles"))
|
||||
parts.append(
|
||||
'<style>'
|
||||
'#lexical-editor { display: flow-root; }'
|
||||
@@ -1912,8 +1933,13 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
||||
'</style>'
|
||||
)
|
||||
|
||||
# Editor JS + init
|
||||
# Initial sx content for SX editor
|
||||
sx_initial_escaped = sx_content.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n').replace('\r', '')
|
||||
parts.append(f"<script>window.__SX_INITIAL__ = '{sx_initial_escaped}' || null;</script>")
|
||||
|
||||
# Editor JS + SX editor JS + init
|
||||
parts.append(f'<script src="{esc(editor_js)}"></script>')
|
||||
parts.append(f'<script src="{esc(sx_editor_js)}"></script>')
|
||||
parts.append(
|
||||
'<script>'
|
||||
'(function() {'
|
||||
@@ -1991,6 +2017,17 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
||||
f" unsplashApiKey: '{unsplash_key}',"
|
||||
f" snippetsUrl: '{snippets_url}',"
|
||||
' });'
|
||||
" if (typeof SxEditor !== 'undefined') {"
|
||||
" SxEditor.mount('sx-editor', {"
|
||||
" initialSx: window.__SX_INITIAL__ || null,"
|
||||
' csrfToken: csrfToken,'
|
||||
' uploadUrls: uploadUrls,'
|
||||
f" oembedUrl: '{oembed_url}',"
|
||||
' onChange: function(sx) {'
|
||||
" document.getElementById('sx-content-input').value = sx;"
|
||||
' }'
|
||||
' });'
|
||||
' }'
|
||||
" document.addEventListener('keydown', function(e) {"
|
||||
" if ((e.ctrlKey || e.metaKey) && e.key === 's') {"
|
||||
" e.preventDefault(); document.getElementById('post-edit-form').requestSubmit();"
|
||||
|
||||
Reference in New Issue
Block a user