fix(ssr): only require account coding for manual transaction edits
Account coding lived in the always-applied base map of edit-form-schema, so every action (including the link/apply-rule/unlink actions) required a valid transaction-account/account. The edit modal always submits the Manual tab's (usually blank) account row, so link submits failed validation before reaching their save-handler and silently no-op'd. Move account validation into the :manual branch of the action :multi so link actions validate without it. Also surface whole-form validation errors in the wizard footer error bar: default-step-footer only handled top-level/sequential error shapes, so nested field-error maps (e.g. a hidden tab's account error) produced an empty bar and a silent failure. Add flatten-form-errors to flatten the humanized error tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -138,6 +138,33 @@
|
||||
[:div.space-y-1 {}
|
||||
children])
|
||||
|
||||
(defn flatten-form-errors
|
||||
"Walks a malli-humanized error structure and returns a flat sequence of
|
||||
human-readable strings, prefixing each leaf message with the nearest
|
||||
field name for context. Lets the footer's error bar surface every
|
||||
validation error for the whole form, even ones whose field lives on a
|
||||
hidden step/tab and so would otherwise be invisible."
|
||||
([errors] (flatten-form-errors nil errors))
|
||||
([field errors]
|
||||
(let [label (cond (keyword? field) (name field)
|
||||
(string? field) field
|
||||
:else nil)
|
||||
decorate (fn [msg] (if label (str label ": " msg) msg))]
|
||||
(cond
|
||||
(map? errors)
|
||||
(mapcat (fn [[k v]] (flatten-form-errors k v)) errors)
|
||||
|
||||
(and (sequential? errors) (every? string? errors))
|
||||
(map decorate errors)
|
||||
|
||||
(sequential? errors)
|
||||
(mapcat #(flatten-form-errors field %) errors)
|
||||
|
||||
(string? errors)
|
||||
[(decorate errors)]
|
||||
|
||||
:else nil))))
|
||||
|
||||
(defn default-step-footer [linear-wizard step & {:keys [validation-route
|
||||
discard-button
|
||||
next-button
|
||||
@@ -146,7 +173,8 @@
|
||||
[:div.flex.items-baseline.gap-x-4
|
||||
(let [step-errors (:step-params fc/*form-errors*)]
|
||||
(com/form-errors {:errors (or (:errors step-errors)
|
||||
(when (sequential? step-errors) step-errors))}))
|
||||
(when (sequential? step-errors) step-errors)
|
||||
(seq (distinct (flatten-form-errors step-errors))))}))
|
||||
(when (not= (first (steps linear-wizard))
|
||||
(step-key step))
|
||||
(when validation-route
|
||||
|
||||
@@ -72,6 +72,27 @@
|
||||
(or (not= approval-status :transaction-approval-status/approved)
|
||||
(seq accounts)))]])
|
||||
|
||||
(def account-coding-schema
|
||||
"Validation for manually-coded transaction account rows. Applied only for
|
||||
the :manual action: link / apply-rule actions build their own accounts
|
||||
server-side, so the (often blank) account row carried along by the manual
|
||||
tab must not be required when one of those actions is submitted."
|
||||
[:maybe
|
||||
[:vector {:coerce? true}
|
||||
[:and
|
||||
[:map
|
||||
[:db/id {:optional true} [:maybe [:or temp-id entity-id]]]
|
||||
[:transaction-account/account [:and entity-id
|
||||
[:fn {:error/message "Not an allowed account."}
|
||||
#(check-allowance % :account/default-allowance)]]]
|
||||
[:transaction-account/location :string]
|
||||
[:transaction-account/amount :double]]
|
||||
[:fn {:error/fn (fn [r x] (:type r))
|
||||
:error/path [:transaction-account/location]}
|
||||
(fn [iea]
|
||||
(check-location-belongs (:transaction-account/location iea)
|
||||
(:transaction-account/account iea)))]]]])
|
||||
|
||||
(def edit-form-schema
|
||||
(mc/schema
|
||||
[:and
|
||||
@@ -81,23 +102,7 @@
|
||||
[:transaction/memo {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:transaction/vendor {:optional true} [:maybe entity-id]]
|
||||
[:transaction/approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")]]
|
||||
[:amount-mode {:optional true} [:maybe [:enum "$" "%"]]]
|
||||
[:transaction/accounts {:optional true}
|
||||
[:maybe
|
||||
[:vector {:coerce? true}
|
||||
[:and
|
||||
[:map
|
||||
[:db/id {:optional true} [:maybe [:or temp-id entity-id]]]
|
||||
[:transaction-account/account [:and entity-id
|
||||
[:fn {:error/message "Not an allowed account."}
|
||||
#(check-allowance % :account/default-allowance)]]]
|
||||
[:transaction-account/location :string]
|
||||
[:transaction-account/amount :double]]
|
||||
[:fn {:error/fn (fn [r x] (:type r))
|
||||
:error/path [:transaction-account/location]}
|
||||
(fn [iea]
|
||||
(check-location-belongs (:transaction-account/location iea)
|
||||
(:transaction-account/account iea)))]]]]]]
|
||||
[:amount-mode {:optional true} [:maybe [:enum "$" "%"]]]]
|
||||
[:multi {:dispatch :action}
|
||||
[:apply-rule [:map
|
||||
[:rule-id {:optional true} [:maybe entity-id]]]]
|
||||
@@ -110,7 +115,8 @@
|
||||
[:autopay-invoice-ids {:decode/string (fn [x] (edn/read-string x))} [:vector {:coerce? true} entity-id]]]]
|
||||
[:link-payment [:map
|
||||
[:payment-id entity-id]]]
|
||||
[:manual (require-approval [:map])]]]))
|
||||
[:manual (require-approval [:map
|
||||
[:transaction/accounts {:optional true} account-coding-schema]])]]]))
|
||||
|
||||
(defn clientize-vendor [{:vendor/keys [terms-overrides automatically-paid-when-due default-account account-overrides] :as vendor} client-id]
|
||||
(if (nil? vendor)
|
||||
|
||||
Reference in New Issue
Block a user