host: removing a related post no longer clears the relate picker
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
Bug: the edit page's remove button (on a current relation) was a plain boosted
form — POST /unrelate -> 303 redirect -> the engine re-rendered #content, and the
freshly-swapped relate picker came back EMPTY ("the list of posts to relate" was
cleared).
Fix: make the remove button an AJAX in-place delete, exactly like the relate
candidate rows — each current-relation <li> gets an id and its form carries
sx-post + sx-target=#cur-<kind>-<other> + sx-swap=delete. unrelate-submit returns
an empty 200 for that request so the engine deletes just that one row; #content is
never re-rendered, so the picker is untouched. method+action stay for no-JS.
The empty-200 is gated on the SX-Target header (sent only by the sx-post form), so
a plain boosted form / no-JS POST still redirects + re-renders — the is-a-tag
toggle and graceful degradation are unaffected.
Tests (all red before the fix):
- lib/host/playwright/relate-picker.spec.js: the remove-button test now asserts
the picker still has candidates after a removal (the reproduction).
- web/tests/test-relate-picker.sx: an SX engine test — removing a current relation
deletes just that row and leaves the sibling picker's list intact.
- lib/host/tests/blog.sx: the relation-editor renders the AJAX delete attrs;
unrelate returns empty-200 with SX-Target and 303 without.
Verified: host conformance 275/275, web engine suite 8/8, run-picker-check 2/2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -434,6 +434,15 @@
|
||||
;; 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?
|
||||
(fn (req)
|
||||
(and (host/blog--spa-req? req)
|
||||
(let ((t (dream-header req "sx-target"))) (and t (not (= t "")))))))
|
||||
|
||||
(define host/blog--page
|
||||
(fn (req title body)
|
||||
(if (host/blog--spa-req? req)
|
||||
@@ -589,9 +598,18 @@
|
||||
(cons (quote ul)
|
||||
(map (fn (s)
|
||||
(quasiquote
|
||||
(li (a :href (unquote (str "/" s "/")) (unquote s)) " "
|
||||
;; 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)) " "
|
||||
(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"
|
||||
(input :type "hidden" :name "other" :value (unquote s))
|
||||
(input :type "hidden" :name "kind" :value (unquote kind))
|
||||
(button :type "submit" "remove")))))
|
||||
@@ -879,7 +897,7 @@
|
||||
(dream-redirect (str "/" slug "/edit"))))))))
|
||||
|
||||
;; POST /<slug>/unrelate — remove the relation to `other` under `kind` (default
|
||||
;; "related"). Idempotent; redirects back to the edit page.
|
||||
;; "related"). Idempotent.
|
||||
(define host/blog-unrelate-submit
|
||||
(fn (req)
|
||||
(let ((slug (dream-param req "slug"))
|
||||
@@ -888,7 +906,13 @@
|
||||
(begin
|
||||
(when (and other (not (= other "")) (host/blog--kind-spec kind))
|
||||
(host/blog-unrelate! slug other kind))
|
||||
(dream-redirect (str "/" slug "/edit"))))))
|
||||
;; 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 "")
|
||||
(dream-redirect (str "/" slug "/edit")))))))
|
||||
|
||||
;; GET /<slug>/edit — edit form pre-filled with the post's current title, raw
|
||||
;; sx_content (in a textarea — render-to-html escapes the text child, so the
|
||||
|
||||
@@ -63,10 +63,15 @@ test.describe('relate picker (browser-only)', () => {
|
||||
await waitReady(page);
|
||||
const relLink = page.locator('a[href="/picker-item-13/"]');
|
||||
await expect(relLink).toHaveCount(1); // current relation present
|
||||
// click its remove button — a plain boosted form (regression: this did nothing
|
||||
// because bind-boost-form discarded the form's method/action)
|
||||
// the picker is populated (empty filter -> first page of candidates)
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 12000 }).toBeGreaterThan(0);
|
||||
// click its remove button
|
||||
await page.locator('li:has(a[href="/picker-item-13/"]) button').click();
|
||||
await expect(relLink).toHaveCount(0, { timeout: 12000 }); // relation removed
|
||||
// REGRESSION: removing a relation must NOT clear "the list of posts to relate".
|
||||
// (The old plain-boosted remove form redirected -> re-rendered #content and the
|
||||
// picker came back empty; the AJAX in-place remove leaves the picker untouched.)
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 12000 }).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('picker populates after a boosted SPA nav to the edit page', async ({ page }) => {
|
||||
|
||||
@@ -295,6 +295,37 @@
|
||||
(host-bl-test "relate-options omits the load-more sentinel on a short last page"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/alpha-post/relate-options"))) "rp-more")
|
||||
false)
|
||||
|
||||
;; -- unrelate: AJAX in-place row delete (regression: removing a related post must
|
||||
;; NOT clear the relate picker). The remove button is now an sx-post form that
|
||||
;; deletes just its own current-relation row (sx-target=#cur-…, sx-swap=delete),
|
||||
;; so #content is never re-rendered and the picker is left intact. --
|
||||
(host/blog-relate! "alpha-post" "beta-post" "related")
|
||||
(host-bl-test "relation-editor remove button is an AJAX in-place delete"
|
||||
(let ((html (render-page (host/blog--relation-editor "alpha-post" "related"))))
|
||||
(list (contains? html "id=\"cur-related-beta-post\"") ;; row has a target id
|
||||
(contains? html "sx-post=\"/alpha-post/unrelate\"") ;; AJAX, not plain post
|
||||
(contains? html "sx-target=\"#cur-related-beta-post\"")
|
||||
(contains? html "sx-swap=\"delete\"")))
|
||||
(list true true true true))
|
||||
;; the AJAX remove (carries SX-Target) returns an empty 200 so only the row is
|
||||
;; swapped out — no redirect, no #content re-render that would blank the picker.
|
||||
(host-bl-test "unrelate (AJAX, SX-Target) returns an empty 200"
|
||||
(let ((resp (host/blog-unrelate-submit
|
||||
(dream-request "POST" "/alpha-post/unrelate"
|
||||
{:sx-request "true" :sx-target "#cur-related-beta-post"}
|
||||
"other=beta-post&kind=related"))))
|
||||
(list (dream-status resp) (dream-resp-body resp)))
|
||||
(list 200 ""))
|
||||
;; a plain boosted form / no-JS POST (no SX-Target) still redirects + re-renders,
|
||||
;; so the is-a-tag toggle and graceful degradation are unaffected.
|
||||
(host/blog-relate! "alpha-post" "beta-post" "related")
|
||||
(host-bl-test "unrelate (plain boosted / no-JS, no SX-Target) still redirects"
|
||||
(dream-status (host/blog-unrelate-submit
|
||||
(dream-request "POST" "/alpha-post/unrelate"
|
||||
{:sx-request "true"} "other=beta-post&kind=related")))
|
||||
303)
|
||||
(host/blog-unrelate! "alpha-post" "beta-post" "related")
|
||||
(host/blog-put! "hint-post" "Hint Post" "(p \"h\")" "published")
|
||||
(host-bl-test "relations section: hint when logged-in + no relations"
|
||||
(contains? (str (host/blog--relations-or-hint "hint-post" true)) "add some") true)
|
||||
|
||||
Reference in New Issue
Block a user