Merge branch 'main' into develop

This commit is contained in:
Andrey Antukh 2021-03-08 11:58:48 +01:00
commit fdeaac7f65
11 changed files with 183 additions and 123 deletions

View file

@ -49,8 +49,10 @@
(declare register-profile) (declare register-profile)
(s/def ::invitation-token ::us/not-empty-string) (s/def ::invitation-token ::us/not-empty-string)
(s/def ::terms-privacy ::us/boolean)
(s/def ::register-profile (s/def ::register-profile
(s/keys :req-un [::email ::password ::fullname] (s/keys :req-un [::email ::password ::fullname ::terms-privacy]
:opt-un [::invitation-token])) :opt-un [::invitation-token]))
(sv/defmethod ::register-profile {:auth false :rlimit :password} (sv/defmethod ::register-profile {:auth false :rlimit :password}
@ -63,6 +65,10 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :email-domain-is-not-allowed)) :code :email-domain-is-not-allowed))
(when-not (:terms-privacy params)
(ex/raise :type :validation
:code :invalid-terms-and-privacy))
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)] (let [cfg (assoc cfg :conn conn)]
(register-profile cfg params)))) (register-profile cfg params))))
@ -331,7 +337,8 @@
{:id id})) {:id id}))
(s/def ::update-profile (s/def ::update-profile
(s/keys :req-un [::id ::fullname ::lang ::theme])) (s/keys :req-un [::id ::fullname]
:opt-un [::lang ::theme]))
(sv/defmethod ::update-profile (sv/defmethod ::update-profile
[{:keys [pool] :as cfg} params] [{:keys [pool] :as cfg} params]

View file

@ -190,6 +190,32 @@
(t/testing "not allowed email domain" (t/testing "not allowed email domain"
(t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com")))))) (t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
(t/deftest test-register-with-no-terms-and-privacy
(let [data {::th/type :register-profile
:email "user@example.com"
:password "foobar"
:fullname "foobar"
:terms-privacy nil}
out (th/mutation! data)
error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :spec-validation))))
(t/deftest test-register-with-bad-terms-and-privacy
(let [data {::th/type :register-profile
:email "user@example.com"
:password "foobar"
:fullname "foobar"
:terms-privacy false}
out (th/mutation! data)
error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :invalid-terms-and-privacy))))
(t/deftest test-register-when-registration-disabled (t/deftest test-register-when-registration-disabled
(with-mocks [mock {:target 'app.config/get (with-mocks [mock {:target 'app.config/get
:return (th/mock-config-get-with :return (th/mock-config-get-with
@ -197,7 +223,8 @@
(let [data {::th/type :register-profile (let [data {::th/type :register-profile
:email "user@example.com" :email "user@example.com"
:password "foobar" :password "foobar"
:fullname "foobar"} :fullname "foobar"
:terms-privacy true}
out (th/mutation! data) out (th/mutation! data)
error (:error out) error (:error out)
edata (ex-data error)] edata (ex-data error)]
@ -210,7 +237,8 @@
data {::th/type :register-profile data {::th/type :register-profile
:email (:email profile) :email (:email profile)
:password "foobar" :password "foobar"
:fullname "foobar"} :fullname "foobar"
:terms-privacy true}
out (th/mutation! data) out (th/mutation! data)
error (:error out) error (:error out)
edata (ex-data error)] edata (ex-data error)]
@ -225,7 +253,8 @@
data {::th/type :register-profile data {::th/type :register-profile
:email "user@example.com" :email "user@example.com"
:password "foobar" :password "foobar"
:fullname "foobar"} :fullname "foobar"
:terms-privacy true}
out (th/mutation! data)] out (th/mutation! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(let [mock (deref mock) (let [mock (deref mock)
@ -250,7 +279,8 @@
data {::th/type :register-profile data {::th/type :register-profile
:email "user@example.com" :email "user@example.com"
:password "foobar" :password "foobar"
:fullname "foobar"} :fullname "foobar"
:terms-privacy true}
_ (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"}) _ (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
out (th/mutation! data)] out (th/mutation! data)]
;; (th/print-result! out) ;; (th/print-result! out)
@ -270,7 +300,8 @@
data {::th/type :register-profile data {::th/type :register-profile
:email "user@example.com" :email "user@example.com"
:password "foobar" :password "foobar"
:fullname "foobar"} :fullname "foobar"
:terms-privacy true}
_ (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"}) _ (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
out (th/mutation! data)] out (th/mutation! data)]

View file

@ -369,6 +369,13 @@
}, },
"used-in" : [ "src/app/main/ui/auth.cljs" ] "used-in" : [ "src/app/main/ui/auth.cljs" ]
}, },
"auth.terms-privacy-agreement" : {
"translations" : {
"en" : "When creating a new account, you agree to our terms of service and privacy policy.",
"es" : "Al crear una nueva cuenta, aceptas nuestros términos de servicio y política de privacidad."
},
"used-in" : [ "src/app/main/ui/auth/register.cljs" ]
},
"auth.verification-email-sent" : { "auth.verification-email-sent" : {
"translations" : { "translations" : {
"ca" : "Em enviat un correu de verificació a", "ca" : "Em enviat un correu de verificació a",
@ -941,6 +948,13 @@
}, },
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs" ] "used-in" : [ "src/app/main/ui/dashboard/grid.cljs" ]
}, },
"errors.terms-privacy-agreement-invalid" : {
"translations" : {
"en" : "You must accept our terms of service and privacy policy.",
"es" : "Debes aceptar nuestros términos de servicio y política de privacidad."
},
"used-in" : [ "src/app/main/ui/auth/register.cljs" ]
},
"errors.clipboard-not-implemented" : { "errors.clipboard-not-implemented" : {
"translations" : { "translations" : {
"ca" : "El teu navegador no pot realitzar aquesta operació", "ca" : "El teu navegador no pot realitzar aquesta operació",

View file

@ -561,27 +561,28 @@ input.element-name {
.input-radio, .input-radio,
.input-checkbox { .input-checkbox {
align-items: center; align-items: center;
color: $color-gray-40;
display: flex; display: flex;
margin-bottom: 10px; margin-bottom: 10px;
margin-top: 10px; margin-top: 10px;
padding-left: 0px; padding-left: 0px;
label{ label{
align-items: center;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
margin-right: 15px; margin-right: 15px;
font-size: $fs13; font-size: $fs12;
&:before{ &:before{
content:""; content:"";
width: 20px; width: 20px;
height: 20px; height: 20px;
margin-right: 10px; margin-right: 10px;
background-color: $color-gray-50; background-color: $color-gray-10;
border: 1px solid $color-gray-60; border: 1px solid $color-gray-30;
box-shadow: inset 0 0 0 0 $color-primary; box-shadow: inset 0 0 0 0 $color-primary;
box-sizing: border-box; box-sizing: border-box;
flex-shrink: 0;
} }
} }
@ -676,7 +677,6 @@ input[type=radio]:checked + label:before{
label { label {
transition: border 0.2s linear 0s, color 0.2s linear 0s; transition: border 0.2s linear 0s, color 0.2s linear 0s;
white-space: nowrap;
position: relative; position: relative;
&:before { &:before {
@ -687,11 +687,11 @@ input[type=radio]:checked + label:before{
&::after { &::after {
display: inline-block; display: inline-block;
width: 16px; width: 20px;
height: 16px; height: 20px;
position: absolute; position: absolute;
left: 3.2px; left: 3.2px;
top: 0px; top: 0;
font-size: $fs11; font-size: $fs11;
transition: border 0.2s linear 0s, color 0.2s linear 0s; transition: border 0.2s linear 0s, color 0.2s linear 0s;
} }
@ -732,7 +732,7 @@ input[type=radio]:checked + label:before{
&::after { &::after {
content:""; content:"";
color: #000000; color: #ffffff;
font-size: $fs16; font-size: $fs16;
} }

View file

@ -275,18 +275,6 @@
margin-top: 15px; margin-top: 15px;
} }
.input-checkbox {
margin: 0;
position: absolute;
top: 10px;
right: 5px;
label {
margin: 0;
}
}
} }
// STYLES FOR LIBRARIES // STYLES FOR LIBRARIES

View file

@ -33,7 +33,7 @@
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::password ::us/string) (s/def ::password ::us/string)
(s/def ::lang (s/nilable ::us/string)) (s/def ::lang (s/nilable ::us/string))
(s/def ::theme ::us/string) (s/def ::theme (s/nilable ::us/string))
(s/def ::created-at ::us/inst) (s/def ::created-at ::us/inst)
(s/def ::password-1 ::us/string) (s/def ::password-1 ::us/string)
(s/def ::password-2 ::us/string) (s/def ::password-2 ::us/string)
@ -55,17 +55,15 @@
(ptk/reify ::profile-fetched (ptk/reify ::profile-fetched
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc state :profile (assoc state :profile data))
(cond-> data
(nil? (:theme data))
(assoc :theme cfg/default-theme))))
ptk/EffectEvent ptk/EffectEvent
(effect [_ state stream] (effect [_ state stream]
(let [profile (:profile state)] (let [profile (:profile state)]
(swap! storage assoc :profile profile) (swap! storage assoc :profile profile)
(i18n/set-locale! (:lang profile)) (i18n/set-locale! (:lang profile))
(theme/set-current-theme! (:theme profile)))))) (some-> (:theme profile)
(theme/set-current-theme!))))))
;; --- Fetch Profile ;; --- Fetch Profile
@ -91,16 +89,19 @@
(watch [_ state stream] (watch [_ state stream]
(let [mdata (meta data) (let [mdata (meta data)
on-success (:on-success mdata identity) on-success (:on-success mdata identity)
on-error (:on-error mdata identity)] on-error (:on-error mdata #(rx/throw %))]
(rx/merge
(->> (rp/mutation :update-profile data) (->> (rp/mutation :update-profile data)
(rx/map fetch-profile) (rx/catch on-error)
(rx/catch on-error)) (rx/mapcat
(fn [_]
(rx/merge
(->> stream (->> stream
(rx/filter (ptk/type? ::profile-fetched)) (rx/filter (ptk/type? ::profile-fetched))
(rx/take 1) (rx/take 1)
(rx/tap on-success) (rx/tap on-success)
(rx/ignore))))))) (rx/ignore))
(rx/of (profile-fetched data))))))))))
;; --- Request Email Change ;; --- Request Email Change

View file

@ -36,17 +36,23 @@
(defn- validate (defn- validate
[data] [data]
(let [password (:password data)] (let [password (:password data)
(when (> 8 (count password)) terms-privacy (:terms-privacy data)]
{:password {:message "errors.password-too-short"}}))) (cond-> {}
(> 8 (count password))
(assoc :password {:message "errors.password-too-short"})
(and (not terms-privacy) false)
(assoc :terms-privacy {:message "errors.terms-privacy-agreement-invalid"}))))
(s/def ::fullname ::us/not-empty-string) (s/def ::fullname ::us/not-empty-string)
(s/def ::password ::us/not-empty-string) (s/def ::password ::us/not-empty-string)
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::invitation-token ::us/not-empty-string) (s/def ::invitation-token ::us/not-empty-string)
(s/def ::terms-privacy ::us/boolean)
(s/def ::register-form (s/def ::register-form
(s/keys :req-un [::password ::fullname ::email] (s/keys :req-un [::password ::fullname ::email ::terms-privacy]
:opt-un [::invitation-token])) :opt-un [::invitation-token]))
(mf/defc register-form (mf/defc register-form
@ -113,10 +119,16 @@
:label (tr "auth.password") :label (tr "auth.password")
:type "password"}]] :type "password"}]]
[:div.fields-row
[:& fm/input {:name :terms-privacy
:class "check-primary"
:tab-index "4"
:label (tr "auth.terms-privacy-agreement")
:type "checkbox"}]]
[:& fm/submit-button [:& fm/submit-button
{:label (tr "auth.register-submit") {:label (tr "auth.register-submit")
:disabled @submitted? :disabled @submitted?}]]))
}]]))
;; --- Register Page ;; --- Register Page

View file

@ -9,70 +9,84 @@
(ns app.main.ui.components.forms (ns app.main.ui.components.forms
(:require (:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[app.common.data :as d] [app.common.data :as d]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.object :as obj] [app.util.dom :as dom]
[app.util.forms :as fm] [app.util.forms :as fm]
[app.util.i18n :as i18n :refer [t tr]] [app.util.i18n :as i18n :refer [tr]]
["react" :as react] [app.util.object :as obj]
[app.util.dom :as dom])) [cuerdas.core :as str]
[rumext.alpha :as mf]))
(def form-ctx (mf/create-context nil)) (def form-ctx (mf/create-context nil))
(def use-form fm/use-form) (def use-form fm/use-form)
(mf/defc input (mf/defc input
[{:keys [type label help-icon disabled name form hint trim] :as props}] [{:keys [label help-icon disabled form hint trim] :as props}]
(let [form (or form (mf/use-ctx form-ctx)) (let [input-type (get props :type)
input-name (get props :name)
more-classes (get props :class)
type' (mf/use-state type) form (or form (mf/use-ctx form-ctx))
type' (mf/use-state input-type)
focus? (mf/use-state false) focus? (mf/use-state false)
touched? (get-in @form [:touched name]) is-checkbox? (= @type' "checkbox")
error (get-in @form [:errors name]) is-radio? (= @type' "radio")
is-text? (or (= @type' "password")
(= @type' "text")
(= @type' "email"))
value (get-in @form [:data name] "") touched? (get-in @form [:touched input-name])
error (get-in @form [:errors input-name])
value (get-in @form [:data input-name] "")
help-icon' (cond help-icon' (cond
(and (= type "password") (and (= input-type "password")
(= @type' "password")) (= @type' "password"))
i/eye i/eye
(and (= type "password") (and (= input-type "password")
(= @type' "text")) (= @type' "text"))
i/eye-closed i/eye-closed
:else :else
help-icon) help-icon)
klass (dom/classnames klass (str more-classes " "
(dom/classnames
:focus @focus? :focus @focus?
:valid (and touched? (not error)) :valid (and touched? (not error))
:invalid (and touched? error) :invalid (and touched? error)
:disabled disabled :disabled disabled
:empty (str/empty? value) :empty (and is-text? (str/empty? value))
:with-icon (not (nil? help-icon'))) :with-icon (not (nil? help-icon'))
:custom-input is-text?
:input-radio is-radio?
:input-checkbox is-checkbox?))
swap-text-password swap-text-password
(fn [] (fn []
(swap! type' (fn [type] (swap! type' (fn [input-type]
(if (= "password" type) (if (= "password" input-type)
"text" "text"
"password")))) "password"))))
on-focus #(reset! focus? true) on-focus #(reset! focus? true)
on-change (fm/on-input-change form name trim) on-change (fm/on-input-change form input-name trim)
on-blur on-blur
(fn [event] (fn [_]
(reset! focus? false) (reset! focus? false)
(when-not (get-in @form [:touched name]) (when-not (get-in @form [:touched input-name])
(swap! form assoc-in [:touched name] true))) (swap! form assoc-in [:touched input-name] true)))
props (-> props props (-> props
(dissoc :help-icon :form :trim) (dissoc :help-icon :form :trim)
(assoc :value value (assoc :id (name input-name)
:value value
:on-focus on-focus :on-focus on-focus
:on-blur on-blur :on-blur on-blur
:placeholder label :placeholder label
@ -80,15 +94,15 @@
:type @type') :type @type')
(obj/clj->props))] (obj/clj->props))]
[:div.custom-input [:div
{:class klass} {:class klass}
[:* [:*
[:label label]
[:> :input props] [:> :input props]
[:label {:for (name input-name)} label]
(when help-icon' (when help-icon'
[:div.help-icon [:div.help-icon
{:style {:cursor "pointer"} {:style {:cursor "pointer"}
:on-click (when (= "password" type) :on-click (when (= "password" input-type)
swap-text-password)} swap-text-password)}
help-icon']) help-icon'])
(cond (cond
@ -100,16 +114,17 @@
(mf/defc textarea (mf/defc textarea
[{:keys [label disabled name form hint trim] :as props}] [{:keys [label disabled form hint trim] :as props}]
(let [form (or form (mf/use-ctx form-ctx)) (let [input-name (get props :name)
form (or form (mf/use-ctx form-ctx))
type' (mf/use-state type)
focus? (mf/use-state false) focus? (mf/use-state false)
touched? (get-in @form [:touched name]) touched? (get-in @form [:touched input-name])
error (get-in @form [:errors name]) error (get-in @form [:errors input-name])
value (get-in @form [:data name] "") value (get-in @form [:data input-name] "")
klass (dom/classnames klass (dom/classnames
:focus @focus? :focus @focus?
@ -120,13 +135,13 @@
) )
on-focus #(reset! focus? true) on-focus #(reset! focus? true)
on-change (fm/on-input-change form name trim) on-change (fm/on-input-change form input-name trim)
on-blur on-blur
(fn [event] (fn [_]
(reset! focus? false) (reset! focus? false)
(when-not (get-in @form [:touched name]) (when-not (get-in @form [:touched input-name])
(swap! form assoc-in [:touched name] true))) (swap! form assoc-in [:touched input-name] true)))
props (-> props props (-> props
(dissoc :help-icon :form :trim) (dissoc :help-icon :form :trim)
@ -134,8 +149,7 @@
:on-focus on-focus :on-focus on-focus
:on-blur on-blur :on-blur on-blur
;; :placeholder label ;; :placeholder label
:on-change on-change :on-change on-change)
:type @type')
(obj/clj->props))] (obj/clj->props))]
[:div.custom-input [:div.custom-input
@ -151,12 +165,14 @@
[:span.hint hint])]])) [:span.hint hint])]]))
(mf/defc select (mf/defc select
[{:keys [options label name form default] [{:keys [options label form default] :as props
:or {default ""}}] :or {default ""}}]
(let [form (or form (mf/use-ctx form-ctx)) (let [input-name (get props :name)
value (get-in @form [:data name] default)
form (or form (mf/use-ctx form-ctx))
value (or (get-in @form [:data input-name]) default)
cvalue (d/seek #(= value (:value %)) options) cvalue (d/seek #(= value (:value %)) options)
on-change (fm/on-input-change form name)] on-change (fm/on-input-change form input-name)]
[:div.custom-select [:div.custom-select
[:select {:value value [:select {:value value

View file

@ -9,8 +9,8 @@
(ns app.main.ui.settings.options (ns app.main.ui.settings.options
(:require (:require
[app.common.spec :as us]
[app.common.data :as d] [app.common.data :as d]
[app.common.spec :as us]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.refs :as refs] [app.main.refs :as refs]
@ -28,10 +28,6 @@
(s/def ::options-form (s/def ::options-form
(s/keys :opt-un [::lang ::theme])) (s/keys :opt-un [::lang ::theme]))
(defn- on-error
[form error]
(st/emit! (dm/error (tr "errors.generic"))))
(defn- on-success (defn- on-success
[form] [form]
(st/emit! (dm/success (tr "notifications.profile-saved")))) (st/emit! (dm/success (tr "notifications.profile-saved"))))
@ -42,8 +38,7 @@
data (cond-> data data (cond-> data
(empty? (:lang data)) (empty? (:lang data))
(assoc :lang nil)) (assoc :lang nil))
mdata {:on-success (partial on-success form) mdata {:on-success (partial on-success form)}]
:on-error (partial on-error form)}]
(st/emit! (du/update-profile (with-meta data mdata))))) (st/emit! (du/update-profile (with-meta data mdata)))))
(mf/defc options-form (mf/defc options-form

View file

@ -31,24 +31,18 @@
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::profile-form (s/def ::profile-form
(s/keys :req-un [::fullname ::lang ::theme ::email])) (s/keys :req-un [::fullname ::email]))
(defn- on-success (defn- on-success
[form] [form]
(st/emit! (dm/success (tr "notifications.profile-saved")))) (st/emit! (dm/success (tr "notifications.profile-saved"))))
(defn- on-error
[form error]
(st/emit! (dm/error (tr "errors.generic"))))
(defn- on-submit (defn- on-submit
[form event] [form event]
(let [data (:clean-data @form) (let [data (:clean-data @form)
mdata {:on-success (partial on-success form) mdata {:on-success (partial on-success form)}]
:on-error (partial on-error form)}]
(st/emit! (du/update-profile (with-meta data mdata))))) (st/emit! (du/update-profile (with-meta data mdata)))))
;; --- Profile Form ;; --- Profile Form
(mf/defc profile-form (mf/defc profile-form

View file

@ -89,7 +89,6 @@
(mf/set-ref-val! state-ref new-value) (mf/set-ref-val! state-ref new-value)
(render inc)) (render inc))
ISwap ISwap
(-swap! [self f] (-swap! [self f]
(let [f (wrap-update-fn f opts)] (let [f (wrap-update-fn f opts)]
@ -119,7 +118,10 @@
([form field trim?] ([form field trim?]
(fn [event] (fn [event]
(let [target (dom/get-target event) (let [target (dom/get-target event)
value (dom/get-value target)] value (if (or (= (.-type target) "checkbox")
(= (.-type target) "radio"))
(.-checked target)
(dom/get-value target))]
(swap! form (fn [state] (swap! form (fn [state]
(-> state (-> state
(assoc-in [:data field] (if trim? (str/trim value) value)) (assoc-in [:data field] (if trim? (str/trim value) value))