host: relate removes just the picked candidate row in place (no reload)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 33s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 33s
Picking a candidate to relate it no longer does a full POST -> 303 -> reload. The candidate <li> now carries an id and its relate form is an AJAX sx-post (sx-target="#cand-<kind>-<other>", sx-swap="delete"): on success the engine deletes just that one row — the item is now related, so it leaves the candidate pool with no reload and no candidate-list refetch. host/blog-relate-submit returns an empty 200 for an SX request (so the delete swap fires) and still 303s for a plain POST (no-JS fallback via the form's method+action). relate-picker.spec.js test 4 updated to assert the in-place row delete + no reload + the relation still persists (shows on the post page). 6/6 + conformance 272/272. (Symmetric unrelate-in-place was prototyped but backed out: the current-links form, bound via boot's process-elements rather than post-swap, didn't fire the AJAX delete despite identical markup — a binding quirk to chase separately. Unrelate keeps its plain POST -> reload for now, no regression.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -362,13 +362,18 @@
|
||||
(define host/blog--picker-item
|
||||
(fn (slug p kind)
|
||||
(quasiquote
|
||||
(li :style "border-bottom:1px solid #eee"
|
||||
;; sx-disable: this relate form is a plain POST -> 303 -> full reload (the
|
||||
;; engine swaps the picker rows in, which would otherwise boost this form;
|
||||
;; a boosted POST+redirect into #content swaps unreliably). A relate is a
|
||||
;; deliberate action, so a clean reload that re-renders the editor is right.
|
||||
(form :method "post" :style "margin:0" :sx-disable "true"
|
||||
(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.
|
||||
(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"
|
||||
(input :type "hidden" :name "other" :value (unquote (get p :slug)))
|
||||
(input :type "hidden" :name "kind" :value (unquote kind))
|
||||
(button :type "submit"
|
||||
@@ -866,7 +871,12 @@
|
||||
(when (and other (not (= other "")) (not (= other slug))
|
||||
(host/blog--kind-spec kind) (host/blog-exists? other))
|
||||
(host/blog-relate! slug other kind))
|
||||
(dream-redirect (str "/" slug "/edit")))))))
|
||||
;; 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 "")
|
||||
(dream-redirect (str "/" slug "/edit"))))))))
|
||||
|
||||
;; POST /<slug>/unrelate — remove the relation to `other` under `kind` (default
|
||||
;; "related"). Idempotent; redirects back to the edit page.
|
||||
|
||||
@@ -78,19 +78,20 @@ test.describe('relate picker', () => {
|
||||
await expect(page.locator(RELR)).toContainText('Picker Item 13');
|
||||
});
|
||||
|
||||
test('clicking a candidate relates it (and it shows on the post page)', async ({ page }) => {
|
||||
// heavier than the others: a full-reload relate (sx-disable) then a goto to the
|
||||
// post page — two WASM kernel boots — so it needs more than the 30s default.
|
||||
test('clicking a candidate relates it and removes just that row (no reload)', async ({ page }) => {
|
||||
test.setTimeout(75000);
|
||||
await loginTo(page, `/${HOST}/edit`);
|
||||
await waitReady(page);
|
||||
await page.fill(RELF, 'Item 07');
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 10000 }).toBe(1);
|
||||
// sentinel survives ONLY if there is no full-page reload
|
||||
await page.evaluate(() => { window.__noReload = true; });
|
||||
await page.locator(`${RELROWS} button`).first().click();
|
||||
// form POST -> 303 back to the edit page; the related list now links the slug
|
||||
await expect(page).toHaveURL(new RegExp(`/${HOST}/edit`));
|
||||
await expect(page.locator('a[href="/picker-item-07/"]')).toHaveCount(1);
|
||||
// and the public post page shows the Related posts block with the title
|
||||
// the AJAX relate deletes just that candidate row in place — no reload, no
|
||||
// candidate-list refetch
|
||||
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 10000 }).toBe(0);
|
||||
expect(await page.evaluate(() => window.__noReload)).toBe(true);
|
||||
// and the relation actually persisted (shows on the public post page)
|
||||
await page.goto(`/${HOST}/`);
|
||||
await expect(page.getByRole('heading', { name: 'Related posts' })).toBeVisible();
|
||||
await expect(page.locator('body')).toContainText('Picker Item 07');
|
||||
|
||||
Reference in New Issue
Block a user