diff --git a/src/clj/auto_ap/ssr/components/multi_modal.clj b/src/clj/auto_ap/ssr/components/multi_modal.clj index 898343bb..cb3b03bd 100644 --- a/src/clj/auto_ap/ssr/components/multi_modal.clj +++ b/src/clj/auto_ap/ssr/components/multi_modal.clj @@ -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 diff --git a/src/clj/auto_ap/ssr/transaction/edit.clj b/src/clj/auto_ap/ssr/transaction/edit.clj index 7dec5a09..8c9c52e0 100644 --- a/src/clj/auto_ap/ssr/transaction/edit.clj +++ b/src/clj/auto_ap/ssr/transaction/edit.clj @@ -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)