From 11e3c85c58524f8198b9e303cedfcb05891e801e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 19 Dec 2025 10:08:22 +0100 Subject: [PATCH] :rewind: Backport form error management improvements from develop --- .../src/app/main/ui/components/forms.cljs | 3 +- frontend/src/app/util/forms.cljs | 88 ++++++++++++------- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 45419d56ca..0e6c55882a 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -50,7 +50,8 @@ touched? (and (contains? (:data @form) input-name) (get-in @form [:touched input-name])) - error (get-in @form [:errors input-name]) + error (or (get-in @form [:errors input-name]) + (get-in @form [:extra-errors input-name])) value (get-in @form [:data input-name] "") diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs index 968c319e7f..a48fae2310 100644 --- a/frontend/src/app/util/forms.cljs +++ b/frontend/src/app/util/forms.cljs @@ -48,7 +48,11 @@ (let [props (m/properties schema) tprops (m/type-properties schema) field (or (first in) - (:error/field props))] + (:error/field props)) + + field (if (vector? field) + field + [field])] (if (contains? acc field) acc @@ -58,30 +62,30 @@ (or (= type :malli.core/missing-key) (nil? value)) - (assoc acc field {:message (tr "errors.field-missing")}) + (assoc-in acc field {:message (tr "errors.field-missing")}) ;; --- CHECK on schema props (contains? props :error/fn) - (assoc acc field (handle-error-fn props problem)) + (assoc-in acc field (handle-error-fn props problem)) (contains? props :error/message) - (assoc acc field (handle-error-message props)) + (assoc-in acc field (handle-error-message props)) (contains? props :error/code) - (assoc acc field (handle-error-code props)) + (assoc-in acc field (handle-error-code props)) ;; --- CHECK on type props (contains? tprops :error/fn) - (assoc acc field (handle-error-fn tprops problem)) + (assoc-in acc field (handle-error-fn tprops problem)) (contains? tprops :error/message) - (assoc acc field (handle-error-message tprops)) + (assoc-in acc field (handle-error-message tprops)) (contains? tprops :error/code) - (assoc acc field (handle-error-code tprops)) + (assoc-in acc field (handle-error-code tprops)) :else - (assoc acc field {:message (tr "errors.invalid-data")}))))) + (assoc-in acc field {:message (tr "errors.invalid-data")}))))) (defn- use-rerender-fn [] @@ -114,20 +118,35 @@ [f {:keys [schema validators]}] (fn [& args] (let [state (apply f args) - cleaned (sm/decode schema (:data state) sm/string-transformer) + cleaned (sm/decode schema (:data state) sm/json-transformer) valid? (sm/validate schema cleaned) - errors (when-not valid? - (collect-schema-errors schema validators state))] + + errors + (when-not valid? + (collect-schema-errors schema validators state)) + + extra-errors + (not-empty (:extra-errors state))] (assoc state :errors errors :clean-data (when valid? cleaned) - :valid (and (not errors) valid?))))) + :valid (and (not errors) + (not extra-errors) + valid?))))) + + +(defn- make-initial-state + [initial-data] + (let [initial (if (fn? initial-data) (initial-data) initial-data) + initial (d/nilv initial {})] + {:initial initial + :data initial + :errors {} + :touched {}})) (defn- create-form-mutator - [internal-state rerender-fn wrap-update-fn initial opts] - (mf/set-ref-val! internal-state initial) - + [internal-state rerender-fn wrap-update-fn opts] (reify IDeref (-deref [_] @@ -136,7 +155,10 @@ IReset (-reset! [_ new-value] (if (nil? new-value) - (mf/set-ref-val! internal-state (if (fn? initial) (initial) initial)) + (let [initial (-> (mf/ref-val internal-state) + (get :initial) + (make-initial-state))] + (mf/set-ref-val! internal-state initial)) (mf/set-ref-val! internal-state new-value)) (rerender-fn)) @@ -162,24 +184,25 @@ (rerender-fn))))) (defn use-form - [& {:keys [initial] :as opts}] + [& {:keys [initial schema validators] :as opts}] (let [rerender-fn (use-rerender-fn) initial (mf/with-memo [initial] - {:data (if (fn? initial) (initial) initial) - :errors {} - :touched {}}) + (make-initial-state initial)) internal-state - (mf/use-ref nil) + (mf/use-ref initial) form-mutator - (mf/with-memo [initial] - (create-form-mutator internal-state rerender-fn wrap-update-schema-fn initial opts))] + (mf/with-memo [schema validators] + (let [mutator (create-form-mutator internal-state rerender-fn wrap-update-schema-fn + (select-keys opts [:schema :validators]))] + (swap! mutator identity) + mutator))] ;; Initialize internal state once - (mf/with-layout-effect [] + (mf/with-effect [] (mf/set-ref-val! internal-state initial)) (mf/with-effect [initial] @@ -191,11 +214,16 @@ ([form field value] (on-input-change form field value false)) ([form field value trim?] - (swap! form (fn [state] - (-> state - (assoc-in [:touched field] true) - (assoc-in [:data field] (if trim? (str/trim value) value)) - (update :errors dissoc field)))))) + (letfn [(clean-errors [errors] + (-> errors + (dissoc field) + (not-empty)))] + (swap! form (fn [state] + (-> state + (assoc-in [:touched field] true) + (assoc-in [:data field] (if trim? (str/trim value) value)) + (update :errors clean-errors) + (update :extra-errors clean-errors))))))) (defn update-input-value! [form field value]