host: relate/unrelate keep both lists in sync (add to current list, never blank the picker)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Two reported bugs on the edit page's relation editor:
1. relating a candidate didn't add it to the current-relations list (the AJAX
relate just deleted the candidate row; the relation only showed after a reload);
2. removing a relation could blank the relate picker.
Fix (lib/host/blog.sx): both the candidate's relate form and a current relation's
remove form now target #rel-editor-<kind> with sx-swap=outerHTML, and the
relate/unrelate handlers return the re-rendered editor for that kind (current list +
a fresh picker). So one swap keeps BOTH lists in sync: the related post moves into
the current list and out of the (re-loaded) candidate pool; removing moves it back.
Gated on the SX-Target header, so a plain boosted form / no-JS POST (the is-a-tag
toggle) still redirects + re-renders #content.
Engine fix (web/orchestration.sx): handle-html-response's non-select branch called
post-swap on the OLD target, which an outerHTML swap has already REPLACED — so the
swapped-in content's triggers (here the re-rendered picker's "load") never bound and
the picker stayed empty. post-swap the swap result (the new node), mirroring the
sx-select branch. Recompiled orchestration.sxbc for the content-addressed client.
Tests:
- web/tests/test-relate-picker.sx: relating re-syncs the editor (post in current
list + picker re-loads); removing does likewise — both fail without the engine fix.
- lib/host/tests/blog.sx: relate/unrelate return the re-rendered editor fragment
(200, #rel-editor + picker), forms wire to #rel-editor-KIND/outerHTML, plain
boosted POST still 303.
- relate-picker.spec.js: the full in-page flow (relate adds to list, remove keeps
the picker, no reload) + persistence.
Verified: host conformance 277/277, web engine suite 8/8, run-picker-check 3/3,
run-spa-check 3/3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -364,16 +364,18 @@
|
||||
(quasiquote
|
||||
(li :id (unquote (str "cand-" kind "-" (get p :slug)))
|
||||
:style "border-bottom:1px solid #eee"
|
||||
;; AJAX relate: sx-post the relation, then sx-swap="delete" removes THIS
|
||||
;; candidate row (its sx-target) — it's now related, so it leaves the pool
|
||||
;; without a reload or a list refetch. method+action stay for the no-JS
|
||||
;; fallback (plain POST -> 303 -> reload); the engine prevents the double
|
||||
;; submit when it handles sx-post.
|
||||
;; AJAX relate: sx-post the relation, then sx-swap="outerHTML" re-renders the
|
||||
;; WHOLE relation editor for this kind (its sx-target #rel-editor-KIND) — the
|
||||
;; just-related post moves into the current-relations list and out of the
|
||||
;; candidate pool, and the fresh picker re-loads its candidates. (A bare
|
||||
;; delete of this row added the relation server-side but never showed it in
|
||||
;; the current list; re-rendering the editor keeps BOTH lists in sync.)
|
||||
;; method+action stay for the no-JS fallback (plain POST -> 303 -> reload).
|
||||
(form :method "post" :style "margin:0"
|
||||
:action (unquote (str "/" slug "/relate"))
|
||||
:sx-post (unquote (str "/" slug "/relate"))
|
||||
:sx-target (unquote (str "#cand-" kind "-" (get p :slug)))
|
||||
:sx-swap "delete"
|
||||
:sx-target (unquote (str "#rel-editor-" kind))
|
||||
:sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "other" :value (unquote (get p :slug)))
|
||||
(input :type "hidden" :name "kind" :value (unquote kind))
|
||||
(button :type "submit"
|
||||
@@ -434,11 +436,12 @@
|
||||
;; so the blog degrades gracefully to plain server-rendered pages.
|
||||
(define host/blog--spa-req? (fn (req) (= (dream-header req "sx-request") "true")))
|
||||
|
||||
;; An AJAX in-place row delete vs. a plain boosted form. The engine sends an
|
||||
;; SX-Target header for an sx-post form (the relation-editor's remove button,
|
||||
;; sx-target=#cur-…), but NOT for a plain boosted form (the is-a-tag toggle). So
|
||||
;; this tells "delete just this row, empty 200" apart from "redirect + re-render".
|
||||
(define host/blog--row-swap-req?
|
||||
;; An AJAX editor swap (the picker's relate / the editor's remove) vs. a plain
|
||||
;; boosted form. The engine sends an SX-Target header for an sx-post form
|
||||
;; (sx-target=#rel-editor-…), but NOT for a plain boosted form (the is-a-tag
|
||||
;; toggle). So this tells "return the re-rendered editor fragment" apart from
|
||||
;; "redirect + re-render #content" (the toggle / no-JS path).
|
||||
(define host/blog--editor-swap-req?
|
||||
(fn (req)
|
||||
(and (host/blog--spa-req? req)
|
||||
(let ((t (dream-header req "sx-target"))) (and t (not (= t "")))))))
|
||||
@@ -591,25 +594,30 @@
|
||||
(let ((spec (host/blog--kind-spec kind))
|
||||
(current (host/blog-out slug kind)))
|
||||
(quasiquote
|
||||
(div :style "margin-top:2em;border-top:1px solid #ccc;padding-top:1em"
|
||||
;; #rel-editor-KIND wraps the WHOLE editor (current list + picker) so relate
|
||||
;; and unrelate can re-render it with one outerHTML swap — keeping the two
|
||||
;; lists in sync. The fresh picker re-loads its candidates (an explicit
|
||||
;; outerHTML swap installs a NEW form the engine binds, unlike the old
|
||||
;; redirect that morphed the stale picker and left it empty).
|
||||
(div :id (unquote (str "rel-editor-" kind))
|
||||
:style "margin-top:2em;border-top:1px solid #ccc;padding-top:1em"
|
||||
(h3 (unquote (get spec :label)))
|
||||
(unquote
|
||||
(if (> (len current) 0)
|
||||
(cons (quote ul)
|
||||
(map (fn (s)
|
||||
(quasiquote
|
||||
;; AJAX unrelate: sx-post the removal, then sx-swap="delete"
|
||||
;; removes just THIS current-relation row (its sx-target) in
|
||||
;; place — NO #content re-render, so the relate picker (the
|
||||
;; "posts to relate" list) is left untouched. method+action
|
||||
;; stay for the no-JS fallback (plain POST -> 303 -> reload).
|
||||
(li :id (unquote (str "cur-" kind "-" s))
|
||||
(a :href (unquote (str "/" s "/")) (unquote s)) " "
|
||||
;; remove: sx-post the unrelate, then sx-swap="outerHTML"
|
||||
;; re-renders this kind's editor (its sx-target
|
||||
;; #rel-editor-KIND) — the row leaves the current list and
|
||||
;; the post returns to the candidate pool, both in sync,
|
||||
;; with the picker NOT cleared. method+action stay for no-JS.
|
||||
(li (a :href (unquote (str "/" s "/")) (unquote s)) " "
|
||||
(form :method "post" :style "display:inline"
|
||||
:action (unquote (str "/" slug "/unrelate"))
|
||||
:sx-post (unquote (str "/" slug "/unrelate"))
|
||||
:sx-target (unquote (str "#cur-" kind "-" s))
|
||||
:sx-swap "delete"
|
||||
:sx-target (unquote (str "#rel-editor-" kind))
|
||||
:sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "other" :value (unquote s))
|
||||
(input :type "hidden" :name "kind" :value (unquote kind))
|
||||
(button :type "submit" "remove")))))
|
||||
@@ -889,11 +897,13 @@
|
||||
(when (and other (not (= other "")) (not (= other slug))
|
||||
(host/blog--kind-spec kind) (host/blog-exists? other))
|
||||
(host/blog-relate! slug other kind))
|
||||
;; AJAX (the picker's sx-post): return an empty 200 so the candidate
|
||||
;; form's sx-swap="delete" removes just that one row — no full reload,
|
||||
;; no candidate-list refetch. Plain POST (no-JS) still redirects.
|
||||
(if (host/blog--spa-req? req)
|
||||
(dream-html "")
|
||||
;; AJAX (the picker's sx-post, carries SX-Target): return the re-rendered
|
||||
;; editor for this kind so its sx-swap="outerHTML" replaces #rel-editor-KIND
|
||||
;; — the just-related post shows in the current list and the picker refreshes
|
||||
;; its candidates. text/html so the client's DOMParser swap path renders the
|
||||
;; already-expanded fragment. Plain boosted form / no-JS still redirects.
|
||||
(if (host/blog--editor-swap-req? req)
|
||||
(dream-html (render-page (host/blog--relation-editor slug kind)))
|
||||
(dream-redirect (str "/" slug "/edit"))))))))
|
||||
|
||||
;; POST /<slug>/unrelate — remove the relation to `other` under `kind` (default
|
||||
@@ -906,12 +916,13 @@
|
||||
(begin
|
||||
(when (and other (not (= other "")) (host/blog--kind-spec kind))
|
||||
(host/blog-unrelate! slug other kind))
|
||||
;; AJAX in-place remove (the editor's sx-post, carries SX-Target): return an
|
||||
;; empty 200 so the current-relation row's sx-swap="delete" removes just that
|
||||
;; one row — no #content re-render, so the relate picker is untouched. A plain
|
||||
;; boosted form (the tag toggle) or a no-JS POST still redirects + re-renders.
|
||||
(if (host/blog--row-swap-req? req)
|
||||
(dream-html "")
|
||||
;; AJAX remove (the editor's sx-post, carries SX-Target): return the
|
||||
;; re-rendered editor for this kind so its sx-swap="outerHTML" replaces
|
||||
;; #rel-editor-KIND — the row leaves the current list, the post returns to the
|
||||
;; (re-loaded) candidate pool, and the picker is NOT cleared. A plain boosted
|
||||
;; form (the tag toggle) or a no-JS POST still redirects + re-renders #content.
|
||||
(if (host/blog--editor-swap-req? req)
|
||||
(dream-html (render-page (host/blog--relation-editor slug kind)))
|
||||
(dream-redirect (str "/" slug "/edit")))))))
|
||||
|
||||
;; GET /<slug>/edit — edit form pre-filled with the post's current title, raw
|
||||
|
||||
Reference in New Issue
Block a user