diff --git a/.claude/skills/ssr-form-migration/reference/scorecard.md b/.claude/skills/ssr-form-migration/reference/scorecard.md index 5770d60d..5a30b28e 100644 --- a/.claude/skills/ssr-form-migration/reference/scorecard.md +++ b/.claude/skills/ssr-form-migration/reference/scorecard.md @@ -39,14 +39,15 @@ Each migration appends one row (after-numbers), referencing the before in the di | Phase | Modal | LOC | Routes | no-cursor twins | faked roots | snapshot merges | OOB | mixed hx- | cookbook reused / added | |-------|-------|-----|--------|-----------------|-------------|-----------------|-----|-----------|-------------------------| | 1 (baseline) | Transaction Edit `transaction/edit.clj` | 1608 | ~12 | 1 | 2 | ~75 | 0 | 8 | — / seeded 7 entries | -| 2 (in progress) | Transaction Edit `transaction/edit.clj` | 1555 | ~12 | **0** | 2 | ~75 | 0 | 8 | — / 0 | +| 2 (in progress) | Transaction Edit `transaction/edit.clj` | 1570 | ~12 | **0** | **0** | ~75 | 0 | 8 | — / 0 | > **Phase 2 progress (partial).** Achieved with parity held (swap spec 6/6 + Shared -> Location green): deleted the dead `*-no-cursor*` twin (no-cursor 1→0, −53 LOC) and fixed -> a real production bug (`:mode` leaking into the upsert → 500 on every advanced manual -> save). **Still open** for this modal — and intentionally *not* forced under parity risk: -> faked cursor roots (2 — de-faking needs the render-fn rewrite, see `gotchas.md`), the -> snapshot round-trip (~75 — removed by the wizard→plain-form reclassification), Selmer -> conversion of the render fns, and route collapse (~12 → ~3). These are the bulk of the -> modal migration and require restructuring the modal's rendering wholesale rather than -> isolated edits; track as the continuation of Phase 2. +> Location green, full suite 31 pass / no regression): deleted the dead `*-no-cursor*` +> twin (no-cursor 1→0), **de-faked the simple-mode cursor** (faked roots 2→0) by rendering +> the row from explicit data with explicit field names (`account-field-name`) + explicit +> error lookup — the render-fn rewrite the `with-field-default` shortcut couldn't do — and +> fixed a real production bug (`:mode` leaking into the upsert → 500 on every advanced +> manual save). **Still open** for this modal: the snapshot round-trip (~75 — removed by +> the wizard→plain-form reclassification), Selmer conversion of the render fns, and route +> collapse (~12 → ~3). These remain the bulk of the migration and need wholesale +> restructuring of the modal's rendering; track as the continuation of Phase 2. diff --git a/src/clj/auto_ap/ssr/transaction/edit.clj b/src/clj/auto_ap/ssr/transaction/edit.clj index a3986e37..3e4cbce5 100644 --- a/src/clj/auto_ap/ssr/transaction/edit.clj +++ b/src/clj/auto_ap/ssr/transaction/edit.clj @@ -183,10 +183,32 @@ (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) client-id)))})]) +(defn- account-field-name + "Explicit form-field name for account row `index`, field `field` -- the same string + the form cursor produces at path [:step-params :transaction/accounts index field] + (via path->name2), without faking a deep cursor to get there." + [index field] + (str "step-params[transaction/accounts][" index "][" + (if (keyword? field) + (str (when (namespace field) (str (namespace field) "/")) (name field)) + field) + "]")) + +(defn- account-field-errors + "Errors for account row `index`, field `field`, read straight from the form errors + at the same path the cursor would walk -- avoids re-rooting a cursor to look them up." + [index field] + (when (bound? #'fc/*form-errors*) + (get-in fc/*form-errors* [:step-params :transaction/accounts index field]))) + (defn simple-mode-fields* "Renders the simple-mode account + location row and the toggle-to-advanced link. Must be called within a fc/start-form + fc/with-field :step-params context. - Caller must establish Alpine x-data with simpleAccountId in scope." + Caller must establish Alpine x-data with simpleAccountId in scope. + + The single account row is rendered from explicit data with explicit field names + (account-field-name 0 ...) rather than faking a synthetic MapCursor rooted at + accounts[0] -- the row always lives at index 0 in simple mode." [request] (let [snapshot (-> request :multi-form-state :snapshot) step-params (-> request :multi-form-state :step-params) @@ -204,50 +226,41 @@ (:transaction/amount snapshot) 0.0))] [:div - (fc/with-field :transaction/accounts - (fc/with-cursor (let [cur fc/*current*] - (if (sequential? @cur) - (nth cur 0 nil) - (auto_ap.cursor.MapCursor. {} (cursor/state cur) (conj (cursor/path cur) 0)))) - [:span - (fc/with-field :db/id - (com/hidden {:name (fc/field-name) - :value row-id})) - [:div.flex.gap-2.mt-2 - (fc/with-field :transaction-account/account - (com/validated-field - {:label "Account" - :errors (fc/field-errors)} - [:div.w-72 - (account-typeahead* {:value account-val - :client-id client-id - :name (fc/field-name) - :x-model "simpleAccountId"})])) - (fc/with-field :transaction-account/location - ;; Selecting the account only affects the valid Location options, so the - ;; change swaps just this cell -- nothing else needs to re-render. - [:div {:id "simple-account-location"} - (com/validated-field - {:label "Location" - :errors (fc/field-errors) - :x-hx-val:account-id "simpleAccountId" - :hx-vals (hx/json (cond-> {:name (fc/field-name)} - client-id (assoc :client-id client-id))) - :x-dispatch:changed "simpleAccountId" - :hx-trigger "changed" - :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) - :hx-target "#simple-account-location" - :hx-select "#simple-account-location" - :hx-swap "outerHTML" - :hx-include "closest form"} - (location-select* - {:name (fc/field-name) - :account-location (:account/location account-id) - :client-locations (pull-attr (dc/db conn) :client/locations client-id) - :value location-val}))]) - (fc/with-field :transaction-account/amount - (com/hidden {:name (fc/field-name) - :value total}))]])) + [:span + (com/hidden {:name (account-field-name 0 :db/id) + :value row-id}) + [:div.flex.gap-2.mt-2 + (com/validated-field + {:label "Account" + :errors (account-field-errors 0 :transaction-account/account)} + [:div.w-72 + (account-typeahead* {:value account-val + :client-id client-id + :name (account-field-name 0 :transaction-account/account) + :x-model "simpleAccountId"})]) + ;; Selecting the account only affects the valid Location options, so the + ;; change swaps just this cell -- nothing else needs to re-render. + [:div {:id "simple-account-location"} + (com/validated-field + {:label "Location" + :errors (account-field-errors 0 :transaction-account/location) + :x-hx-val:account-id "simpleAccountId" + :hx-vals (hx/json (cond-> {:name (account-field-name 0 :transaction-account/location)} + client-id (assoc :client-id client-id))) + :x-dispatch:changed "simpleAccountId" + :hx-trigger "changed" + :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) + :hx-target "#simple-account-location" + :hx-select "#simple-account-location" + :hx-swap "outerHTML" + :hx-include "closest form"} + (location-select* + {:name (account-field-name 0 :transaction-account/location) + :account-location (:account/location account-id) + :client-locations (pull-attr (dc/db conn) :client/locations client-id) + :value location-val}))] + (com/hidden {:name (account-field-name 0 :transaction-account/amount) + :value total})]] [:div.mt-1 [:a.text-sm.text-blue-600.hover:underline.cursor-pointer {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)