diff --git a/.claude/skills/ssr-form-migration/reference/form-vs-wizard.md b/.claude/skills/ssr-form-migration/reference/form-vs-wizard.md index e039c77e..bfb7b35b 100644 --- a/.claude/skills/ssr-form-migration/reference/form-vs-wizard.md +++ b/.claude/skills/ssr-form-migration/reference/form-vs-wizard.md @@ -229,3 +229,36 @@ accounts totals all post the whole `#wizard-form`; in the engine that form carri `invoice/*` fields + the opaque `wizard-id`, so a fragment decodes what it needs straight from `form-params` (and, for a cross-step value like the invoice total on the accounts step, reads `ws/get-all` via the posted `wizard-id`). No `mm/wrap-decode-multi-form-state` stack survives. + +## Sub-editor: a parameterized sub-step on the linear engine (Phase 10, bank accounts) + +The engine's steps are a flat list — it has no nested/parameterized step like the old +mm `[:bank-account which]`. When a step owns a *collection you edit one item at a time* +(a list view ⇄ a per-item editor, with accept/discard/sort), don't try to bend the step +list. Model it as a **sub-editor of that step**, entirely in whole-form swaps: + +- **The step renders the list view** (cards/rows + an "add" affordance). Each item's + edit/new control is an `hx-get` that targets `#wizard-form` with `hx-swap outerHTML` and + carries `?wizard-id=&index=N` (the wizard-id is in the render ctx). +- **The editor is its own `
`** (so it swaps cleanly and the next + swap replaces it) with the item's fields + hidden `wizard-id` + a hidden item index. Its + Accept `hx-post`s an accept route; Discard `hx-get`s a discard route. It is NOT a wizard + step and does NOT go through `handle-step-submit`. +- **Dedicated routes mutate the step's data in the session directly** and re-render the + list via the engine: read `(ws/step-data session wid )`, splice the decoded + item into the vector (`assoc` at index, or `conj` to append for new), `ws/put-step`, then + `(wizard2/render-wizard {:config … :wizard-id wid :session session' :request request})` + and `(assoc :session session')`. Discard just re-renders from the unchanged session. +- **The step's own `:decode` is a pass-through.** Because the list lives in the session + (managed by the sub-editor, not by in-form inputs), the step's Next must re-affirm it, + not decode it from a near-empty form. Read it back with the wizard-id — but the engine + strips `wizard-id`/`current-step`/`direction` from form-params before `:decode`, so smuggle + it through an extra hidden the engine leaves alone (we used `wiz`): + `(or (ws/step-data (:session request) (get-in request [:form-params "wiz"]) ) {…})`. +- Give the step a no-op `:validate` (`(fn [_ _] nil)`) — items are validated on Accept. +- Clean control keys out of the decoded item before storage (`select-keys` to `:db/id` + + the entity's own namespace) so `wizard-id`/index/`:new?` never reach datomic. + +This keeps the doctrine intact (every byte is a whole-form swap of `#wizard-form`; no EDN +snapshot rides the page) while giving the linear engine an add/edit/sort sub-flow it has +no native concept for. diff --git a/.claude/skills/ssr-form-migration/reference/scorecard.md b/.claude/skills/ssr-form-migration/reference/scorecard.md index deaf5686..a167ee07 100644 --- a/.claude/skills/ssr-form-migration/reference/scorecard.md +++ b/.claude/skills/ssr-form-migration/reference/scorecard.md @@ -293,3 +293,38 @@ blank-nested-entity upsert error (see `gotchas.md`). create across all 5 steps persists; edit opens prefilled and a rename persists; a too-short name blocks advancing). Create + edit semantics also confirmed at the REPL (incl. the cookie-session EDN round-trip). `maybe-spread-locations`-style domain helpers untouched. + +--- + +## Phase 10 — New/Edit Client (the largest modal: 7 steps + a bank-account sub-editor) + +**Coupling outcome:** `defrecord` **9 → 0** (InfoModal / MatchesModal / ContactModal / +BankAccountsModal / IntegrationsModal / BankAccountModal / CashFlowModal / +OtherSettingsModal + ClientWizard all gone), `mm/` **0**, `fc/` cursor refs **0**, +`step-params[…]` **0**, `bank-account-card`/`bank-account-form` multimethods (dispatched on +`(comp deref :bank-account/type)`) collapsed to plain `case` on a data map. Routes: the +broken `navigate` + `discard` are deleted; four bank-account sub-editor routes added +(new/edit/accept/discard) + sort kept. The grid, both form schemas, and ~200 lines of +sales power-query export are preserved **verbatim** (stitched around the rewritten wizard +region rather than retyped). + +**The new pattern — a parameterized sub-step on a linear engine.** The old +`[:bank-account which]` mm sub-step (open one account, Accept/discard/sort, back to the +list) doesn't map onto wizard2's flat step list. Modeled instead as a *sub-editor of the +bank-accounts step*: see `form-vs-wizard.md` ("Sub-editor"). Key moves: list + editor are +both whole-form swaps of `#wizard-form`; dedicated routes mutate `:bank-accounts` +step-data in the session via `ws/put-step` and re-render through `wizard2/render-wizard`; +the step's own `:decode` is a **pass-through** that re-reads the session list (via a `wiz` +hidden the engine doesn't strip) so Next never wipes the out-of-band list. + +**Fixes carried/!surfaced:** new-vs-edit keyed off `:db/id` presence (engine always POSTs, +so the old PUT/POST split is gone); client + bank-account dates → `#inst` for EDN-safe +session; the **blank-address** trap recurs (empty Contact address posts blank fields → +all-nil db/id-less map → "tempid used only as value") — same `blank-address?` drop as +Phase 9. A long detour confirmed the REPL is direct-link-poisoned: validate migrations +against a fresh `TEST_SERVER_PORT=… lein run -m auto-ap.test-server` JVM, not the REPL. + +**Verification:** full e2e suite **71/71** (65 prior + 6 client-wizard: new dialog + +timeline; edit prefill w/ disabled code; bank-accounts card + add affordance; editor +open/discard; accept-merge; edit→save round-trip). Engine flow + accept + pass-through + +edit init also confirmed at the REPL.