0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-18 13:04:38 -05:00

♻️ Refactor user profile form.

This commit is contained in:
Andrey Antukh 2019-08-28 19:01:11 +02:00
parent 88cf5483c8
commit 08bd135d55
4 changed files with 132 additions and 124 deletions

View file

@ -17,21 +17,19 @@
;; --- Profile Fetched ;; --- 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 (defn profile-fetched
[data] [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 ;; --- Fetch Profile
@ -60,24 +58,6 @@
;; --- Update Profile ;; --- 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 ::fullname string?)
(s/def ::email us/email?) (s/def ::email us/email?)
(s/def ::username string?) (s/def ::username string?)
@ -90,11 +70,28 @@
::username])) ::username]))
(defn update-profile (defn update-profile
[data on-success on-error] [data {:keys [on-success on-error]}]
{:pre [(us/valid? ::update-profile data) {:pre [(us/valid? ::update-profile data)
(fn? on-error) (fn? on-error)
(fn? on-success)]} (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) ;; --- Update Password (Form)

View file

@ -21,7 +21,7 @@
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.workers :as uwrk] [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.forms :as sc]
[uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt] [uxbox.util.geom.point :as gpt]

View file

@ -20,14 +20,6 @@
[uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.interop :refer [iterable->seq]])) [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 (defn profile->form
[profile] [profile]
(let [language (get-in profile [:metadata :language])] (let [language (get-in profile [:metadata :language])]
@ -35,80 +27,84 @@
(cond-> language (assoc :language language))))) (cond-> language (assoc :language language)))))
(def profile-ref (def profile-ref
(-> (comp (l/key :profile) (-> (l/key :profile)
(l/lens profile->form))
(l/derive st/state))) (l/derive st/state)))
(s/def ::fullname ::fm/non-empty-string) (def profile-form-spec
(s/def ::username ::fm/non-empty-string) {:fullname [fm/required fm/string fm/non-empty-string]
(s/def ::email ::fm/email) :username [fm/required fm/string fm/non-empty-string]
(s/def ::language #{"en" "fr"}) :email [fm/required fm/email]
:language [fm/required fm/string]})
(s/def ::profile-form
(s/keys :req-un [::fullname
::username
::language
::email]))
(defn- on-error (defn- on-error
[{:keys [code] :as payload}] [error {:keys [errors] :as form}]
(case code (prn "on-error" error form)
:uxbox.services.users/registration-disabled (case (:code error)
(st/emit! (tr "errors.api.form.registration-disabled"))
:uxbox.services.users/email-already-exists :uxbox.services.users/email-already-exists
(st/emit! (assoc-error :email (tr "errors.api.form.email-already-exists"))) (swap! form assoc-in [:errors :email] "errors.api.form.email-already-exists")
:uxbox.services.users/username-already-exists
(st/emit! (assoc-error :username (tr "errors.api.form.username-already-exists")))))
(defn- on-field-change :uxbox.services.users/username-already-exists
[event field] (swap! form assoc-in [:errors :username] "errors.api.form.username-already-exists")))
(let [value (dom/event->value event)]
(st/emit! (assoc-value field value)))) (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 ;; --- Profile Form
(mf/def profile-form (mf/defc profile-form
:mixins [mf/memo mf/reactive (fm/clear-mixin st/store :profile)] [props]
:render (let [{:keys [data] :as form} (fm/use-form {:initial initial-data
(fn [own props] :spec profile-form-spec})]
(let [data (merge {:language @i18n/locale} [:form.profile-form {:on-submit #(on-submit % form)}
(mf/react profile-ref) [:span.user-settings-label (tr "settings.profile.section-basic-data")]
(mf/react form-data)) [:input.input-text
errors (mf/react form-errors) {:type "text"
valid? (fm/valid? ::profile-form data) :name "fullname"
on-success #(st/emit! (clear-form)) :on-blur (fm/on-input-blur form)
on-submit #(st/emit! (udu/update-profile data on-success on-error))] :on-change (fm/on-input-change form)
[:form.profile-form :value (:fullname data "")
[:span.user-settings-label (tr "settings.profile.section-basic-data")] :placeholder (tr "settings.profile.your-name")}]
[:input.input-text [:& fm/error-input {:form form :field :fullname}]
{:type "text" [:input.input-text
:on-change #(on-field-change % :fullname) {:type "text"
:value (:fullname data "") :name "username"
:placeholder (tr "settings.profile.your-name")}] :on-blur (fm/on-input-blur form)
[:input.input-text :on-change (fm/on-input-change form)
{:type "text" :value (:username data "")
:on-change #(on-field-change % :username) :placeholder (tr "settings.profile.your-username")}]
:value (:username data "") [:& fm/error-input {:form form :field :username}]
: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)
[:span.user-settings-label (tr "settings.profile.section-i18n-data")] [:input.input-text
[:select.input-select {:value (:language data) {:type "email"
:on-change #(on-field-change % :language)} :name "email"
[:option {:value "en"} "English"] :on-blur (fm/on-input-blur form)
[:option {:value "fr"} "Français"]] :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 [:span.user-settings-label (tr "settings.profile.section-i18n-data")]
{:type "button" [:select.input-select {:value (:language data)
:class (when-not valid? "btn-disabled") :name "language"
:disabled (not valid?) :on-blur (fm/on-input-blur form)
:on-click on-submit :on-change (fm/on-input-change form)}
:value (tr "settings.update-settings")}]]))) [: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 ;; --- Profile Photo Form
@ -121,7 +117,7 @@
(first))] (first))]
(st/emit! (udu/update-photo file)) (st/emit! (udu/update-photo file))
(dom/clean-value! target)))] (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)) photo (if (or (str/empty? photo) (nil? photo))
"images/avatar.jpg" "images/avatar.jpg"
photo)] photo)]
@ -139,4 +135,4 @@
[:section.user-settings-content [:section.user-settings-content
[:span.user-settings-label (tr "settings.profile.your-avatar")] [:span.user-settings-label (tr "settings.profile.your-avatar")]
[:& profile-photo-form] [:& profile-photo-form]
(profile-form)]]) [:& profile-form]]])

View file

@ -44,22 +44,23 @@
([self f x y] (update-fn #(f % x y))) ([self f x y] (update-fn #(f % x y)))
([self f x y more] (update-fn #(apply f % x y more)))))) ([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 (defn use-form
[{:keys [initial spec] :as opts}] [{:keys [initial spec] :as opts}]
(let [[data update-data] (mf/useState initial) (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
[errors update-errors] (mf/useState nil) :errors {}
[touched update-touched] (mf/useState {}) :touched {}})
[errors' clean-data] (validate data spec) [errors' clean-data] (validate spec (:data state))
errors (merge (reduce-kv #(assoc %1 %2 (:message %3)) {} errors')
data (impl-mutator data update-data) (:errors state))]
errors (-> (merge {} errors' errors) (-> (assoc state
(impl-mutator update-errors)) :errors errors
touched (impl-mutator touched update-touched)] :clean-data clean-data
{:clean-data clean-data :valid (not (seq errors)))
:touched touched (impl-mutator update-state))))
:data data
:errors errors
:valid (not (seq errors))}))
(defn on-input-change (defn on-input-change
[{:keys [data] :as form}] [{:keys [data] :as form}]
@ -67,7 +68,10 @@
(let [target (dom/get-target event) (let [target (dom/get-target event)
field (keyword (.-name target)) field (keyword (.-name target))
value (dom/get-value 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 (defn on-input-blur
[{:keys [touched] :as form}] [{:keys [touched] :as form}]
@ -75,7 +79,18 @@
(let [target (dom/get-target event) (let [target (dom/get-target event)
field (keyword (.-name target))] field (keyword (.-name target))]
(when-not (get touched field) (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 ;; --- Additional Validators