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:
parent
88cf5483c8
commit
08bd135d55
4 changed files with 132 additions and 124 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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]]])
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue