refactor(ssr): remove the EDN snapshot round-trip; transaction edit is a plain form (heuristic 2)

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.
This commit is contained in:
2026-06-03 15:20:26 -07:00
parent 0b5bfd9c84
commit d0fad63e24
4 changed files with 104 additions and 59 deletions

View File

@@ -773,7 +773,9 @@
[:div.mt-4 {:hx-post (bidi/path-for ssr-routes/only-routes ::route/unlink-payment)
:hx-trigger "unlinkPayment"
:hx-target "#payment-matches"
:hx-include "this"
;; include the whole form so the db/id hidden rides along (the plain
;; form derives state from db/id instead of a serialized snapshot)
:hx-include "closest form"
:hx-swap "outerHTML"
:hx-confirm "Are you sure you want to unlink this payment?"}
@@ -1327,15 +1329,20 @@
(if current-step
(mm/get-step this current-step)
(mm/get-step this :links)))
(render-wizard [this {:keys [multi-form-state] :as request}]
(mm/default-render-wizard
this request
:form-params
(-> mm/default-form-props
(assoc :hx-post
(str (bidi/path-for ssr-routes/only-routes ::route/edit-submit))
:hx-ext "response-targets"))
:render-timeline? false))
(render-wizard [this {:keys [multi-form-state form-errors] :as request}]
;; Plain-form render: no snapshot / edit-path / current-step hidden fields. The entity
;; id rides in the form; all other state is the live form (step-params) re-derived
;; against the entity on each request (see wrap-derive-state).
(let [step (mm/get-current-step this)]
[:form#wizard-form (-> mm/default-form-props
(assoc :hx-post (str (bidi/path-for ssr-routes/only-routes ::route/edit-submit))
:hx-ext "response-targets"))
(fc/start-form multi-form-state (when form-errors {:step-params form-errors})
(list
(com/hidden {:name "db/id" :value (-> multi-form-state :snapshot :db/id)})
(fc/with-field :step-params
(com/modal {:id "wizardmodal"}
(mm/render-step step request)))))]))
(steps [_]
[:links])
(get-step [this step-key]
@@ -1350,32 +1357,53 @@
(def edit-wizard (->EditWizard nil nil))
(defn initial-edit-wizard-state [request]
(let [tx-id (-> request :route-params :db/id)
entity (dc/pull (dc/db conn)
'[:db/id
:transaction/vendor
:transaction/client
:transaction/description-original
:transaction/status
:transaction/type
:transaction/memo
{[:transaction/approval-status :xform iol-ion.query/ident] [:db/ident]}
:transaction/amount
:transaction/accounts]
tx-id)
entity (-> entity
(update :transaction/vendor :db/id)
(update :transaction/client :db/id))]
(mm/->MultiStepFormState entity
[]
entity)))
(defn entity->base
"The persisted transaction, shaped like the form's base state (what the old snapshot was
seeded with). The plain form derives its state fresh from this + the live posted form,
instead of round-tripping an EDN snapshot hidden field."
[tx-id]
(-> (dc/pull (dc/db conn)
'[:db/id
:transaction/vendor
:transaction/client
:transaction/description-original
:transaction/status
:transaction/type
:transaction/memo
{[:transaction/approval-status :xform iol-ion.query/ident] [:db/ident]}
:transaction/amount
:transaction/accounts]
tx-id)
(update :transaction/vendor :db/id)
(update :transaction/client :db/id)))
(defn- render-account-grid-body [request]
(fc/start-form (:multi-form-state request) nil
(fc/with-field :step-params
(fc/with-field :transaction/accounts
(account-grid-body* request)))))
(defn wrap-derive-state
"Plain-form replacement for the EDN-snapshot round-trip. Builds :multi-form-state from
the entity (loaded by the db/id hidden field, or the route on initial open) overlaid
with the live posted step-params -- no serialized snapshot. Runs after wrap-decode /
wrap-wizard, which provide nested + schema-typed step-params. The 30-odd `:snapshot`
reads keep working: snapshot is now `entity step-params`, derived per request."
[handler]
(fn [request]
(let [tx-id (->db-id (or (some-> request :form-params (get "db/id"))
(-> request :route-params :db/id)))
base (entity->base tx-id)
posted (-> request :multi-form-state :step-params)
;; Fields the form does NOT edit always come from the entity. Everything else is
;; the live posted form, which is authoritative even when ABSENT -- an absent
;; field means the user cleared it (e.g. removed all account rows), not "fall
;; back to the entity's persisted value". Merging base's editable fields back in
;; would resurrect persisted accounts after a remove-all.
entity-only (select-keys base [:db/id :transaction/client :transaction/amount
:transaction/description-original
:transaction/status :transaction/type])
;; On initial open there is no posted form -> render the entity. On every post
;; the form is authoritative for the editable fields.
step-params (if (seq posted) posted base)
snapshot (if (seq posted) (merge entity-only posted) base)]
(handler (-> request
(assoc :entity (d-transactions/get-by-id tx-id))
(assoc :multi-form-state (mm/->MultiStepFormState snapshot [] step-params)))))))
(defn render-full-form
"Helper to render the complete transaction edit form for whole-form re-rendering."
@@ -1518,18 +1546,18 @@
(apply-middleware-to-all-handlers
{::route/edit-wizard (-> mm/open-wizard-handler
(wrap-must {:activity :edit :subject :transaction} (fn get-client [request] (-> request :entity :transaction/client)))
(wrap-derive-state)
(mm/wrap-wizard edit-wizard)
(mm/wrap-init-multi-form-state initial-edit-wizard-state)
(wrap-entity [:route-params :db/id] d-transactions/default-read)
(mm/wrap-decode-multi-form-state)
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
::route/edit-wizard-navigate (-> mm/next-handler
(wrap-must {:activity :edit :subject :transaction} (fn get-client [request] (-> request :entity :transaction/client)))
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
(wrap-derive-state)
(mm/wrap-wizard edit-wizard)
(mm/wrap-decode-multi-form-state))
::route/edit-submit (-> mm/submit-handler
(wrap-must {:activity :edit :subject :transaction} (fn get-client [request] (-> request :entity :transaction/client)))
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
(wrap-derive-state)
(mm/wrap-wizard edit-wizard)
(mm/wrap-decode-multi-form-state))
::route/location-select (-> location-select
@@ -1540,16 +1568,14 @@
[:account-id {:optional true}
[:maybe entity-id]]]))
::route/edit-form-changed (-> edit-form-changed-handler
(wrap-derive-state)
(mm/wrap-wizard edit-wizard)
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
(mm/wrap-decode-multi-form-state))
::route/unlink-payment (-> unlink-payment
(wrap-must {:activity :edit :subject :transaction} (fn get-client [request] (-> request :entity :transaction/client)))
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
(wrap-derive-state)
(mm/wrap-wizard edit-wizard)
(mm/wrap-decode-multi-form-state)
#_(wrap-schema-enforce :form-schema
save-schema))}
(mm/wrap-decode-multi-form-state))}
(fn [h]
(-> h
(wrap-client-redirect-unauthenticated)))))