mirror of
https://github.com/penpot/penpot.git
synced 2025-05-22 11:06:13 +02:00
♻️ Refactor forms
Mainly replace spec with schema with better and more reusable validations
This commit is contained in:
parent
f095e1b29f
commit
7be79c10fd
62 changed files with 786 additions and 1165 deletions
|
@ -7,8 +7,8 @@
|
|||
(ns app.main.ui.auth.login
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as log]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.main.data.messages :as msg]
|
||||
|
@ -72,20 +72,19 @@
|
|||
(s/keys :req-un [::email ::password]
|
||||
:opt-un [::invitation-token]))
|
||||
|
||||
(defn handle-error-messages
|
||||
[errors _data]
|
||||
(d/update-when errors :email
|
||||
(fn [{:keys [code] :as error}]
|
||||
(cond-> error
|
||||
(= code ::us/email)
|
||||
(assoc :message (tr "errors.email-invalid"))))))
|
||||
(def ^:private schema:login-form
|
||||
[:map {:title "LoginForm"}
|
||||
[:email [::sm/email {:error/code "errors.invalid-email"}]]
|
||||
[:password [:string {:min 1}]]
|
||||
[:invitation-token {:optional true}
|
||||
[:string {:min 1}]]])
|
||||
|
||||
(mf/defc login-form
|
||||
[{:keys [params on-success-callback origin] :as props}]
|
||||
(let [initial (mf/use-memo (mf/deps params) (constantly params))
|
||||
(let [initial (mf/with-memo [params] params)
|
||||
error (mf/use-state false)
|
||||
form (fm/use-form :spec ::login-form
|
||||
:validators [handle-error-messages]
|
||||
form (fm/use-form :schema schema:login-form
|
||||
;; :validators [handle-error-messages]
|
||||
:initial initial)
|
||||
|
||||
on-error
|
||||
|
|
|
@ -7,39 +7,29 @@
|
|||
(ns app.main.ui.auth.recovery
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::password-1 ::us/not-empty-string)
|
||||
(s/def ::password-2 ::us/not-empty-string)
|
||||
(s/def ::token ::us/not-empty-string)
|
||||
|
||||
(s/def ::recovery-form
|
||||
(s/keys :req-un [::password-1
|
||||
::password-2]))
|
||||
|
||||
(defn- password-equality
|
||||
[errors data]
|
||||
(let [password-1 (:password-1 data)
|
||||
password-2 (:password-2 data)]
|
||||
(cond-> errors
|
||||
(and password-1 password-2
|
||||
(not= password-1 password-2))
|
||||
(assoc :password-2 {:message "errors.password-invalid-confirmation"})
|
||||
|
||||
(and password-1 (> 8 (count password-1)))
|
||||
(assoc :password-1 {:message "errors.password-too-short"}))))
|
||||
(def ^:private schema:recovery-form
|
||||
[:and
|
||||
[:map {:title "RecoveryForm"}
|
||||
[:token ::sm/text]
|
||||
[:password-1 ::sm/password]
|
||||
[:password-2 ::sm/password]]
|
||||
[:fn {:error/code "errors.password-invalid-confirmation"
|
||||
:error/field :password-2}
|
||||
(fn [{:keys [password-1 password-2]}]
|
||||
(= password-1 password-2))]])
|
||||
|
||||
(defn- on-error
|
||||
[_form _error]
|
||||
(st/emit! (msg/error (tr "auth.notifications.invalid-token-error"))))
|
||||
(st/emit! (msg/error (tr "errors.invalid-recovery-token"))))
|
||||
|
||||
(defn- on-success
|
||||
[_]
|
||||
|
@ -56,14 +46,13 @@
|
|||
|
||||
(mf/defc recovery-form
|
||||
[{:keys [params] :as props}]
|
||||
(let [form (fm/use-form :spec ::recovery-form
|
||||
:validators [password-equality
|
||||
(fm/validate-not-empty :password-1 (tr "auth.password-not-empty"))
|
||||
(fm/validate-not-empty :password-2 (tr "auth.password-not-empty"))]
|
||||
(let [form (fm/use-form :schema schema:recovery-form
|
||||
:initial params)]
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:class (stl/css :recovery-form)
|
||||
:form form}
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input {:type "password"
|
||||
:name :password-1
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
(ns app.main.ui.auth.recovery-request
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
|
@ -17,30 +16,24 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::recovery-request-form (s/keys :req-un [::email]))
|
||||
(defn handle-error-messages
|
||||
[errors _data]
|
||||
(d/update-when errors :email
|
||||
(fn [{:keys [code] :as error}]
|
||||
(cond-> error
|
||||
(= code :missing)
|
||||
(assoc :message (tr "errors.email-invalid"))))))
|
||||
(def ^:private schema:recovery-request-form
|
||||
[:map {:title "RecoverRequestForm"}
|
||||
[:email ::sm/email]])
|
||||
|
||||
(mf/defc recovery-form
|
||||
[{:keys [on-success-callback] :as props}]
|
||||
(let [form (fm/use-form :spec ::recovery-request-form
|
||||
:validators [handle-error-messages]
|
||||
(let [form (fm/use-form :schema schema:recovery-request-form
|
||||
:initial {})
|
||||
submitted (mf/use-state false)
|
||||
|
||||
default-success-finish #(st/emit! (msg/info (tr "auth.notifications.recovery-token-sent")))
|
||||
default-success-finish
|
||||
(mf/use-fn
|
||||
#(st/emit! (msg/info (tr "auth.notifications.recovery-token-sent"))))
|
||||
|
||||
on-success
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [cdata _]
|
||||
(reset! submitted false)
|
||||
(if (nil? on-success-callback)
|
||||
|
@ -48,7 +41,7 @@
|
|||
(on-success-callback (:email cdata)))))
|
||||
|
||||
on-error
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [data cause]
|
||||
(reset! submitted false)
|
||||
(let [code (-> cause ex-data :code)]
|
||||
|
@ -65,7 +58,7 @@
|
|||
(rx/throw cause)))))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! submitted true)
|
||||
(let [cdata (:clean-data @form)
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
(ns app.main.ui.auth.register
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.users :as du]
|
||||
|
@ -22,67 +21,42 @@
|
|||
[app.util.router :as rt]
|
||||
[app.util.storage :as sto]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- PAGE: Register
|
||||
|
||||
(defn- validate-password-length
|
||||
[errors data]
|
||||
(let [password (:password data)]
|
||||
(cond-> errors
|
||||
(> 8 (count password))
|
||||
(assoc :password {:message "errors.password-too-short"}))))
|
||||
|
||||
(defn- validate-email
|
||||
[errors _]
|
||||
(d/update-when errors :email
|
||||
(fn [{:keys [code] :as error}]
|
||||
(cond-> error
|
||||
(= code ::us/email)
|
||||
(assoc :message (tr "errors.email-invalid"))))))
|
||||
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::invitation-token ::us/not-empty-string)
|
||||
(s/def ::terms-privacy ::us/boolean)
|
||||
|
||||
(s/def ::register-form
|
||||
(s/keys :req-un [::password ::email]
|
||||
:opt-un [::invitation-token]))
|
||||
|
||||
(defn- on-prepare-register-error
|
||||
[form cause]
|
||||
(let [{:keys [type code]} (ex-data cause)]
|
||||
(condp = [type code]
|
||||
[:restriction :registration-disabled]
|
||||
(st/emit! (msg/error (tr "errors.registration-disabled")))
|
||||
|
||||
[:restriction :email-domain-is-not-allowed]
|
||||
(st/emit! (msg/error (tr "errors.email-domain-not-allowed")))
|
||||
|
||||
[:validation :email-as-password]
|
||||
(swap! form assoc-in [:errors :password]
|
||||
{:message "errors.email-as-password"})
|
||||
|
||||
(st/emit! (msg/error (tr "errors.generic"))))))
|
||||
|
||||
(defn- on-prepare-register-success
|
||||
[params]
|
||||
(st/emit! (rt/nav :auth-register-validate {} params)))
|
||||
(def ^:private schema:register-form
|
||||
[:map {:title "RegisterForm"}
|
||||
[:password ::sm/password]
|
||||
[:email ::sm/email]
|
||||
[:invitation-token {:optional true} ::sm/text]])
|
||||
|
||||
(mf/defc register-form
|
||||
{::mf/props :obj}
|
||||
[{:keys [params on-success-callback]}]
|
||||
(let [initial (mf/use-memo (mf/deps params) (constantly params))
|
||||
form (fm/use-form :spec ::register-form
|
||||
:validators [validate-password-length
|
||||
validate-email
|
||||
(fm/validate-not-empty :password (tr "auth.password-not-empty"))]
|
||||
form (fm/use-form :schema schema:register-form
|
||||
:initial initial)
|
||||
|
||||
submitted? (mf/use-state false)
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
(fn [form cause]
|
||||
(let [{:keys [type code]} (ex-data cause)]
|
||||
(condp = [type code]
|
||||
[:restriction :registration-disabled]
|
||||
(st/emit! (msg/error (tr "errors.registration-disabled")))
|
||||
|
||||
[:restriction :email-domain-is-not-allowed]
|
||||
(st/emit! (msg/error (tr "errors.email-domain-not-allowed")))
|
||||
|
||||
[:validation :email-as-password]
|
||||
(swap! form assoc-in [:errors :password]
|
||||
{:code "errors.email-as-password"})
|
||||
|
||||
(st/emit! (msg/error (tr "errors.generic")))))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps on-success-callback)
|
||||
|
@ -90,16 +64,14 @@
|
|||
(reset! submitted? true)
|
||||
(let [cdata (:clean-data @form)
|
||||
on-success (fn [data]
|
||||
(if (nil? on-success-callback)
|
||||
(on-prepare-register-success data)
|
||||
(on-success-callback data)))
|
||||
on-error (fn [data]
|
||||
(on-prepare-register-error form data))]
|
||||
(if (fn? on-success-callback)
|
||||
(on-success-callback data)
|
||||
(st/emit! (rt/nav :auth-register-validate {} data))))]
|
||||
|
||||
(->> (rp/cmd! :prepare-register-profile cdata)
|
||||
(rx/map #(merge % params))
|
||||
(rx/finalize #(reset! submitted? false))
|
||||
(rx/subs! on-success on-error)))))]
|
||||
(rx/subs! on-success (partial on-error form))))))]
|
||||
|
||||
[:& fm/form {:on-submit on-submit :form form}
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
|
@ -164,33 +136,6 @@
|
|||
|
||||
;; --- PAGE: register validation
|
||||
|
||||
(defn- on-register-success
|
||||
[data]
|
||||
(cond
|
||||
(some? (:invitation-token data))
|
||||
(let [token (:invitation-token data)]
|
||||
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
|
||||
|
||||
(:is-active data)
|
||||
(st/emit! (du/login-from-register))
|
||||
|
||||
:else
|
||||
(do
|
||||
(swap! sto/storage assoc ::email (:email data))
|
||||
(st/emit! (rt/nav :auth-register-success)))))
|
||||
|
||||
(s/def ::accept-terms-and-privacy (s/and ::us/boolean true?))
|
||||
(s/def ::accept-newsletter-subscription ::us/boolean)
|
||||
|
||||
(if (contains? cf/flags :terms-and-privacy-checkbox)
|
||||
(s/def ::register-validate-form
|
||||
(s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
|
||||
:opt-un [::accept-newsletter-subscription]))
|
||||
(s/def ::register-validate-form
|
||||
(s/keys :req-un [::token ::fullname]
|
||||
:opt-un [::accept-terms-and-privacy
|
||||
::accept-newsletter-subscription])))
|
||||
|
||||
(mf/defc terms-and-privacy
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
|
@ -210,34 +155,48 @@
|
|||
:default-checked false
|
||||
:label terms-label}]]))
|
||||
|
||||
(def ^:private schema:register-validate-form
|
||||
[:map {:title "RegisterValidateForm"}
|
||||
[:token ::sm/text]
|
||||
[:fullname [::sm/text {:max 250}]]
|
||||
[:accept-terms-and-privacy {:optional (not (contains? cf/flags :terms-and-privacy-checkbox))}
|
||||
[:and :boolean [:= true]]]])
|
||||
|
||||
(mf/defc register-validate-form
|
||||
{::mf/props :obj}
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [params on-success-callback]}]
|
||||
(let [validators (mf/with-memo []
|
||||
[(fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))
|
||||
(fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))])
|
||||
|
||||
form (fm/use-form :spec ::register-validate-form
|
||||
:validators validators
|
||||
:initial params)
|
||||
|
||||
(let [form (fm/use-form :schema schema:register-validate-form :initial params)
|
||||
submitted? (mf/use-state false)
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
(mf/deps on-success-callback)
|
||||
(fn [params]
|
||||
(if (nil? on-success-callback)
|
||||
(on-register-success params)
|
||||
(on-success-callback (:email params)))))
|
||||
(if (fn? on-success-callback)
|
||||
(on-success-callback (:email params))
|
||||
|
||||
(cond
|
||||
(some? (:invitation-token params))
|
||||
(let [token (:invitation-token params)]
|
||||
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
|
||||
|
||||
(:is-active params)
|
||||
(st/emit! (du/login-from-register))
|
||||
|
||||
:else
|
||||
(do
|
||||
(swap! sto/storage assoc ::email (:email params))
|
||||
(st/emit! (rt/nav :auth-register-success)))))))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
(fn [_cause]
|
||||
(fn [_]
|
||||
(st/emit! (msg/error (tr "errors.generic")))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps on-success on-error)
|
||||
(fn [form _]
|
||||
(reset! submitted? true)
|
||||
(let [params (:clean-data @form)]
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[cljs.core :as c]
|
||||
[clojure.string]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
@ -26,7 +25,9 @@
|
|||
(def use-form fm/use-form)
|
||||
|
||||
(mf/defc input
|
||||
[{:keys [label help-icon disabled form hint trim children data-testid on-change-value placeholder show-success?] :as props}]
|
||||
[{:keys [label help-icon disabled form hint trim children data-testid on-change-value placeholder show-success? show-error]
|
||||
:or {show-error true}
|
||||
:as props}]
|
||||
(let [input-type (get props :type "text")
|
||||
input-name (get props :name)
|
||||
more-classes (get props :class)
|
||||
|
@ -152,11 +153,14 @@
|
|||
children])
|
||||
|
||||
(cond
|
||||
(and touched? (:message error))
|
||||
[:div {:id (dm/str "error-" input-name)
|
||||
:class (stl/css :error)
|
||||
:data-testid (clojure.string/join [data-testid "-error"])}
|
||||
(tr (:message error))]
|
||||
(and touched? (:code error) show-error)
|
||||
(let [code (:code error)]
|
||||
[:div {:id (dm/str "error-" input-name)
|
||||
:class (stl/css :error)
|
||||
:data-testid (dm/str data-testid "-error")}
|
||||
(if (vector? code)
|
||||
(tr (nth code 0) (i18n/c (nth code 1)))
|
||||
(tr code))])
|
||||
|
||||
(string? hint)
|
||||
[:div {:class (stl/css :hint)} hint])]]))
|
||||
|
@ -207,8 +211,8 @@
|
|||
[:label {:class (stl/css :textarea-label)} label]
|
||||
[:> :textarea props]
|
||||
(cond
|
||||
(and touched? (:message error))
|
||||
[:span {:class (stl/css :error)} (tr (:message error))]
|
||||
(and touched? (:code error))
|
||||
[:span {:class (stl/css :error)} (tr (:code error))]
|
||||
|
||||
(string? hint)
|
||||
[:span {:class (stl/css :hint)} hint])]))
|
||||
|
@ -550,41 +554,3 @@
|
|||
[:span {:class (stl/css :text)} (:text item)]
|
||||
[:button {:class (stl/css :icon)
|
||||
:on-click #(remove-item! item)} i/close]]])])]))
|
||||
|
||||
;; --- Validators
|
||||
|
||||
(defn all-spaces?
|
||||
[value]
|
||||
(let [trimmed (str/trim value)]
|
||||
(str/empty? trimmed)))
|
||||
|
||||
(def max-length-allowed 250)
|
||||
(def max-uri-length-allowed 2048)
|
||||
|
||||
(defn max-length?
|
||||
[value length]
|
||||
(> (count value) length))
|
||||
|
||||
(defn validate-length
|
||||
[field length errors-msg]
|
||||
(fn [errors data]
|
||||
(cond-> errors
|
||||
(max-length? (get data field) length)
|
||||
(assoc field {:message errors-msg}))))
|
||||
|
||||
(defn validate-not-empty
|
||||
[field error-msg]
|
||||
(fn [errors data]
|
||||
(cond-> errors
|
||||
(all-spaces? (get data field))
|
||||
(assoc field {:message error-msg}))))
|
||||
|
||||
(defn validate-not-all-spaces
|
||||
[field error-msg]
|
||||
(fn [errors data]
|
||||
(let [value (get data field)]
|
||||
(cond-> errors
|
||||
(and
|
||||
(all-spaces? value)
|
||||
(> (count value) 0))
|
||||
(assoc field {:message error-msg})))))
|
||||
|
|
|
@ -7,25 +7,24 @@
|
|||
(ns app.main.ui.dashboard.change-owner
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::member-id ::us/uuid)
|
||||
(s/def ::leave-modal-form
|
||||
(s/keys :req-un [::member-id]))
|
||||
(def ^:private schema:leave-modal-form
|
||||
[:map {:title "LeaveModalForm"}
|
||||
[:member-id ::sm/uuid]])
|
||||
|
||||
(mf/defc leave-and-reassign-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :leave-and-reassign}
|
||||
[{:keys [profile team accept]}]
|
||||
(let [form (fm/use-form :spec ::leave-modal-form :initial {})
|
||||
(let [form (fm/use-form :schema schema:leave-modal-form :initial {})
|
||||
members-map (mf/deref refs/dashboard-team-members)
|
||||
members (vals members-map)
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.dashboard :as dd]
|
||||
|
@ -33,7 +34,6 @@
|
|||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
||||
(def ^:private arrow-icon
|
||||
(i/icon-xref :arrow (stl/css :arrow-icon)))
|
||||
|
||||
|
@ -131,6 +131,12 @@
|
|||
(s/def ::invite-member-form
|
||||
(s/keys :req-un [::role ::emails ::team-id]))
|
||||
|
||||
(def ^:private schema:invite-member-form
|
||||
[:map {:title "InviteMemberForm"}
|
||||
[:role :keyword]
|
||||
[:emails [::sm/set {:kind ::sm/email :min 1}]]
|
||||
[:team-id ::sm/uuid]])
|
||||
|
||||
(mf/defc invite-members-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :invite-members
|
||||
|
@ -139,9 +145,14 @@
|
|||
(let [members-map (mf/deref refs/dashboard-team-members)
|
||||
perms (:permissions team)
|
||||
|
||||
roles (mf/use-memo (mf/deps perms) #(get-available-roles perms))
|
||||
initial (mf/use-memo (constantly {:role "editor" :team-id (:id team)}))
|
||||
form (fm/use-form :spec ::invite-member-form
|
||||
roles (mf/with-memo [perms]
|
||||
(get-available-roles perms))
|
||||
team-id (:id team)
|
||||
|
||||
initial (mf/with-memo [team-id]
|
||||
{:role "editor" :team-id team-id})
|
||||
|
||||
form (fm/use-form :schema schema:invite-member-form
|
||||
:initial initial)
|
||||
error-text (mf/use-state "")
|
||||
|
||||
|
@ -746,10 +757,11 @@
|
|||
;; WEBHOOKS SECTION
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::uri ::us/uri)
|
||||
(s/def ::mtype ::us/not-empty-string)
|
||||
(s/def ::webhook-form
|
||||
(s/keys :req-un [::uri ::mtype]))
|
||||
(def ^:private schema:webhook-form
|
||||
[:map {:title "WebhookForm"}
|
||||
[:uri [::sm/uri {:max 4069 :prefix #"^http[s]?://"
|
||||
:error/code "errors.webhooks.invalid-uri"}]]
|
||||
[:mtype ::sm/text]])
|
||||
|
||||
(def valid-webhook-mtypes
|
||||
[{:label "application/json" :value "application/json"}
|
||||
|
@ -763,12 +775,12 @@
|
|||
{::mf/register modal/components
|
||||
::mf/register-as :webhook}
|
||||
[{:keys [webhook] :as props}]
|
||||
;; FIXME: this is a workaround because input fields do not support rendering hooks
|
||||
(let [initial (mf/use-memo (fn [] (or (some-> webhook (update :uri str))
|
||||
{:is-active false :mtype "application/json"})))
|
||||
form (fm/use-form :spec ::webhook-form
|
||||
:initial initial
|
||||
:validators [(fm/validate-length :uri fm/max-uri-length-allowed (tr "team.webhooks.max-length"))])
|
||||
|
||||
(let [initial (mf/with-memo []
|
||||
(or (some-> webhook (update :uri str))
|
||||
{:is-active false :mtype "application/json"}))
|
||||
form (fm/use-form :schema schema:webhook-form
|
||||
:initial initial)
|
||||
on-success
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
(ns app.main.ui.dashboard.team-form
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
|
@ -19,12 +19,11 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[app.util.router :as rt]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::team-form
|
||||
(s/keys :req-un [::name]))
|
||||
(def ^:private schema:team-form
|
||||
[:map {:title "TeamForm"}
|
||||
[:name [::sm/text {:max 250}]]])
|
||||
|
||||
(defn- on-create-success
|
||||
[_form response]
|
||||
|
@ -68,24 +67,23 @@
|
|||
(on-update-submit form)
|
||||
(on-create-submit form))))
|
||||
|
||||
(mf/defc team-form-modal {::mf/register modal/components
|
||||
::mf/register-as :team-form}
|
||||
(mf/defc team-form-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :team-form}
|
||||
[{:keys [team] :as props}]
|
||||
(let [initial (mf/use-memo (fn [] (or team {})))
|
||||
form (fm/use-form :spec ::team-form
|
||||
:validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space"))
|
||||
(fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))]
|
||||
form (fm/use-form :schema schema:team-form
|
||||
:initial initial)
|
||||
handle-keydown
|
||||
(mf/use-callback
|
||||
(mf/deps)
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(when (kbd/enter? e)
|
||||
(dom/prevent-default e)
|
||||
(dom/stop-propagation e)
|
||||
(on-submit form e))))
|
||||
|
||||
on-close #(st/emit! (modal/hide))]
|
||||
on-close
|
||||
(mf/use-fn #(st/emit! (modal/hide)))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.events :as-alias ev]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
@ -56,25 +56,20 @@
|
|||
(tr "labels.start"))
|
||||
:class (stl/css :next-button)}]]]))
|
||||
|
||||
(s/def ::questions-form-step-1
|
||||
(s/keys :req-un [::planning
|
||||
::expected-use]
|
||||
:opt-un [::planning-other]))
|
||||
(def ^:private schema:questions-form-1
|
||||
[:and
|
||||
|
||||
(defn- step-1-form-validator
|
||||
[errors data]
|
||||
(let [planning (:planning data)
|
||||
planning-other (:planning-other data)]
|
||||
(cond-> errors
|
||||
(and (= planning "other")
|
||||
(str/blank? planning-other))
|
||||
(assoc :planning-other {:code "missing"})
|
||||
[:map {:title "QuestionsFormStep1"}
|
||||
[:planning ::sm/text]
|
||||
[:expected-use [:enum "work" "education" "personal"]]
|
||||
[:planning-other {:optional true}
|
||||
[::sm/text {:max 512}]]]
|
||||
|
||||
(not= planning "other")
|
||||
(assoc :planning-other nil)
|
||||
|
||||
(str/blank? planning)
|
||||
(assoc :planning {:code "missing"}))))
|
||||
[:fn {:error/field :planning-other}
|
||||
(fn [{:keys [planning planning-other]}]
|
||||
(or (not= planning "other")
|
||||
(and (= planning "other")
|
||||
(not (str/blank? planning-other)))))]])
|
||||
|
||||
(mf/defc step-1
|
||||
{::mf/props :obj}
|
||||
|
@ -143,24 +138,24 @@
|
|||
[:& fm/input {:name :planning-other
|
||||
:class (stl/css :input-spacing)
|
||||
:placeholder (tr "labels.other")
|
||||
:show-error false
|
||||
:label ""}])]]))
|
||||
|
||||
(s/def ::questions-form-step-2
|
||||
(s/keys :req-un [::experience-design-tool]
|
||||
:opt-un [::experience-design-tool-other]))
|
||||
(def ^:private schema:questions-form-2
|
||||
[:and
|
||||
[:map {:title "QuestionsFormStep2"}
|
||||
[:experience-design-tool
|
||||
[:enum "figma" "sketch" "adobe-xd" "canva" "invision" "other"]]
|
||||
[:experience-design-tool-other {:optional true}
|
||||
[::sm/text {:max 512}]]]
|
||||
|
||||
(defn- step-2-form-validator
|
||||
[errors data]
|
||||
(let [experience (:experience-design-tool data)
|
||||
experience-other (:experience-design-tool-other data)]
|
||||
|
||||
(cond-> errors
|
||||
(and (= experience "other")
|
||||
(str/blank? experience-other))
|
||||
(assoc :experience-design-tool-other {:code "missing"})
|
||||
|
||||
(not= experience "other")
|
||||
(assoc :experience-design-tool-other nil))))
|
||||
[:fn {:error/field :experience-design-tool-other}
|
||||
(fn [data]
|
||||
(let [experience (:experience-design-tool data)
|
||||
experience-other (:experience-design-tool-other data)]
|
||||
(or (not= experience "other")
|
||||
(and (= experience "other")
|
||||
(not (str/blank? experience-other))))))]])
|
||||
|
||||
(mf/defc step-2
|
||||
{::mf/props :obj}
|
||||
|
@ -180,7 +175,7 @@
|
|||
(conj {:label (tr "labels.other-short") :value "other" :icon i/curve})))
|
||||
|
||||
current-experience
|
||||
(dm/get-in @form [:clean-data :experience-design-tool])
|
||||
(dm/get-in @form [:data :experience-design-tool])
|
||||
|
||||
on-design-tool-change
|
||||
(mf/use-fn
|
||||
|
@ -212,33 +207,34 @@
|
|||
[:& fm/input {:name :experience-design-tool-other
|
||||
:class (stl/css :input-spacing)
|
||||
:placeholder (tr "labels.other")
|
||||
:show-error false
|
||||
:label ""}])]]))
|
||||
|
||||
(s/def ::questions-form-step-3
|
||||
(s/keys :req-un [::team-size ::role ::responsability]
|
||||
:opt-un [::role-other ::responsability-other]))
|
||||
|
||||
(defn- step-3-form-validator
|
||||
[errors data]
|
||||
(let [role (:role data)
|
||||
role-other (:role-other data)
|
||||
responsability (:responsability data)
|
||||
responsability-other (:responsability-other data)]
|
||||
(def ^:private schema:questions-form-3
|
||||
[:and
|
||||
[:map {:title "QuestionsFormStep3"}
|
||||
[:team-size
|
||||
[:enum "more-than-50" "31-50" "11-30" "2-10" "freelancer" "personal-project"]]
|
||||
[:role
|
||||
[:enum "designer" "developer" "student-teacher" "graphic-design" "marketing" "manager" "other"]]
|
||||
[:responsability
|
||||
[:enum "team-leader" "team-member" "freelancer" "ceo-founder" "director" "student-teacher" "other"]]
|
||||
|
||||
(cond-> errors
|
||||
(and (= role "other")
|
||||
(str/blank? role-other))
|
||||
(assoc :role-other {:code "missing"})
|
||||
[:role-other {:optional true} [::sm/text {:max 512}]]
|
||||
[:responsability-other {:optional true} [::sm/text {:max 512}]]]
|
||||
|
||||
(not= role "other")
|
||||
(assoc :role-other nil)
|
||||
[:fn {:error/field :role-other}
|
||||
(fn [{:keys [role role-other]}]
|
||||
(or (not= role "other")
|
||||
(and (= role "other")
|
||||
(not (str/blank? role-other)))))]
|
||||
|
||||
(and (= responsability "other")
|
||||
(str/blank? responsability-other))
|
||||
(assoc :responsability-other {:code "missing"})
|
||||
|
||||
(not= responsability "other")
|
||||
(assoc :responsability-other nil))))
|
||||
[:fn {:error/field :responsability-other}
|
||||
(fn [{:keys [responsability responsability-other]}]
|
||||
(or (not= responsability "other")
|
||||
(and (= responsability "other")
|
||||
(not (str/blank? responsability-other)))))]])
|
||||
|
||||
(mf/defc step-3
|
||||
{::mf/props :obj}
|
||||
|
@ -264,7 +260,6 @@
|
|||
{:label (tr "labels.director") :value "director"}])
|
||||
(conj {:label (tr "labels.other-short") :value "other"})))
|
||||
|
||||
|
||||
team-size-options
|
||||
(mf/with-memo []
|
||||
[{:label (tr "labels.select-option") :value "" :key "team-size" :disabled true}
|
||||
|
@ -301,6 +296,7 @@
|
|||
[:& fm/input {:name :role-other
|
||||
:class (stl/css :input-spacing)
|
||||
:placeholder (tr "labels.other")
|
||||
:show-error false
|
||||
:label ""}])]
|
||||
|
||||
[:div {:class (stl/css :modal-question)}
|
||||
|
@ -314,6 +310,7 @@
|
|||
[:& fm/input {:name :responsability-other
|
||||
:class (stl/css :input-spacing)
|
||||
:placeholder (tr "labels.other")
|
||||
:show-error false
|
||||
:label ""}])]
|
||||
|
||||
[:div {:class (stl/css :modal-question)}
|
||||
|
@ -323,21 +320,18 @@
|
|||
:select-class (stl/css :select-class)
|
||||
:name :team-size}]]]))
|
||||
|
||||
(s/def ::questions-form-step-4
|
||||
(s/keys :req-un [::start-with]
|
||||
:opt-un [::start-with-other]))
|
||||
(def ^:private schema:questions-form-4
|
||||
[:and
|
||||
[:map {:title "QuestionsFormStep4"}
|
||||
[:start-with
|
||||
[:enum "ui" "wireframing" "prototyping" "ds" "code" "other"]]
|
||||
[:start-with-other {:optional true} [::sm/text {:max 512}]]]
|
||||
|
||||
(defn- step-4-form-validator
|
||||
[errors data]
|
||||
(let [start (:start-with data)
|
||||
start-other (:start-with-other data)]
|
||||
(cond-> errors
|
||||
(and (= start "other")
|
||||
(str/blank? start-other))
|
||||
(assoc :start-with-other {:code "missing"})
|
||||
|
||||
(not= start "other")
|
||||
(assoc :start-with-other nil))))
|
||||
[:fn {:error/field :start-with-other}
|
||||
(fn [{:keys [start-with start-with-other]}]
|
||||
(or (not= start-with "other")
|
||||
(and (= start-with "other")
|
||||
(not (str/blank? start-with-other)))))]])
|
||||
|
||||
(mf/defc step-4
|
||||
{::mf/props :obj}
|
||||
|
@ -386,23 +380,21 @@
|
|||
[:& fm/input {:name :start-with-other
|
||||
:class (stl/css :input-spacing)
|
||||
:label ""
|
||||
:show-error false
|
||||
:placeholder (tr "labels.other")}])]]))
|
||||
|
||||
(s/def ::questions-form-step-5
|
||||
(s/keys :req-un [::referer]
|
||||
:opt-un [::referer-other]))
|
||||
(def ^:private schema:questions-form-5
|
||||
[:and
|
||||
[:map {:title "QuestionsFormStep5"}
|
||||
[:referer
|
||||
[:enum "youtube" "event" "search" "social" "article" "other"]]
|
||||
[:referer-other {:optional true} [::sm/text {:max 512}]]]
|
||||
|
||||
(defn- step-5-form-validator
|
||||
[errors data]
|
||||
(let [referer (:referer data)
|
||||
referer-other (:referer-other data)]
|
||||
(cond-> errors
|
||||
(and (= referer "other")
|
||||
(str/blank? referer-other))
|
||||
(assoc :referer-other {:code "missing"})
|
||||
|
||||
(not= referer "other")
|
||||
(assoc :referer-other nil))))
|
||||
[:fn {:error/field :referer-other}
|
||||
(fn [{:keys [referer referer-other]}]
|
||||
(or (not= referer "other")
|
||||
(and (= referer "other")
|
||||
(not (str/blank? referer-other)))))]])
|
||||
|
||||
(mf/defc step-5
|
||||
{::mf/props :obj}
|
||||
|
@ -444,6 +436,7 @@
|
|||
[:& fm/input {:name :referer-other
|
||||
:class (stl/css :input-spacing)
|
||||
:label ""
|
||||
:show-error false
|
||||
:placeholder (tr "labels.other")}])]]))
|
||||
|
||||
(mf/defc questions-modal
|
||||
|
@ -456,28 +449,23 @@
|
|||
;; and we want to keep the filled info
|
||||
step-1-form (fm/use-form
|
||||
:initial {}
|
||||
:validators [step-1-form-validator]
|
||||
:spec ::questions-form-step-1)
|
||||
:schema schema:questions-form-1)
|
||||
|
||||
step-2-form (fm/use-form
|
||||
:initial {}
|
||||
:validators [step-2-form-validator]
|
||||
:spec ::questions-form-step-2)
|
||||
:schema schema:questions-form-2)
|
||||
|
||||
step-3-form (fm/use-form
|
||||
:initial {}
|
||||
:validators [step-3-form-validator]
|
||||
:spec ::questions-form-step-3)
|
||||
:schema schema:questions-form-3)
|
||||
|
||||
step-4-form (fm/use-form
|
||||
:initial {}
|
||||
:validators [step-4-form-validator]
|
||||
:spec ::questions-form-step-4)
|
||||
:schema schema:questions-form-4)
|
||||
|
||||
step-5-form (fm/use-form
|
||||
:initial {}
|
||||
:validators [step-5-form-validator]
|
||||
:spec ::questions-form-step-5)
|
||||
:schema schema:questions-form-5)
|
||||
|
||||
on-next
|
||||
(mf/use-fn
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as msg]
|
||||
|
@ -18,7 +18,6 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
@ -55,11 +54,10 @@
|
|||
[:p {:class (stl/css :modal-desc)}
|
||||
(tr "onboarding.team-modal.create-team-feature-5")]]]])
|
||||
|
||||
|
||||
(s/def ::emails (s/and ::us/set-of-valid-emails))
|
||||
(s/def ::role ::us/keyword)
|
||||
(s/def ::invite-form
|
||||
(s/keys :req-un [::role ::emails]))
|
||||
(def ^:private schema:invite-form
|
||||
[:map {:title "InviteForm"}
|
||||
[:role :keyword]
|
||||
[:emails [::sm/set {:kind ::sm/email}]]])
|
||||
|
||||
(defn- get-available-roles
|
||||
[]
|
||||
|
@ -73,7 +71,7 @@
|
|||
#(do {:role "editor"
|
||||
:name name}))
|
||||
|
||||
form (fm/use-form :spec ::invite-form
|
||||
form (fm/use-form :schema schema:invite-form
|
||||
:initial initial)
|
||||
|
||||
params (:clean-data @form)
|
||||
|
@ -151,7 +149,7 @@
|
|||
:name :emails
|
||||
:auto-focus? true
|
||||
:trim true
|
||||
:valid-item-fn us/parse-email
|
||||
:valid-item-fn sm/parse-email
|
||||
:caution-item-fn #{}
|
||||
:label (tr "modals.invite-member.emails")
|
||||
:on-submit on-submit}]]
|
||||
|
@ -172,18 +170,16 @@
|
|||
|
||||
[:div {:class (stl/css :paginator)} "2/2"]]))
|
||||
|
||||
(def ^:private schema:team-form
|
||||
[:map {:title "TeamForm"}
|
||||
[:name [::sm/text {:max 250}]]])
|
||||
|
||||
(mf/defc team-form-step-1
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [on-submit]}]
|
||||
(let [validators (mf/with-memo []
|
||||
[(fm/validate-not-empty :name (tr "auth.name.not-all-space"))
|
||||
(fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))])
|
||||
|
||||
form (fm/use-form
|
||||
:spec ::team-form
|
||||
:initial {}
|
||||
:validators validators)
|
||||
(let [form (fm/use-form :schema schema:team-form
|
||||
:initial {})
|
||||
|
||||
on-submit*
|
||||
(mf/use-fn
|
||||
|
@ -240,10 +236,6 @@
|
|||
|
||||
[:div {:class (stl/css :paginator)} "1/2"]]))
|
||||
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::team-form
|
||||
(s/keys :req-un [::name]))
|
||||
|
||||
(mf/defc onboarding-team-modal
|
||||
{::mf/props :obj}
|
||||
[]
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
(ns app.main.ui.settings.access-tokens
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
|
@ -20,8 +20,6 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[app.util.time :as dt]
|
||||
[app.util.webapi :as wapi]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
@ -40,17 +38,10 @@
|
|||
(def token-created-ref
|
||||
(l/derived :access-token-created st/state))
|
||||
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::expiration-date ::us/not-empty-string)
|
||||
(s/def ::access-token-form
|
||||
(s/keys :req-un [::name ::expiration-date]))
|
||||
|
||||
(defn- name-validator
|
||||
[errors data]
|
||||
(let [name (:name data)]
|
||||
(cond-> errors
|
||||
(str/blank? name)
|
||||
(assoc :name {:message (tr "dashboard.access-tokens.errors-required-name")}))))
|
||||
(def ^:private schema:form
|
||||
[:map {:title "AccessTokenForm"}
|
||||
[:name [::sm/text {:max 250}]]
|
||||
[:expiration-date [::sm/text {:max 250}]]])
|
||||
|
||||
(def initial-data
|
||||
{:name "" :expiration-date "never"})
|
||||
|
@ -61,10 +52,8 @@
|
|||
[]
|
||||
(let [form (fm/use-form
|
||||
:initial initial-data
|
||||
:spec ::access-token-form
|
||||
:validators [name-validator
|
||||
(fm/validate-not-empty :name (tr "auth.name.not-all-space"))
|
||||
(fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))])
|
||||
:schema schema:form)
|
||||
|
||||
created (mf/deref token-created-ref)
|
||||
created? (mf/use-state false)
|
||||
locale (mf/deref i18n/locale)
|
||||
|
|
|
@ -7,9 +7,7 @@
|
|||
(ns app.main.ui.settings.change-email
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dma]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
|
@ -20,24 +18,8 @@
|
|||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::email-1 ::us/email)
|
||||
(s/def ::email-2 ::us/email)
|
||||
|
||||
(defn- email-equality
|
||||
[errors data]
|
||||
(let [email-1 (:email-1 data)
|
||||
email-2 (:email-2 data)]
|
||||
(cond-> errors
|
||||
(and email-1 email-2 (not= email-1 email-2))
|
||||
(assoc :email-2 {:message (tr "errors.email-invalid-confirmation")
|
||||
:code :different-emails}))))
|
||||
|
||||
(s/def ::email-change-form
|
||||
(s/keys :req-un [::email-1 ::email-2]))
|
||||
|
||||
(defn- on-error
|
||||
[form error]
|
||||
(case (:code (ex-data error))
|
||||
|
@ -71,30 +53,32 @@
|
|||
:on-success (partial on-success profile)}]
|
||||
(st/emit! (du/request-email-change (with-meta params mdata)))))
|
||||
|
||||
(def ^:private schema:email-change-form
|
||||
[:and
|
||||
[:map {:title "EmailChangeForm"}
|
||||
[:email-1 ::sm/email]
|
||||
[:email-2 ::sm/email]]
|
||||
[:fn {:error/code "errors.invalid-email-confirmation"
|
||||
:error/field :email-2}
|
||||
(fn [data]
|
||||
(let [email-1 (:email-1 data)
|
||||
email-2 (:email-2 data)]
|
||||
(= email-1 email-2)))]])
|
||||
|
||||
(mf/defc change-email-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :change-email}
|
||||
[]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
form (fm/use-form :spec ::email-change-form
|
||||
:validators [email-equality]
|
||||
form (fm/use-form :schema schema:email-change-form
|
||||
:initial profile)
|
||||
on-close
|
||||
(mf/use-callback #(st/emit! (modal/hide)))
|
||||
(mf/use-fn #(st/emit! (modal/hide)))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps profile)
|
||||
(partial on-submit profile))
|
||||
|
||||
on-email-change
|
||||
(mf/use-callback
|
||||
(fn [_ _]
|
||||
(let [different-emails-error? (= (dma/get-in @form [:errors :email-2 :code]) :different-emails)
|
||||
email-1 (dma/get-in @form [:clean-data :email-1])
|
||||
email-2 (dma/get-in @form [:clean-data :email-2])]
|
||||
(when (and different-emails-error? (= email-1 email-2))
|
||||
(swap! form d/dissoc-in [:errors :email-2])))))]
|
||||
(partial on-submit profile))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
|
@ -118,16 +102,14 @@
|
|||
:name :email-1
|
||||
:label (tr "modals.change-email.new-email")
|
||||
:trim true
|
||||
:show-success? true
|
||||
:on-change-value on-email-change}]]
|
||||
:show-success? true}]]
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input {:type "email"
|
||||
:name :email-2
|
||||
:label (tr "modals.change-email.confirm-email")
|
||||
:trim true
|
||||
:show-success? true
|
||||
:on-change-value on-email-change}]]]
|
||||
:show-success? true}]]]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"Feedback form."
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
|
@ -17,25 +17,22 @@
|
|||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::content ::us/not-empty-string)
|
||||
(s/def ::subject ::us/not-empty-string)
|
||||
|
||||
(s/def ::feedback-form
|
||||
(s/keys :req-un [::subject ::content]))
|
||||
(def ^:private schema:feedback-form
|
||||
[:map {:title "FeedbackForm"}
|
||||
[:subject [::sm/text {:max 250}]]
|
||||
[:content [::sm/text {:max 5000}]]])
|
||||
|
||||
(mf/defc feedback-form
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
form (fm/use-form :spec ::feedback-form
|
||||
:validators [(fm/validate-length :subject fm/max-length-allowed (tr "auth.name.too-long"))
|
||||
(fm/validate-not-empty :subject (tr "auth.name.not-all-space"))])
|
||||
form (fm/use-form :schema schema:feedback-form)
|
||||
loading (mf/use-state false)
|
||||
|
||||
on-succes
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps profile)
|
||||
(fn [_]
|
||||
(reset! loading false)
|
||||
|
@ -43,7 +40,7 @@
|
|||
(swap! form assoc :data {} :touched {} :errors {})))
|
||||
|
||||
on-error
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps profile)
|
||||
(fn [{:keys [code] :as error}]
|
||||
(reset! loading false)
|
||||
|
@ -52,7 +49,7 @@
|
|||
(st/emit! (msg/error (tr "errors.generic"))))))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps profile)
|
||||
(fn [form _]
|
||||
(reset! loading true)
|
||||
|
@ -106,8 +103,8 @@
|
|||
|
||||
(mf/defc feedback-page
|
||||
[]
|
||||
(mf/use-effect
|
||||
#(dom/set-html-title (tr "title.settings.feedback")))
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "title.settings.feedback")))
|
||||
|
||||
[:div {:class (stl/css :dashboard-settings)}
|
||||
[:div {:class (stl/css :form-container)}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
(ns app.main.ui.settings.options
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.refs :as refs]
|
||||
|
@ -15,14 +14,12 @@
|
|||
[app.main.ui.components.forms :as fm]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::lang (s/nilable ::us/string))
|
||||
(s/def ::theme (s/nilable ::us/not-empty-string))
|
||||
|
||||
(s/def ::options-form
|
||||
(s/keys :opt-un [::lang ::theme]))
|
||||
(def ^:private schema:options-form
|
||||
[:map {:title "OptionsForm"}
|
||||
[:lang {:optional true} [:string {:max 20}]]
|
||||
[:theme {:optional true} [:string {:max 250}]]])
|
||||
|
||||
(defn- on-success
|
||||
[profile]
|
||||
|
@ -41,7 +38,7 @@
|
|||
(let [profile (mf/deref refs/profile)
|
||||
initial (mf/with-memo [profile]
|
||||
(update profile :lang #(or % "")))
|
||||
form (fm/use-form :spec ::options-form
|
||||
form (fm/use-form :schema schema:options-form
|
||||
:initial initial)]
|
||||
|
||||
[:& fm/form {:class (stl/css :options-form)
|
||||
|
|
|
@ -7,14 +7,13 @@
|
|||
(ns app.main.ui.settings.password
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.users :as udu]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- on-error
|
||||
|
@ -22,10 +21,10 @@
|
|||
(case (:code (ex-data error))
|
||||
:old-password-not-match
|
||||
(swap! form assoc-in [:errors :password-old]
|
||||
{:message (tr "errors.wrong-old-password")})
|
||||
{:code "errors.wrong-old-password"})
|
||||
:email-as-password
|
||||
(swap! form assoc-in [:errors :password-1]
|
||||
{:message (tr "errors.email-as-password")})
|
||||
{:code "errors.email-as-password"})
|
||||
|
||||
(let [msg (tr "generic.error")]
|
||||
(st/emit! (msg/error msg)))))
|
||||
|
@ -47,40 +46,29 @@
|
|||
:on-error (partial on-error form)})]
|
||||
(st/emit! (udu/update-password params))))
|
||||
|
||||
(s/def ::password-1 ::us/not-empty-string)
|
||||
(s/def ::password-2 ::us/not-empty-string)
|
||||
(s/def ::password-old (s/nilable ::us/string))
|
||||
|
||||
(defn- password-equality
|
||||
[errors data]
|
||||
(let [password-1 (:password-1 data)
|
||||
password-2 (:password-2 data)]
|
||||
|
||||
(cond-> errors
|
||||
(and password-1 password-2 (not= password-1 password-2))
|
||||
(assoc :password-2 {:message (tr "errors.password-invalid-confirmation")})
|
||||
|
||||
(and password-1 (> 8 (count password-1)))
|
||||
(assoc :password-1 {:message (tr "errors.password-too-short")}))))
|
||||
|
||||
(s/def ::password-form
|
||||
(s/keys :req-un [::password-1
|
||||
::password-2
|
||||
::password-old]))
|
||||
(def ^:private schema:password-form
|
||||
[:and
|
||||
[:map {:title "PasswordForm"}
|
||||
[:password-1 ::sm/password]
|
||||
[:password-2 ::sm/password]
|
||||
[:password-old ::sm/password]]
|
||||
[:fn {:error/code "errors.password-invalid-confirmation"
|
||||
:error/field :password-2}
|
||||
(fn [{:keys [password-1 password-2]}]
|
||||
(= password-1 password-2))]])
|
||||
|
||||
(mf/defc password-form
|
||||
[]
|
||||
(let [initial (mf/use-memo (constantly {:password-old nil}))
|
||||
form (fm/use-form :spec ::password-form
|
||||
:validators [(fm/validate-not-all-spaces :password-old (tr "auth.password-not-empty"))
|
||||
(fm/validate-not-empty :password-1 (tr "auth.password-not-empty"))
|
||||
(fm/validate-not-empty :password-2 (tr "auth.password-not-empty"))
|
||||
password-equality]
|
||||
:initial initial)]
|
||||
(let [initial (mf/with-memo []
|
||||
{:password-old ""
|
||||
:password-1 ""
|
||||
:password-2 ""})
|
||||
form (fm/use-form :schema schema:password-form
|
||||
:initial initial)]
|
||||
|
||||
[:& fm/form {:class (stl/css :password-form)
|
||||
:on-submit on-submit
|
||||
:form form}
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input
|
||||
{:type "password"
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
(ns app.main.ui.settings.profile
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
|
@ -18,14 +18,12 @@
|
|||
[app.main.ui.components.forms :as fm]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::email ::us/email)
|
||||
|
||||
(s/def ::profile-form
|
||||
(s/keys :req-un [::fullname ::email]))
|
||||
(def ^:private schema:profile-form
|
||||
[:map {:title "ProfileForm"}
|
||||
[:fullname [::sm/text {:max 250}]]
|
||||
[:email ::sm/email]])
|
||||
|
||||
(defn- on-submit
|
||||
[form _event]
|
||||
|
@ -37,19 +35,18 @@
|
|||
;; --- Profile Form
|
||||
|
||||
(mf/defc profile-form
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
form (fm/use-form :spec ::profile-form
|
||||
:initial profile
|
||||
:validators [(fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))
|
||||
(fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))])
|
||||
form (fm/use-form :schema schema:profile-form
|
||||
:initial profile)
|
||||
|
||||
handle-show-change-email
|
||||
(mf/use-callback
|
||||
on-show-change-email
|
||||
(mf/use-fn
|
||||
#(modal/show! :change-email {}))
|
||||
|
||||
handle-show-delete-account
|
||||
(mf/use-callback
|
||||
on-show-delete-account
|
||||
(mf/use-fn
|
||||
#(modal/show! :delete-account {}))]
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
|
@ -62,7 +59,7 @@
|
|||
:label (tr "dashboard.your-name")}]]
|
||||
|
||||
[:div {:class (stl/css :fields-row)
|
||||
:on-click handle-show-change-email}
|
||||
:on-click on-show-change-email}
|
||||
[:& fm/input
|
||||
{:type "email"
|
||||
:name :email
|
||||
|
@ -71,7 +68,7 @@
|
|||
|
||||
[:div {:class (stl/css :options)}
|
||||
[:div.change-email
|
||||
[:a {:on-click handle-show-change-email}
|
||||
[:a {:on-click on-show-change-email}
|
||||
(tr "dashboard.change-email")]]]]
|
||||
|
||||
[:> fm/submit-button*
|
||||
|
@ -81,17 +78,25 @@
|
|||
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :link-item)}
|
||||
[:a {:on-click handle-show-delete-account
|
||||
[:a {:on-click on-show-delete-account
|
||||
:data-testid "remove-acount-btn"}
|
||||
(tr "dashboard.remove-account")]]]]))
|
||||
|
||||
;; --- Profile Photo Form
|
||||
|
||||
(mf/defc profile-photo-form []
|
||||
(let [file-input (mf/use-ref nil)
|
||||
profile (mf/deref refs/profile)
|
||||
photo (cf/resolve-profile-photo-url profile)
|
||||
on-image-click #(dom/click (mf/ref-val file-input))
|
||||
(mf/defc profile-photo-form
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [input-ref (mf/use-ref nil)
|
||||
profile (mf/deref refs/profile)
|
||||
|
||||
photo
|
||||
(mf/with-memo [profile]
|
||||
(cf/resolve-profile-photo-url profile))
|
||||
|
||||
on-image-click
|
||||
(mf/use-fn
|
||||
#(dom/click (mf/ref-val input-ref)))
|
||||
|
||||
on-file-selected
|
||||
(fn [file]
|
||||
|
@ -104,15 +109,17 @@
|
|||
[:img {:src photo}]
|
||||
[:& file-uploader {:accept "image/jpeg,image/png"
|
||||
:multi false
|
||||
:ref file-input
|
||||
:ref input-ref
|
||||
:on-selected on-file-selected
|
||||
:data-testid "profile-image-input"}]]]))
|
||||
|
||||
;; --- Profile Page
|
||||
|
||||
(mf/defc profile-page []
|
||||
(mf/defc profile-page
|
||||
[]
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "title.settings.profile")))
|
||||
|
||||
[:div {:class (stl/css :dashboard-settings)}
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:h2 (tr "labels.profile")]
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.store :as st]
|
||||
|
@ -18,7 +18,6 @@
|
|||
[app.main.ui.workspace.sidebar.assets.common :as cmm]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc asset-group-title
|
||||
|
@ -92,21 +91,18 @@
|
|||
(compare key1 key2))))
|
||||
assets)))
|
||||
|
||||
(s/def ::asset-name ::us/not-empty-string)
|
||||
(s/def ::name-group-form
|
||||
(s/keys :req-un [::asset-name]))
|
||||
(def ^:private schema:group-form
|
||||
[:map {:title "GroupForm"}
|
||||
[:name [::sm/text {:max 250}]]])
|
||||
|
||||
(mf/defc name-group-dialog
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :name-group-dialog}
|
||||
[{:keys [path last-path accept] :as ctx
|
||||
:or {path "" last-path ""}}]
|
||||
(let [initial (mf/use-memo
|
||||
(mf/deps last-path)
|
||||
(constantly {:asset-name last-path}))
|
||||
form (fm/use-form :spec ::name-group-form
|
||||
:validators [(fm/validate-not-empty :asset-name (tr "auth.name.not-all-space"))
|
||||
(fm/validate-length :asset-name fm/max-length-allowed (tr "auth.name.too-long"))]
|
||||
(let [initial (mf/with-memo [last-path]
|
||||
{:asset-name last-path})
|
||||
form (fm/use-form :schema schema:group-form
|
||||
:initial initial)
|
||||
|
||||
create? (empty? path)
|
||||
|
@ -117,7 +113,7 @@
|
|||
(mf/use-fn
|
||||
(mf/deps form)
|
||||
(fn [_]
|
||||
(let [asset-name (get-in @form [:clean-data :asset-name])]
|
||||
(let [asset-name (get-in @form [:clean-data :name])]
|
||||
(if create?
|
||||
(accept asset-name)
|
||||
(accept path asset-name))
|
||||
|
@ -135,7 +131,7 @@
|
|||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:& fm/form {:form form :on-submit on-accept}
|
||||
[:& fm/input {:name :asset-name
|
||||
[:& fm/input {:name :name
|
||||
:class (stl/css :input-wrapper)
|
||||
:auto-focus? true
|
||||
:label (tr "workspace.assets.group-name")
|
||||
|
|
|
@ -8,119 +8,146 @@
|
|||
(:refer-clojure :exclude [uuid])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.spec :as us]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[malli.core :as m]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- Handlers Helpers
|
||||
|
||||
(defn- interpret-problem
|
||||
[acc {:keys [path pred via] :as problem}]
|
||||
(cond
|
||||
(and (empty? path)
|
||||
(list? pred)
|
||||
(= (first (last pred)) 'cljs.core/contains?))
|
||||
(let [field (last (last pred))
|
||||
path (conj path field)
|
||||
root (first via)]
|
||||
(assoc-in acc path {:code :missing :type :builtin :root root :field field}))
|
||||
(defn- interpret-schema-problem
|
||||
[acc {:keys [schema in value] :as problem}]
|
||||
(let [props (merge (m/type-properties schema)
|
||||
(m/properties schema))
|
||||
field (or (first in) (:error/field props))]
|
||||
|
||||
(and (seq path) (seq via))
|
||||
(let [field (first path)
|
||||
code (last via)
|
||||
root (first via)]
|
||||
(assoc-in acc path {:code code :type :builtin :root root :field field}))
|
||||
(if (contains? acc field)
|
||||
acc
|
||||
(cond
|
||||
(nil? value)
|
||||
(assoc acc field {:code "errors.field-missing"})
|
||||
|
||||
:else acc))
|
||||
(contains? props :error/code)
|
||||
(assoc acc field {:code (:error/code props)})
|
||||
|
||||
(declare create-form-mutator)
|
||||
(contains? props :error/message)
|
||||
(assoc acc field {:code (:error/message props)})
|
||||
|
||||
(defn use-form
|
||||
[& {:keys [initial] :as opts}]
|
||||
(let [state (mf/useState 0)
|
||||
render (aget state 1)
|
||||
(contains? props :error/fn)
|
||||
(let [v-fn (:error/fn props)
|
||||
code (v-fn problem)]
|
||||
(assoc acc field {:code code}))
|
||||
|
||||
get-state (mf/use-callback
|
||||
(mf/deps initial)
|
||||
(fn []
|
||||
{:data (if (fn? initial) (initial) initial)
|
||||
:errors {}
|
||||
:touched {}}))
|
||||
(contains? props :error/validators)
|
||||
(let [validators (:error/validators props)
|
||||
props (reduce #(%2 %1 value) props validators)]
|
||||
(assoc acc field {:code (d/nilv (:error/code props) "errors.invalid-data")}))
|
||||
|
||||
state-ref (mf/use-ref (get-state))
|
||||
form (mf/use-memo (mf/deps initial) #(create-form-mutator state-ref render get-state opts))]
|
||||
:else
|
||||
(assoc acc field {:code "errors.invalid-data"})))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps initial)
|
||||
(defn- use-rerender-fn
|
||||
[]
|
||||
(let [state (mf/useState 0)
|
||||
render-fn (aget state 1)]
|
||||
(mf/use-fn
|
||||
(mf/deps render-fn)
|
||||
(fn []
|
||||
(if (fn? initial)
|
||||
(swap! form update :data merge (initial))
|
||||
(swap! form update :data merge initial))))
|
||||
(render-fn inc)))))
|
||||
|
||||
form))
|
||||
(defn- apply-validators
|
||||
[validators state errors]
|
||||
(reduce (fn [errors validator-fn]
|
||||
(merge errors (validator-fn errors (:data state))))
|
||||
errors
|
||||
validators))
|
||||
|
||||
(defn- wrap-update-fn
|
||||
[f {:keys [spec validators]}]
|
||||
(defn- collect-schema-errors
|
||||
[schema validators state]
|
||||
(let [explain (sm/explain schema (:data state))
|
||||
errors (->> (reduce interpret-schema-problem {} (:errors explain))
|
||||
(apply-validators validators state))]
|
||||
|
||||
(-> (:errors state)
|
||||
(merge errors)
|
||||
(d/without-nils)
|
||||
(not-empty))))
|
||||
|
||||
(defn- wrap-update-schema-fn
|
||||
[f {:keys [schema validators]}]
|
||||
(fn [& args]
|
||||
(let [state (apply f args)
|
||||
cleaned (s/conform spec (:data state))
|
||||
problems (when (= ::s/invalid cleaned)
|
||||
(::s/problems (s/explain-data spec (:data state))))
|
||||
|
||||
errors (reduce interpret-problem {} problems)
|
||||
|
||||
|
||||
errors (reduce (fn [errors vf]
|
||||
(merge errors (vf errors (:data state))))
|
||||
errors
|
||||
validators)
|
||||
errors (merge (:errors state) errors)
|
||||
errors (d/without-nils errors)]
|
||||
|
||||
(let [state (apply f args)
|
||||
cleaned (sm/decode schema (:data state))
|
||||
valid? (sm/validate schema cleaned)
|
||||
errors (when-not valid?
|
||||
(collect-schema-errors schema validators state))]
|
||||
|
||||
(assoc state
|
||||
:errors errors
|
||||
:clean-data (when (not= cleaned ::s/invalid) cleaned)
|
||||
:valid (and (empty? errors)
|
||||
(not= cleaned ::s/invalid))))))
|
||||
:clean-data (when valid? cleaned)
|
||||
:valid (and (not errors) valid?)))))
|
||||
|
||||
(defn- create-form-mutator
|
||||
[state-ref render get-state opts]
|
||||
[internal-state rerender-fn wrap-update-fn initial opts]
|
||||
(reify
|
||||
IDeref
|
||||
(-deref [_]
|
||||
(mf/ref-val state-ref))
|
||||
(mf/ref-val internal-state))
|
||||
|
||||
IReset
|
||||
(-reset! [_ new-value]
|
||||
(if (nil? new-value)
|
||||
(mf/set-ref-val! state-ref (get-state))
|
||||
(mf/set-ref-val! state-ref new-value))
|
||||
(render inc))
|
||||
(mf/set-ref-val! internal-state (if (fn? initial) (initial) initial))
|
||||
(mf/set-ref-val! internal-state new-value))
|
||||
(rerender-fn))
|
||||
|
||||
ISwap
|
||||
(-swap! [_ f]
|
||||
(let [f (wrap-update-fn f opts)]
|
||||
(mf/set-ref-val! state-ref (f (mf/ref-val state-ref)))
|
||||
(render inc)))
|
||||
|
||||
(mf/set-ref-val! internal-state (f (mf/ref-val internal-state)))
|
||||
(rerender-fn)))
|
||||
|
||||
(-swap! [_ f x]
|
||||
(let [f (wrap-update-fn f opts)]
|
||||
(mf/set-ref-val! state-ref (f (mf/ref-val state-ref) x))
|
||||
(render inc)))
|
||||
(mf/set-ref-val! internal-state (f (mf/ref-val internal-state) x))
|
||||
(rerender-fn)))
|
||||
|
||||
(-swap! [_ f x y]
|
||||
(let [f (wrap-update-fn f opts)]
|
||||
(mf/set-ref-val! state-ref (f (mf/ref-val state-ref) x y))
|
||||
(render inc)))
|
||||
(mf/set-ref-val! internal-state (f (mf/ref-val internal-state) x y))
|
||||
(rerender-fn)))
|
||||
|
||||
(-swap! [_ f x y more]
|
||||
(let [f (wrap-update-fn f opts)]
|
||||
(mf/set-ref-val! state-ref (apply f (mf/ref-val state-ref) x y more))
|
||||
(render inc)))))
|
||||
(mf/set-ref-val! internal-state (apply f (mf/ref-val internal-state) x y more))
|
||||
(rerender-fn)))))
|
||||
|
||||
(defn use-form
|
||||
[& {:keys [initial] :as opts}]
|
||||
(let [rerender-fn (use-rerender-fn)
|
||||
|
||||
internal-state
|
||||
(mf/use-ref nil)
|
||||
|
||||
form-mutator
|
||||
(mf/with-memo [initial]
|
||||
(create-form-mutator internal-state rerender-fn wrap-update-schema-fn initial opts))]
|
||||
|
||||
;; Initialize internal state once
|
||||
(mf/with-effect []
|
||||
(mf/set-ref-val! internal-state
|
||||
{:data {}
|
||||
:errors {}
|
||||
:touched {}}))
|
||||
|
||||
(mf/with-effect [initial]
|
||||
(if (fn? initial)
|
||||
(swap! form-mutator update :data merge (initial))
|
||||
(swap! form-mutator update :data merge initial)))
|
||||
|
||||
form-mutator))
|
||||
|
||||
(defn on-input-change
|
||||
([form field value]
|
||||
|
@ -150,8 +177,8 @@
|
|||
(mf/defc field-error
|
||||
[{:keys [form field type]
|
||||
:as props}]
|
||||
(let [{:keys [message] :as error} (get-in form [:errors field])
|
||||
touched? (get-in form [:touched field])
|
||||
(let [{:keys [message] :as error} (dm/get-in form [:errors field])
|
||||
touched? (dm/get-in form [:touched field])
|
||||
show? (and touched? error message
|
||||
(cond
|
||||
(nil? type) true
|
||||
|
@ -164,12 +191,6 @@
|
|||
|
||||
(defn error-class
|
||||
[form field]
|
||||
(when (and (get-in form [:errors field])
|
||||
(get-in form [:touched field]))
|
||||
(when (and (dm/get-in form [:errors field])
|
||||
(dm/get-in form [:touched field]))
|
||||
"invalid"))
|
||||
|
||||
;; --- Form Specs and Conformers
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::not-empty-string ::us/not-empty-string)
|
||||
(s/def ::color ::us/rgb-color-str)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue