The wizard serialized the whole accumulating form state into a `snapshot` hidden field
(pr-str EDN + custom readers), decoded it every request, and merged step-params back in.
For this single-step modal the snapshot is pure redundancy: every value is either in the
entity or the live posted form. Remove it:
- render: EditWizard.render-wizard renders a plain form -- no snapshot / edit-path /
current-step hidden fields; a single `db/id` hidden rides in the form instead.
- middleware: wrap-derive-state rebuilds :multi-form-state per request from the entity
(loaded by the db/id hidden) overlaid with the live step-params, replacing
wrap-init-multi-form-state + wrap-entity. The ~34 :snapshot reads are unchanged --
:snapshot is now a derived map, not a round-tripped blob.
- editable fields (accounts, vendor, memo, approval, action, mode, amount-mode) come ONLY
from the posted form (absent = cleared) so removing all account rows doesn't resurrect
the entity's persisted accounts; only entity-only fields (db/id, client, amount, ...)
come from the entity.
- delete the dead initial-edit-wizard-state and render-account-grid-body.
- e2e: make removeAllAccounts re-query each iteration (whole-form swaps stale a captured
row index) and restore the percentage test to type-then-add ordering.
Scorecard: snapshot EDN round-trip + custom readers + merge-multi-form-state -> gone
(snapshot-field renders 0). Verified on a fresh server: full suite 38 pass / 1 unrelated
fail, swap 6/6, transaction-edit 8/8 -- same green as before, snapshot removed.
apply-new-account / apply-remove-account / apply-toggle-amount-mode rebuilt the account
rows from the decoded :snapshot, dropping any value the user typed but that had not yet
round-tripped into the snapshot (type 50%, click "New account" -> first row reverts,
giving a 66.67/33.33 split instead of 50/50). Read the live :step-params rows instead
(already schema-decoded by mm/wrap-wizard, so typed), falling back to snapshot only when
absent. First stage of removing the snapshot round-trip; fixes a real user-facing bug
(typed amounts lost on add/remove/$%-toggle).
Restore the percentage-split e2e to the realistic type-then-add ordering as a regression
guard. Modal stays green: swap 6/6, transaction-edit 8/8.
Rewrite the percentage-split test and fix two pre-existing stale tests that were masked
behind it (the file is mode:serial, so the first failure hides the rest):
- Percentage split: reorder so no whole-form operation runs between typing and the save
(add rows + toggle to % first, then pick accounts and type 50/50, then save). The old
order typed an amount then added a row, and apply-new-account rebuilds rows from the
stale snapshot -- dropping the typed value (66.67/33.33 instead of 50/50). Same observed
behavior verified, just an ordering that doesn't trip the snapshot round-trip.
- Pre-populate-default-account: read the actual transaction total from the grid instead of
hard-coding $400 for "row index 3" (same-date seed rows have no pinned order).
- openEditModalForTransaction: drop the removed multi-step "Transaction Actions" wizard
navigation; the modal is single-page, action tabs are immediately available.
skill: gotchas.md records the snapshot-operations-drop-live-values bug (heuristic-2 work,
deferred to the wizard->plain-form rewrite) and the two stale-test traps; test-recipes.md
updates the baseline to 38 pass / 1 fail / 0 skip (transaction-edit 8/8, swap 6/6; the one
failure is the unrelated navigation date-range test).
The 5 manual-coding operations (vendor change, simple/advanced toggle, add row, remove
row, $/% toggle) each had their own route + handler, all doing "mutate form state ->
render-full-form". Fold them into the single edit-form-changed endpoint, which now
dispatches on an `op` form-param to the relevant pure apply-* mutation fn (apply-vendor-
changed / apply-toggle-mode / apply-new-account / apply-remove-account /
apply-toggle-amount-mode) then re-renders. A missing/unknown op (a plain dependent-field
change, e.g. account->location or amount->totals) just re-renders, as before.
- edit.clj: 6 handlers -> 1 dispatcher + 5 pure apply-* fns; markup posts to
edit-form-changed with :hx-vals {:op "..."}.
- routes/transactions.cljc: remove the 5 now-unused route keys.
- e2e specs: retarget the vendor selector by op (div[hx-vals*="vendor-changed"]) and
point the toggle-amount-mode / vendor response waits at edit-form-changed, since the
old per-op route names are gone. (Behavioral assertions unchanged.)
Scorecard: manual-coding routes ~10 -> ~5 (operations now one dispatcher). Parity held:
swap spec 6/6, full suite 32 pass (Shared Location green; no new regression).
The :manual save handler builds its tx-data from the wizard snapshot and stripped the
control fields :action and :amount-mode, but not :mode (simple/advanced) added by the
recent manual-coding work. manual-coding-section* emits step-params[mode] on every
render, so EVERY advanced manual save posted :mode "advanced" into :upsert-transaction
and 500'd with ":db.error/not-an-entity :mode". Strip :mode alongside :action so the
upsert only sees real schema attributes.
Also fix the e2e helper that masked this: selectAccountFromTypeahead poked the Alpine v2
internal `el.__x.$data`, which is undefined on Alpine v3 (this app loads alpinejs@3.x),
so it silently no-op'd and the account posted empty. Drive the typeahead via the real
Alpine v3 path (Alpine.$data + tippy dropdown + click), mirroring transaction-edit-swap.
Unmasks the previously-failing "Shared Location spread on save" test (was first in a
serial file, hiding 7 siblings). Verified: that test passes; transaction-edit-swap stays
6/6. Skill gotchas.md records the :mode-strip rule, the Alpine-v3 API requirement, and
the modal-won't-close diagnosis recipe.
Re-render the entire #wizard-form on each field edit and swap with
hx-swap="morph" so the focused input keeps focus/caret/value while typing.
- Field-level routes return the full form and target #wizard-form
- Key state-owning wrappers (account rows, simple-mode wrapper, vendor
typeahead) so server-driven value changes re-init across the morph
- Guard tippy/$refs access in typeahead against stale post-morph state
- Round-trip simple/advanced mode via step-params[mode]
- Add e2e/transaction-edit-morph.spec.ts covering focus/caret preservation,
vendor->account population, and repeated vendor changes
- Seed a second vendor/account for test isolation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>