From 08bd135d55ccf329914c752bd55827b2bbaf8382 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 28 Aug 2019 19:01:11 +0200 Subject: [PATCH] :recycle: Refactor user profile form. --- frontend/src/uxbox/main/data/users.cljs | 63 ++++---- frontend/src/uxbox/main/data/workspace.cljs | 2 +- .../src/uxbox/main/ui/settings/profile.cljs | 144 +++++++++--------- frontend/src/uxbox/util/forms.cljs | 47 ++++-- 4 files changed, 132 insertions(+), 124 deletions(-) diff --git a/frontend/src/uxbox/main/data/users.cljs b/frontend/src/uxbox/main/data/users.cljs index 639fbebf03..f657212c1d 100644 --- a/frontend/src/uxbox/main/data/users.cljs +++ b/frontend/src/uxbox/main/data/users.cljs @@ -17,21 +17,19 @@ ;; --- Profile Fetched -(deftype ProfileFetched [data] - ptk/UpdateEvent - (update [this state] - (assoc state :profile data)) - - ptk/EffectEvent - (effect [this state stream] - (swap! storage assoc :profile data) - ;; (prn "profile-fetched" data) - (when-let [lang (get-in data [:metadata :language])] - (i18n/set-current-locale! lang)))) - (defn profile-fetched [data] - (ProfileFetched. data)) + (reify + ptk/UpdateEvent + (update [this state] + (assoc state :profile data)) + + ptk/EffectEvent + (effect [this state stream] + (swap! storage assoc :profile data) + ;; (prn "profile-fetched" data) + (when-let [lang (get-in data [:metadata :language])] + (i18n/set-current-locale! lang))))) ;; --- Fetch Profile @@ -60,24 +58,6 @@ ;; --- Update Profile -(deftype UpdateProfile [data on-success on-error] - ptk/WatchEvent - (watch [_ state s] - (letfn [(handle-error [{payload :payload}] - (on-error payload) - (rx/empty))] - (let [data (-> (:profile state) - (assoc :fullname (:fullname data)) - (assoc :email (:email data)) - (assoc :username (:username data)) - (assoc-in [:metadata :language] (:language data)))] - (prn "update-profile" data) - (->> (rp/req :update/profile data) - (rx/map :payload) - (rx/do on-success) - (rx/map profile-updated) - (rx/catch rp/client-error? handle-error)))))) - (s/def ::fullname string?) (s/def ::email us/email?) (s/def ::username string?) @@ -90,11 +70,28 @@ ::username])) (defn update-profile - [data on-success on-error] + [data {:keys [on-success on-error]}] {:pre [(us/valid? ::update-profile data) (fn? on-error) (fn? on-success)]} - (UpdateProfile. data on-success on-error)) + (reify + ptk/WatchEvent + (watch [_ state s] + (letfn [(handle-error [{payload :payload}] + (on-error payload) + (rx/empty))] + (let [data (-> (:profile state) + (assoc :fullname (:fullname data)) + (assoc :email (:email data)) + (assoc :username (:username data)) + (assoc-in [:metadata :language] (:language data)))] + (prn "update-profile" data) + (->> (rp/req :update/profile data) + (rx/map :payload) + (rx/do on-success) + (rx/map profile-updated) + ;; (rx/map profile-fetched) + (rx/catch rp/client-error? handle-error))))))) ;; --- Update Password (Form) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index e21f18582f..c0fa4f1f2e 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -21,7 +21,7 @@ [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.workers :as uwrk] - [uxbox.util.data :refer [dissoc-in index-of seek]] + [uxbox.util.data :refer [dissoc-in index-of]] [uxbox.util.forms :as sc] [uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.point :as gpt] diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index 47e7e0b045..3d7b2fb1ad 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -20,14 +20,6 @@ [uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.interop :refer [iterable->seq]])) - -(def form-data (fm/focus-data :profile st/state)) -(def form-errors (fm/focus-errors :profile st/state)) - -(def assoc-value (partial fm/assoc-value :profile)) -(def assoc-error (partial fm/assoc-error :profile)) -(def clear-form (partial fm/clear-form :profile)) - (defn profile->form [profile] (let [language (get-in profile [:metadata :language])] @@ -35,80 +27,84 @@ (cond-> language (assoc :language language))))) (def profile-ref - (-> (comp (l/key :profile) - (l/lens profile->form)) + (-> (l/key :profile) (l/derive st/state))) -(s/def ::fullname ::fm/non-empty-string) -(s/def ::username ::fm/non-empty-string) -(s/def ::email ::fm/email) -(s/def ::language #{"en" "fr"}) - -(s/def ::profile-form - (s/keys :req-un [::fullname - ::username - ::language - ::email])) +(def profile-form-spec + {:fullname [fm/required fm/string fm/non-empty-string] + :username [fm/required fm/string fm/non-empty-string] + :email [fm/required fm/email] + :language [fm/required fm/string]}) (defn- on-error - [{:keys [code] :as payload}] - (case code - :uxbox.services.users/registration-disabled - (st/emit! (tr "errors.api.form.registration-disabled")) + [error {:keys [errors] :as form}] + (prn "on-error" error form) + (case (:code error) :uxbox.services.users/email-already-exists - (st/emit! (assoc-error :email (tr "errors.api.form.email-already-exists"))) - :uxbox.services.users/username-already-exists - (st/emit! (assoc-error :username (tr "errors.api.form.username-already-exists"))))) + (swap! form assoc-in [:errors :email] "errors.api.form.email-already-exists") -(defn- on-field-change - [event field] - (let [value (dom/event->value event)] - (st/emit! (assoc-value field value)))) + :uxbox.services.users/username-already-exists + (swap! form assoc-in [:errors :username] "errors.api.form.username-already-exists"))) + +(defn- initial-data + [] + (merge {:language @i18n/locale} + (profile->form (deref profile-ref)))) + +(defn- on-submit + [event form] + (dom/prevent-default event) + (let [data (:clean-data form) + opts {:on-success #(prn "On Success" %) + :on-error #(on-error % form)}] + (st/emit! (udu/update-profile data opts)))) ;; --- Profile Form -(mf/def profile-form - :mixins [mf/memo mf/reactive (fm/clear-mixin st/store :profile)] - :render - (fn [own props] - (let [data (merge {:language @i18n/locale} - (mf/react profile-ref) - (mf/react form-data)) - errors (mf/react form-errors) - valid? (fm/valid? ::profile-form data) - on-success #(st/emit! (clear-form)) - on-submit #(st/emit! (udu/update-profile data on-success on-error))] - [:form.profile-form - [:span.user-settings-label (tr "settings.profile.section-basic-data")] - [:input.input-text - {:type "text" - :on-change #(on-field-change % :fullname) - :value (:fullname data "") - :placeholder (tr "settings.profile.your-name")}] - [:input.input-text - {:type "text" - :on-change #(on-field-change % :username) - :value (:username data "") - :placeholder (tr "settings.profile.your-username")}] - (fm/input-error errors :username) - [:input.input-text - {:type "email" - :on-change #(on-field-change % :email) - :value (:email data "") - :placeholder (tr "settings.profile.your-email")}] - (fm/input-error errors :email) +(mf/defc profile-form + [props] + (let [{:keys [data] :as form} (fm/use-form {:initial initial-data + :spec profile-form-spec})] + [:form.profile-form {:on-submit #(on-submit % form)} + [:span.user-settings-label (tr "settings.profile.section-basic-data")] + [:input.input-text + {:type "text" + :name "fullname" + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:fullname data "") + :placeholder (tr "settings.profile.your-name")}] + [:& fm/error-input {:form form :field :fullname}] + [:input.input-text + {:type "text" + :name "username" + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:username data "") + :placeholder (tr "settings.profile.your-username")}] + [:& fm/error-input {:form form :field :username}] - [:span.user-settings-label (tr "settings.profile.section-i18n-data")] - [:select.input-select {:value (:language data) - :on-change #(on-field-change % :language)} - [:option {:value "en"} "English"] - [:option {:value "fr"} "Français"]] + [:input.input-text + {:type "email" + :name "email" + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:email data "") + :placeholder (tr "settings.profile.your-email")}] + [:& fm/error-input {:form form :field :email}] - [:input.btn-primary - {:type "button" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :on-click on-submit - :value (tr "settings.update-settings")}]]))) + [:span.user-settings-label (tr "settings.profile.section-i18n-data")] + [:select.input-select {:value (:language data) + :name "language" + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form)} + [:option {:value "en"} "English"] + [:option {:value "fr"} "Français"]] + + [:input.btn-primary + {:type "submit" + :class (when-not (:valid form) "btn-disabled") + :disabled (not (:valid form)) + :value (tr "settings.update-settings")}]])) ;; --- Profile Photo Form @@ -121,7 +117,7 @@ (first))] (st/emit! (udu/update-photo file)) (dom/clean-value! target)))] - (let [{:keys [photo]} (mf/deref profile-ref) + (let [{:keys [photo] :as profile} (mf/deref profile-ref) photo (if (or (str/empty? photo) (nil? photo)) "images/avatar.jpg" photo)] @@ -139,4 +135,4 @@ [:section.user-settings-content [:span.user-settings-label (tr "settings.profile.your-avatar")] [:& profile-photo-form] - (profile-form)]]) + [:& profile-form]]]) diff --git a/frontend/src/uxbox/util/forms.cljs b/frontend/src/uxbox/util/forms.cljs index 5d2427c661..4034792371 100644 --- a/frontend/src/uxbox/util/forms.cljs +++ b/frontend/src/uxbox/util/forms.cljs @@ -44,22 +44,23 @@ ([self f x y] (update-fn #(f % x y))) ([self f x y more] (update-fn #(apply f % x y more)))))) +(defn- simplify-errors + [errors] + (reduce-kv #(assoc %1 %2 (:message %3)) {} errors)) + (defn use-form [{:keys [initial spec] :as opts}] - (let [[data update-data] (mf/useState initial) - [errors update-errors] (mf/useState nil) - [touched update-touched] (mf/useState {}) - [errors' clean-data] (validate data spec) - - data (impl-mutator data update-data) - errors (-> (merge {} errors' errors) - (impl-mutator update-errors)) - touched (impl-mutator touched update-touched)] - {:clean-data clean-data - :touched touched - :data data - :errors errors - :valid (not (seq errors))})) + (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) + :errors {} + :touched {}}) + [errors' clean-data] (validate spec (:data state)) + errors (merge (reduce-kv #(assoc %1 %2 (:message %3)) {} errors') + (:errors state))] + (-> (assoc state + :errors errors + :clean-data clean-data + :valid (not (seq errors))) + (impl-mutator update-state)))) (defn on-input-change [{:keys [data] :as form}] @@ -67,7 +68,10 @@ (let [target (dom/get-target event) field (keyword (.-name target)) value (dom/get-value target)] - (swap! data assoc field value)))) + (swap! form (fn [state] + (-> state + (assoc-in [:data field] value) + (update :errors dissoc field))))))) (defn on-input-blur [{:keys [touched] :as form}] @@ -75,7 +79,18 @@ (let [target (dom/get-target event) field (keyword (.-name target))] (when-not (get touched field) - (swap! touched assoc field true))))) + (swap! form assoc-in [:touched field] true))))) + +;; --- Helper Components + +(mf/defc error-input + [{:keys [form field] :as props}] + (let [touched? (get-in form [:touched field]) + error? (get-in form [:errors field])] + (when (and touched? error?) + [:ul.form-errors + [:li {:key error?} (tr error?)]]))) + ;; --- Additional Validators