♻️ Initial profile and auth refactor.

This commit is contained in:
Andrey Antukh 2020-05-22 13:48:21 +02:00
parent d0defe5d93
commit 7d5f9c1078
59 changed files with 2712 additions and 1407 deletions

View file

@ -41,7 +41,7 @@
(and (or (= path "")
(nil? match))
(not authed?))
(st/emit! (rt/nav :login))
(st/emit! (rt/nav :auth-login))
(and (nil? match) authed?)
(st/emit! (rt/nav :dashboard-team {:team-id (:default-team-id profile)}))

View file

@ -51,14 +51,18 @@
ptk/WatchEvent
(watch [this state s]
(let [params {:email email
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)
params {:email email
:password password
:scope "webapp"}
on-error #(rx/of (dm/error (tr "errors.auth.unauthorized")))]
:scope "webapp"}]
(->> (rp/mutation :login params)
(rx/map logged-in)
(rx/catch rp/client-error? on-error))))))
(rx/tap on-success)
(rx/catch (fn [err]
(on-error err)
(rx/empty)))
(rx/map logged-in))))))
;; --- Logout
(def clear-user-data
@ -81,8 +85,8 @@
(ptk/reify ::logout
ptk/WatchEvent
(watch [_ state stream]
(rx/of (rt/nav :login)
clear-user-data))))
(rx/of clear-user-data
(rt/nav :auth-login)))))
;; --- Register
@ -93,18 +97,37 @@
(defn register
"Create a register event instance."
[data on-error]
[data]
(s/assert ::register data)
(s/assert fn? on-error)
(ptk/reify ::register
ptk/WatchEvent
(watch [_ state stream]
(letfn [(handle-error [{payload :payload}]
(on-error payload)
(rx/empty))]
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :register-profile data)
(rx/map (fn [_] (login data)))
(rx/catch rp/client-error? handle-error))))))
(rx/tap on-success)
(rx/map #(login data))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Request Account Deletion
(def request-account-deletion
(letfn [(on-error [{:keys [code] :as error}]
(if (= :uxbox.services.mutations.profile/owner-teams-with-people code)
(let [msg (tr "settings.notifications.profile-deletion-not-allowed")]
(rx/of (dm/error msg)))
(rx/empty)))]
(ptk/reify ::request-account-deletion
ptk/WatchEvent
(watch [_ state stream]
(rx/concat
(->> (rp/mutation :delete-profile {})
(rx/map #(rt/nav :auth-goodbye))
(rx/catch on-error)))))))
;; --- Recovery Request
@ -112,38 +135,43 @@
(s/keys :req-un [::email]))
(defn request-profile-recovery
[data on-success]
[data]
(us/verify ::recovery-request data)
(us/verify fn? on-success)
(ptk/reify ::request-profile-recovery
ptk/WatchEvent
(watch [_ state stream]
(letfn [(on-error [{payload :payload}]
(rx/empty))]
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :request-profile-recovery data)
(rx/tap on-success)
(rx/catch rp/client-error? on-error))))))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Recovery (Password)
(s/def ::token string?)
(s/def ::on-error fn?)
(s/def ::on-success fn?)
(s/def ::recover-profile
(s/keys :req-un [::password ::token ::on-error ::on-success]))
(s/keys :req-un [::password ::token]))
(defn recover-profile
[{:keys [token password on-error on-success] :as data}]
[{:keys [token password] :as data}]
(us/verify ::recover-profile data)
(ptk/reify ::recover-profile
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation :recover-profile {:token token :password password})
(rx/tap on-success)
(rx/catch (fn [err]
(on-error)
(rx/empty)))))))
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :recover-profile data)
(rx/tap on-success)
(rx/catch (fn [err]
(on-error)
(rx/empty))))))))
;; --- Create Demo Profile

View file

@ -143,7 +143,7 @@
(->> (rp/query :projects-by-team {:team-id team-id})
(rx/map projects-fetched)
(rx/catch (fn [error]
(rx/of (rt/nav' :not-authorized))))))))
(rx/of (rt/nav' :auth-login))))))))
(defn projects-fetched
[projects]
@ -212,7 +212,7 @@
(->> (rp/query :recent-files params)
(rx/map recent-files-fetched)
(rx/catch (fn [e]
(rx/of (rt/nav' :not-authorized)))))))))
(rx/of (rt/nav' :auth-login)))))))))
(defn recent-files-fetched
[recent-files]

View file

@ -61,3 +61,9 @@
(show {:content message
:type :info
:timeout timeout}))
(defn success
[message & {:keys [timeout] :or {timeout 3000}}]
(show {:content message
:type :info
:timeout timeout}))

View file

@ -13,6 +13,7 @@
[uxbox.common.spec :as us]
[uxbox.config :as cfg]
[uxbox.main.repo :as rp]
[uxbox.util.router :as rt]
[uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.storage :refer [storage]]
[uxbox.util.avatars :as avatars]
@ -74,7 +75,11 @@
ptk/WatchEvent
(watch [_ state s]
(->> (rp/query! :profile)
(rx/map profile-fetched)))))
(rx/map profile-fetched)
(rx/catch (fn [error]
(if (= (:type error) :not-found)
(rx/of (rt/nav :auth-login))
(rx/empty))))))))
;; --- Update Profile
@ -91,9 +96,35 @@
(rx/empty))]
(->> (rp/mutation :update-profile data)
(rx/do on-success)
(rx/map profile-fetched)
(rx/map (constantly fetch-profile))
(rx/catch rp/client-error? handle-error))))))
;; --- Request Email Change
(defn request-email-change
[{:keys [email] :as data}]
(ptk/reify ::request-email-change
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :request-email-change data)
(rx/tap on-success)
(rx/map (constantly fetch-profile))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Cancel Email Change
(def cancel-email-change
(ptk/reify ::cancel-email-change
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation :cancel-email-change {})
(rx/map (constantly fetch-profile))))))
;; --- Update Password (Form)
(s/def ::update-password
@ -107,15 +138,16 @@
(ptk/reify ::update-password
ptk/WatchEvent
(watch [_ state s]
(let [mdata (meta data)
on-success (:on-success mdata identity)
on-error (:on-error mdata identity)
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)
params {:old-password (:password-old data)
:password (:password-1 data)}]
(->> (rp/mutation :update-profile-password params)
(rx/catch rp/client-error? #(do (on-error (:payload %))
(rx/empty)))
(rx/do on-success)
(rx/tap on-success)
(rx/catch (fn [err]
(on-error err)
(rx/empty)))
(rx/ignore))))))

View file

@ -95,7 +95,7 @@
(defmethod mutation :logout
[id params]
(let [url (str url "/api/logout")]
(->> (http/send! {:method :post :url url :body params :auth false})
(->> (http/send! {:method :post :url url :body params})
(rx/mapcat handle-response))))
(def client-error? http/client-error?)

View file

@ -21,11 +21,8 @@
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.ui.dashboard :refer [dashboard]]
[uxbox.main.ui.login :refer [login-page]]
[uxbox.main.ui.static :refer [not-found-page not-authorized-page]]
[uxbox.main.ui.profile.recovery :refer [profile-recovery-page]]
[uxbox.main.ui.profile.recovery-request :refer [profile-recovery-request-page]]
[uxbox.main.ui.profile.register :refer [profile-register-page]]
[uxbox.main.ui.auth :refer [auth verify-token]]
[uxbox.main.ui.settings :as settings]
[uxbox.main.ui.viewer :refer [viewer-page]]
[uxbox.main.ui.workspace :as workspace]
@ -35,14 +32,18 @@
;; --- Routes
(def routes
[["/login" :login]
["/register" :profile-register]
["/recovery/request" :profile-recovery-request]
["/recovery" :profile-recovery]
[["/auth"
["/login" :auth-login]
["/register" :auth-register]
["/recovery/request" :auth-recovery-request]
["/recovery" :auth-recovery]
["/verify-token" :auth-verify-token]
["/goodbye" :auth-goodbye]]
["/settings"
["/profile" :settings-profile]
["/password" :settings-password]]
["/password" :settings-password]
["/options" :settings-options]]
["/view/:page-id" :viewer]
["/not-found" :not-found]
@ -84,20 +85,20 @@
{::mf/wrap [#(mf/catch % {:fallback app-error})]}
[{:keys [route] :as props}]
(case (get-in route [:data :name])
:login
[:& login-page]
:profile-register
[:& profile-register-page]
(:auth-login
:auth-register
:auth-goodbye
:auth-recovery-request
:auth-recovery)
[:& auth {:route route}]
:profile-recovery-request
[:& profile-recovery-request-page]
:profile-recovery
[:& profile-recovery-page]
:auth-verify-token
[:& verify-token {:route route}]
(:settings-profile
:settings-password)
:settings-password
:settings-options)
[:& settings/settings {:route route}]
:debug-icons-preview

View file

@ -0,0 +1,93 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth
(:require
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as du]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.auth.login :refer [login-page]]
[uxbox.main.ui.auth.recovery :refer [recovery-page]]
[uxbox.main.ui.auth.recovery-request :refer [recovery-request-page]]
[uxbox.main.ui.auth.register :refer [register-page]]
[uxbox.main.repo :as rp]
[uxbox.util.timers :as ts]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(mf/defc goodbye-page
[{:keys [locale] :as props}]
[:div.goodbay
[:h1 (t locale "auth.goodbye-title")]])
(mf/defc auth
[{:keys [route] :as props}]
(let [section (get-in route [:data :name])
locale (mf/deref i18n/locale)]
[:*
[:& messages]
[:div.auth
[:section.auth-sidebar
[:a.logo i/logo]
[:span.tagline (t locale "auth.sidebar-tagline")]]
[:section.auth-content
(case section
:auth-register [:& register-page {:locale locale}]
:auth-login [:& login-page {:locale locale}]
:auth-goodbye [:& goodbye-page {:locale locale}]
:auth-recovery-request [:& recovery-request-page {:locale locale}]
:auth-recovery [:& recovery-page {:locale locale
:params (:query-params route)}])]]]))
(defn- handle-email-verified
[data]
(let [msg (tr "settings.notifications.email-verified-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :settings-profile)
du/fetch-profile)))
(defn- handle-email-changed
[data]
(let [msg (tr "settings.notifications.email-changed-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :settings-profile)
du/fetch-profile)))
(mf/defc verify-token
[{:keys [route] :as props}]
(let [token (get-in route [:query-params :token])]
(mf/use-effect
(fn []
(->> (rp/mutation :verify-profile-token {:token token})
(rx/subs
(fn [response]
(case (:type response)
:verify-email (handle-email-verified response)
:change-email (handle-email-changed response)
nil))
(fn [error]
(case (:code error)
:uxbox.services.mutations.profile/email-already-exists
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :settings-profile)))
(let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :settings-profile)))))))))
[:div.verify-token
i/loader-pencil]))

View file

@ -0,0 +1,86 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.login
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.common.spec :as us]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.store :as st]
[uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
(s/def ::login-form
(s/keys :req-un [::email ::password]))
(defn- on-error
[form error]
(st/emit! (dm/error (tr "errors.auth.unauthorized"))))
(defn- on-submit
[form event]
(let [params (with-meta (:clean-data form)
{:on-error (partial on-error form)})]
(st/emit! (da/login params))))
(mf/defc login-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::login-form
:initial {}}
[:& input
{:name :email
:type "text"
:tab-index "2"
:help-icon i/at
:label (t locale "auth.email-label")}]
[:& input
{:type "password"
:name :password
:tab-index "3"
:help-icon i/eye
:label (t locale "auth.password-label")}]
[:& submit-button
{:label (t locale "auth.login-submit-label")}]])
(mf/defc login-page
[{:keys [locale] :as props}]
[:div.generic-form.login-form
[:div.form-container
[:h1 (t locale "auth.login-title")]
[:div.subtitle (t locale "auth.login-subtitle")]
[:& login-form {:locale locale}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
:tab-index "5"}
(t locale "auth.forgot-password")]]
[:div.link-entry
[:span (t locale "auth.register-label") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-register))
:tab-index "6"}
(t locale "auth.register")]]
[:div.link-entry
[:span (t locale "auth.create-demo-profile-label") " "]
[:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "6"}
(t locale "auth.create-demo-profile")]]]]])

View file

@ -0,0 +1,98 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.recovery
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t tr]]
[uxbox.util.router :as rt]))
(s/def ::password-1 ::fm/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string)
(s/def ::token ::fm/not-empty-string)
(s/def ::recovery-form
(s/keys :req-un [::password-1
::password-2]))
(defn- password-equality
[data]
(let [password-1 (:password-1 data)
password-2 (:password-2 data)]
(cond-> {}
(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"}))))
(defn- on-error
[form error]
(st/emit! (dm/error (tr "auth.notifications.invalid-token-error"))))
(defn- on-success
[_]
(st/emit! (dm/info (tr "auth.notifications.password-changed-succesfully"))
(rt/nav :auth-login)))
(defn- on-submit
[form event]
(let [params (with-meta {:token (get-in form [:clean-data :token])
:password (get-in form [:clean-data :password-2])}
{:on-error (partial on-error form)
:on-success (partial on-success form)})]
(st/emit! (uda/recover-profile params))))
(mf/defc recovery-form
[{:keys [locale params] :as props}]
[:& form {:on-submit on-submit
:spec ::recovery-form
:validators [password-equality]
:initial params}
[:& input {:type "password"
:name :password-1
:label (t locale "auth.new-password-label")}]
[:& input {:type "password"
:name :password-2
:label (t locale "auth.confirm-password-label")}]
[:& submit-button
{:label (t locale "auth.recovery-submit-label")}]])
;; --- Recovery Request Page
(mf/defc recovery-page
[{:keys [locale params] :as props}]
[:section.generic-form
[:div.form-container
[:h1 "Forgot your password?"]
[:div.subtitle "Please enter your new password"]
[:& recovery-form {:locale locale :params params}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
(t locale "profile.recovery.go-to-login")]]]]])

View file

@ -0,0 +1,68 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.recovery-request
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email]))
(defn- on-submit
[form event]
(let [on-success #(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
(rt/nav :auth-login))
params (with-meta (:clean-data form)
{:on-success on-success})]
(st/emit! (uda/request-profile-recovery params))))
(mf/defc recovery-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::recovery-request-form
:initial {}}
[:& input {:name :email
:label (t locale "auth.email-label")
:help-icon i/at
:type "text"}]
[:& submit-button
{:label (t locale "auth.recovery-request-submit-label")}]])
;; --- Recovery Request Page
(mf/defc recovery-request-page
[{:keys [locale] :as props}]
[:section.generic-form
[:div.form-container
[:h1 (t locale "auth.recovery-request-title")]
[:div.subtitle (t locale "auth.recovery-request-subtitle")]
[:& recovery-form {:locale locale}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
(t locale "auth.go-back-to-login")]]]]])

View file

@ -0,0 +1,120 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.register
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.config :as cfg]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as uda]
[uxbox.main.store :as st]
[uxbox.main.data.auth :as da]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(mf/defc demo-warning
[_]
[:div.featured-note.warning
[:span
[:strong "WARNING: "]
"This is a " [:strong "demo"] " service, "
[:strong "DO NOT USE"] " for real work, "
" the projects will be periodicaly wiped."]])
(s/def ::fullname ::fm/not-empty-string)
(s/def ::password ::fm/not-empty-string)
(s/def ::email ::fm/email)
(s/def ::register-form
(s/keys :req-un [::password
::fullname
::email]))
(defn- on-error
[form error]
(case (:code error)
:uxbox.services.mutations.profile/registration-disabled
(st/emit! (tr "errors.registration-disabled"))
:uxbox.services.mutations.profile/email-already-exists
(swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"})
(st/emit! (tr "errors.unexpected-error"))))
(defn- validate
[data]
(let [password (:password data)]
(when (> 8 (count password))
{:password {:message "errors.password-too-short"}})))
(defn- on-submit
[form event]
(let [data (with-meta (:clean-data form)
{:on-error (partial on-error form)})]
(st/emit! (uda/register data))))
(mf/defc register-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::register-form
:validators [validate]
:initial {}}
[:& input {:name :fullname
:tab-index "1"
:label (t locale "auth.fullname-label")
:type "text"}]
[:& input {:type "email"
:name :email
:tab-index "2"
:help-icon i/at
:label (t locale "auth.email-label")}]
[:& input {:name :password
:tab-index "3"
:hint (t locale "auth.password-length-hint")
:label (t locale "auth.password-label")
:type "password"}]
[:& submit-button
{:label (t locale "auth.register-submit-label")}]])
;; --- Register Page
(mf/defc register-page
[{:keys [locale] :as props}]
[:section.generic-form
[:div.form-container
[:h1 (t locale "auth.register-title")]
[:div.subtitle (t locale "auth.register-subtitle")]
(when cfg/demo-warning
[:& demo-warning])
[:& register-form {:locale locale}]
[:div.links
[:div.link-entry
[:span (t locale "auth.already-have-account") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-login))
:tab-index "4"}
(t locale "auth.login-here")]]
[:div.link-entry
[:span (t locale "auth.create-demo-profile-label") " "]
[:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "5"}
(t locale "auth.create-demo-profile")]]]]])

View file

@ -0,0 +1,150 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.components.forms
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[uxbox.common.data :as d]
[uxbox.main.ui.icons :as i]
[uxbox.util.object :as obj]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
["react" :as react]
[uxbox.util.dom :as dom]))
(def form-ctx (mf/create-context nil))
(mf/defc input
[{:keys [type label help-icon disabled name form hint] :as props}]
(let [form (mf/use-ctx form-ctx)
type' (mf/use-state type)
focus? (mf/use-state false)
locale (mf/deref i18n/locale)
touched? (get-in form [:touched name])
error (get-in form [:errors name])
klass (dom/classnames
:focus @focus?
:valid (and touched? (not error))
:invalid (and touched? error)
:disabled disabled)
swap-text-password
(fn []
(swap! type' (fn [type]
(if (= "password" type)
"text"
"password"))))
on-focus #(reset! focus? true)
on-change (fm/on-input-change form name)
on-blur
(fn [event]
(reset! focus? false)
(when-not (get-in form [:touched name])
(swap! form assoc-in [:touched name] true)))
value (get-in form [:data name] "")
props (-> props
(dissoc :help-icon :form)
(assoc :value value
:on-focus on-focus
:on-blur on-blur
:placeholder label
:on-change on-change
:type @type')
(obj/clj->props))]
[:div.custom-input
[:div.input-container {:class klass}
[:div.main-content
(when-not (str/empty? value)
[:label label])
[:> :input props]]
[:div.help-icon
{:style {:cursor "pointer"}
:on-click (when (= "password" type)
swap-text-password)}
(cond
(and (= type "password")
(= @type' "password"))
i/eye
(and (= type "password")
(= @type' "text"))
i/eye-closed
:else
help-icon)]]
(cond
(and touched? (:message error))
[:span.error (t locale (:message error))]
(string? hint)
[:span.hint hint])]))
(mf/defc select
[{:keys [options label name form default]
:or {default ""}}]
(let [form (mf/use-ctx form-ctx)
value (get-in form [:data name] default)
cvalue (d/seek #(= value (:value %)) options)
on-change (fm/on-input-change form name)]
[:div.custom-select
[:select {:value value
:on-change on-change}
(for [item options]
[:option {:key (:value item) :value (:value item)} (:label item)])]
[:div.input-container
[:div.main-content
[:label label]
[:span.value (:label cvalue "")]]
[:div.icon
i/arrow-slide]]]))
(mf/defc submit-button
[{:keys [label form] :as props}]
(let [form (mf/use-ctx form-ctx)]
[:input.btn-primary.btn-large
{:name "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value label
:type "submit"}]))
(mf/defc form
[{:keys [on-submit spec validators initial children class] :as props}]
(let [frm (fm/use-form :spec spec
:validators validators
:initial initial)]
(mf/use-effect
(mf/deps initial)
(fn []
(if (fn? initial)
(swap! frm update :data merge (initial))
(swap! frm update :data merge initial))))
[:& (mf/provider form-ctx) {:value frm}
[:form {:class class
:on-submit (fn [event]
(dom/prevent-default event)
(on-submit frm event))}
children]]))

View file

@ -22,6 +22,7 @@
(def arrow-end (icon-xref :arrow-end))
(def arrow-slide (icon-xref :arrow-slide))
(def artboard (icon-xref :artboard))
(def at (icon-xref :at))
(def auto-fix (icon-xref :auto-fix))
(def auto-height (icon-xref :auto-height))
(def auto-width (icon-xref :auto-width))
@ -60,6 +61,7 @@
(def lock (icon-xref :lock))
(def lock-open (icon-xref :lock-open))
(def logo (icon-xref :uxbox-logo))
(def logout (icon-xref :logout))
(def logo-icon (icon-xref :uxbox-logo-icon))
(def lowercase (icon-xref :lowercase))
(def mail (icon-xref :mail))

View file

@ -1,99 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.login
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.common.spec :as us]
[uxbox.main.ui.icons :as i]
[uxbox.config :as cfg]
[uxbox.main.data.auth :as da]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
(s/def ::login-form
(s/keys :req-un [::email ::password]))
(defn- on-submit
[event form]
(dom/prevent-default event)
(let [{:keys [email password]} (:clean-data form)]
(st/emit! (da/login {:email email :password password}))))
(mf/defc demo-warning
[_]
[:div.message-inline
[:p
[:strong "WARNING: "]
"This is a " [:strong "demo"] " service, "
[:strong "DO NOT USE"] " for real work, "
" the projects will be periodicaly wiped."]])
(mf/defc login-form
[]
(let [{:keys [data] :as form} (fm/use-form ::login-form {})]
[:form {:on-submit #(on-submit % form)}
[:div.login-content
(when cfg/demo-warning
[:& demo-warning])
[:input.input-text
{:name "email"
:tab-index "2"
:value (:email data "")
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:placeholder (tr "login.email")
:type "text"}]
[:input.input-text
{:name "password"
:tab-index "3"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (tr "login.password")
:type "password"}]
[:input.btn-primary.btn-large
{:name "login"
:tab-index "4"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "login.submit")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :profile-recovery-request))
:tab-index "5"}
(tr "login.forgot-password")]
[:a {:on-click #(st/emit! (rt/nav :profile-register))
:tab-index "6"}
(tr "login.register")]]
[:a.btn-secondary.btn-small {:on-click #(st/emit! da/create-demo-profile)
:tab-index "7"
:title (tr "login.create-demo-profile-description")}
(tr "login.create-demo-profile")]]]))
(mf/defc login-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& login-form]]])

View file

@ -27,13 +27,12 @@
(defn- on-parent-clicked
[event parent-ref]
(dom/stop-propagation event)
(dom/prevent-default event)
(let [parent (mf/ref-val parent-ref)
current (dom/get-target event)]
(when (dom/equals? parent current)
(reset! state nil)
#_(st/emit! (udl/hide-lightbox)))))
(dom/stop-propagation event)
(dom/prevent-default event)
(reset! state nil))))
(mf/defc modal-wrapper
[{:keys [component props]}]
@ -46,7 +45,8 @@
parent-ref (mf/use-ref nil)]
[:div.lightbox {:class classes
:ref parent-ref
:on-click #(on-parent-clicked % parent-ref)}
:on-click #(on-parent-clicked % parent-ref)
}
(mf/element component props)]))
(mf/defc modal

View file

@ -1,91 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.profile.recovery
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt]))
(s/def ::token ::us/not-empty-string)
(s/def ::password ::us/not-empty-string)
(s/def ::recovery-form (s/keys :req-un [::token ::password]))
(mf/defc recovery-form
[]
(let [{:keys [data] :as form} (fm/use-form ::recovery-form {})
locale (i18n/use-locale)
on-success
(fn []
(st/emit! (dm/info (t locale "profile.recovery.password-changed"))
(rt/nav :login)))
on-error
(fn []
(st/emit! (dm/error (t locale "profile.recovery.invalid-token"))))
on-submit
(fn [event]
(dom/prevent-default event)
(st/emit! (uda/recover-profile (assoc (:clean-data form)
:on-error on-error
:on-success on-success))))]
[:form {:on-submit on-submit}
[:div.login-content
[:input.input-text
{:name "token"
:value (:token data "")
:class (fm/error-class form :token)
:on-blur (fm/on-input-blur form :token)
:on-change (fm/on-input-change form :token)
:placeholder (t locale "profile.recovery.token")
:auto-complete "off"
:type "text"}]
[:input.input-text
{:name "password"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (t locale "profile.recovery.password")
:type "password"}]
[:input.btn-primary
{:name "recover"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (t locale "profile.recovery.submit-recover")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(t locale "profile.recovery.go-to-login")]]]]))
;; --- Recovery Request Page
(mf/defc profile-recovery-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& recovery-form]]])

View file

@ -1,75 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.profile.recovery-request
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email]))
(mf/defc recovery-form
[]
(let [{:keys [data] :as form} (fm/use-form ::recovery-request-form {})
locale (i18n/use-locale)
on-success
(fn []
(st/emit! (dm/info (t locale "profile.recovery.recovery-token-sent"))
(rt/nav :profile-recovery)))
on-submit
(fn [event]
(dom/prevent-default event)
(st/emit! (uda/request-profile-recovery (:clean-data form) on-success)))]
[:form {:on-submit on-submit}
[:div.login-content
[:input.input-text
{:name "email"
:value (:email data "")
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:placeholder (t locale "profile.recovery.email")
:type "text"}]
[:input.btn-primary
{:name "login"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (t locale "profile.recovery.submit-request")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(t locale "profile.recovery.go-to-login")]]]]))
;; --- Recovery Request Page
(mf/defc profile-recovery-request-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& recovery-form]]])

View file

@ -1,120 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.profile.register
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as uda]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt]))
(s/def ::fullname ::fm/not-empty-string)
(s/def ::password ::fm/not-empty-string)
(s/def ::email ::fm/email)
(s/def ::register-form
(s/keys :req-un [::password
::fullname
::email]))
(defn- on-error
[error form]
(case (:code error)
:uxbox.services.users/registration-disabled
(st/emit! (tr "errors.api.form.registration-disabled"))
:uxbox.services.users/email-already-exists
(swap! form assoc-in [:errors :email]
{:type ::api
:message "errors.api.form.email-already-exists"})
(st/emit! (tr "errors.api.form.unexpected-error"))))
(defn- on-submit
[event form]
(dom/prevent-default event)
(let [data (:clean-data form)
on-error #(on-error % form)]
(st/emit! (uda/register data on-error))))
(mf/defc register-form
[props]
(let [{:keys [data] :as form} (fm/use-form ::register-form {})]
[:form {:on-submit #(on-submit % form)}
[:div.login-content
[:input.input-text
{:name "fullname"
:tab-index "1"
:value (:fullname data "")
:class (fm/error-class form :fullname)
:on-blur (fm/on-input-blur form :fullname)
:on-change (fm/on-input-change form :fullname)
:placeholder (tr "profile.register.fullname")
:type "text"}]
[:& fm/field-error {:form form
:type #{::api}
:field :fullname}]
[:input.input-text
{:type "email"
:name "email"
:tab-index "3"
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:value (:email data "")
:placeholder (tr "profile.register.email")}]
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
[:input.input-text
{:name "password"
:tab-index "4"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (tr "profile.register.password")
:type "password"}]
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
[:input.btn-primary
{:type "submit"
:tab-index "5"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "profile.register.get-started")}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(tr "profile.register.already-have-account")]]]]))
;; --- Register Page
(mf/defc profile-register-page
[props]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& register-form]]])

View file

@ -2,8 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings
(:require
@ -18,6 +20,7 @@
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.settings.header :refer [header]]
[uxbox.main.ui.settings.password :refer [password-page]]
[uxbox.main.ui.settings.options :refer [options-page]]
[uxbox.main.ui.settings.profile :refer [profile-page]]))
(mf/defc settings
@ -30,7 +33,8 @@
[:& header {:section section :profile profile}]
(case section
:settings-profile (mf/element profile-page)
:settings-password (mf/element password-page))]]))
:settings-password (mf/element password-page)
:settings-options (mf/element options-page))]]))

View file

@ -0,0 +1,102 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.change-email
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.users :as du]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.refs :as refs]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.main.ui.modal :as modal]
[uxbox.util.i18n :as i18n :refer [tr t]]))
(s/def ::email-1 ::fm/email)
(s/def ::email-2 ::fm/email)
(s/def ::email-change-form
(s/keys :req-un [::email-1 ::email-2]))
(defn- on-error
[form error]
(cond
(= (:code error) :uxbox.services.mutations.profile/email-already-exists)
(swap! form (fn [data]
(let [error {:message (tr "errors.email-already-exists")}]
(-> data
(assoc-in [:errors :email-1] error)
(assoc-in [:errors :email-2] error)))))
:else
(let [msg (tr "errors.unexpected-error")]
(st/emit! (dm/error msg)))))
(defn- on-submit
[form event]
(let [data (with-meta {:email (get-in form [:clean-data :email-1])}
{:on-error (partial on-error form)})]
(st/emit! (du/request-email-change data))))
(mf/defc change-email-form
[{:keys [locale profile] :as props}]
[:section.modal-content.generic-form
[:h2 (t locale "settings.change-email-title")]
[:span.featured-note
[:span.text
[:span "Well send you an email to your current email "]
[:strong (:email profile)]
[:span " to verify your identity."]]]
[:& form {:on-submit on-submit
:spec ::email-change-form
:initial {}}
[:& input {:type "text"
:name :email-1
:label (t locale "settings.new-email-label")}]
[:& input {:type "text"
:name :email-2
:label (t locale "settings.confirm-email-label")}]
[:& submit-button
{:label (t locale "settings.change-email-submit-label")}]]])
(mf/defc change-email-confirmation
[{:keys [locale profile] :as locale}]
[:section.modal-content.generic-form.confirmation
[:h2 (t locale "settings.verification-sent-title")]
[:span.featured-note
[:span.icon i/trash]
[:span.text
[:span (str/format "We have sent you an email to “")]
[:strong (:email profile)]
[:span "” Please follow the instructions to verify the email."]]]
[:button.btn-primary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.close-modal-label")]])
(mf/defc change-email-modal
[props]
(let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)]
[:section.generic-modal.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close]
(if (:pending-email profile)
[:& change-email-confirmation {:locale locale :profile profile}]
[:& change-email-form {:locale locale :profile profile}])]))

View file

@ -0,0 +1,42 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.delete-account
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.users :as du]
[uxbox.main.store :as st]
[uxbox.main.ui.modal :as modal]
[uxbox.util.i18n :as i18n :refer [tr t]]))
(mf/defc delete-account-modal
[props]
(let [locale (mf/deref i18n/locale)]
[:section.generic-modal.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close]
[:section.modal-content.generic-form
[:h2 (t locale "settings.delete-account-title")]
[:span.featured-note
[:span.text
[:span (t locale "settings.delete-account-info")]]]
[:div.button-row
[:button.btn-warning.btn-large
{:on-click #(do
(modal/hide!)
(st/emit! da/request-account-deletion))}
(t locale "settings.yes-delete-my-account")]
[:button.btn-secondary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.cancel-and-keep-my-account")]]]]))

View file

@ -16,22 +16,60 @@
(mf/defc header
[{:keys [section profile] :as props}]
(let [profile? (= section :settings-profile)
(let [profile? (= section :settings-profile)
password? (= section :settings-password)
locale (i18n/use-locale)
team-id (:default-team-id profile)]
[:header
[:div.main-logo
{:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))}
i/logo-icon]
[:section.main-bar
[:nav
[:a.nav-item
{:class (when profile? "current")
:on-click #(st/emit! (rt/nav :settings-profile))}
(t locale "settings.profile")]
[:a.nav-item
{:class (when password? "current")
:on-click #(st/emit! (rt/nav :settings-password))}
(t locale "settings.password")]]]]))
options? (= section :settings-options)
team-id (:default-team-id profile)
go-back #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))
logout #(st/emit! da/logout)
locale (mf/deref i18n/locale)
team-id (:default-team-id profile)]
[:header
[:section.secondary-menu
[:div.left {:on-click go-back}
[:span.icon i/arrow-slide]
[:span.label "Dashboard"]]
[:div.right {:on-click logout}
[:span.label "Log out"]
[:span.icon i/logout]]]
[:h1 "Your account"]
[:nav
[:a.nav-item
{:class (when profile? "current")
:on-click #(st/emit! (rt/nav :settings-profile))}
(t locale "settings.profile")]
[:a.nav-item
{:class (when password? "current")
:on-click #(st/emit! (rt/nav :settings-password))}
(t locale "settings.password")]
[:a.nav-item
{:class (when options? "current")
:on-click #(st/emit! (rt/nav :settings-options))}
(t locale "settings.options")]
[:a.nav-item
{:class "foobar"
:on-click #(st/emit! (rt/nav :settings-profile))}
(t locale "settings.teams")]]]))
;; [:div.main-logo
;; {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))}
;; i/logo-icon]
;; [:section.main-bar
;; [:nav
;; [:a.nav-item
;; {:class (when profile? "current")
;; :on-click #(st/emit! (rt/nav :settings-profile))}
;; (t locale "settings.profile")]
;; [:a.nav-item
;; {:class (when password? "current")
;; :on-click #(st/emit! (rt/nav :settings-password))}
;; (t locale "settings.password")]]]]))

View file

@ -1,43 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.settings.notifications
(:require
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.util.i18n :refer [tr]]))
(mf/defc notifications-page
[]
[:section.dashboard-content.user-settings
[:section.user-settings-content
[:span.user-settings-label (tr "settings.notifications.notifications-saved")]
[:p (tr "settings.notifications.description")]
[:div.input-radio.radio-primary
[:input {:type "radio"
:id "notification-1"
:name "notification"
:value "none"}]
[:label {:for "notification-1"
:value (tr "settings.notifications.none")} (tr "settings.notifications.none")]
[:input {:type "radio"
:id "notification-2"
:name "notification"
:value "every-hour"}]
[:label {:for "notification-2"
:value (tr "settings.notifications.every-hour")} (tr "settings.notifications.every-hour")]
[:input {:type "radio"
:id "notification-3"
:name "notification"
:value "every-day"}]
[:label {:for "notification-3"
:value (tr "settings.notifications.every-day")} (tr "settings.notifications.every-day")]]
[:input.btn-primary {:type "submit"
:class "btn-disabled"
:disabled true
:value (tr "settings.update-settings")}]
]])

View file

@ -0,0 +1,75 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.options
(:require
[rumext.alpha :as mf]
[cljs.spec.alpha :as s]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu]
[uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [select submit-button form]]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t tr]]))
(s/def ::lang (s/nilable ::fm/not-empty-string))
(s/def ::theme (s/nilable ::fm/not-empty-string))
(s/def ::options-form
(s/keys :opt-un [::lang ::theme]))
(defn- on-error
[form error])
(defn- on-submit
[form event]
(dom/prevent-default event)
(let [data (:clean-data form)
on-success #(st/emit! (dm/info (tr "settings.notifications.profile-saved")))
on-error #(on-error % form)]
(st/emit! (udu/update-profile (with-meta data
{:on-success on-success
:on-error on-error})))))
(mf/defc options-form
[{:keys [locale profile] :as props}]
[:& form {:class "options-form"
:on-submit on-submit
:spec ::options-form
:initial profile}
[:h2 (t locale "settings.language-change-title")]
[:& select {:options [{:label "English" :value "en"}
{:label "Français" :value "fr"}]
:label (t locale "settings.language-label")
:default "en"
:name :lang}]
[:h2 (t locale "settings.theme-change-title")]
[:& select {:label (t locale "settings.theme-label")
:name :theme
:default "default"
:options [{:label "Default" :value "default"}]}]
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]])
;; --- Password Page
(mf/defc options-page
[props]
(let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)]
[:section.settings-options.generic-form
[:div.forms-container
[:& options-form {:locale locale :profile profile}]]]))

View file

@ -5,8 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2016-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2016-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.password
(:require
@ -15,40 +14,52 @@
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu]
[uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.store :as st]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]]))
[uxbox.util.i18n :as i18n :refer [t tr]]))
(defn- on-error
[form error]
(case (:code error)
:uxbox.services.users/old-password-not-match
:uxbox.services.mutations.profile/old-password-not-match
(swap! form assoc-in [:errors :password-old]
{:type ::api :message "settings.password.wrong-old-password"})
{:message (tr "errors.wrong-old-password")})
:else (throw (ex-info "unexpected" {:error error}))))
:else
(let [msg (tr "generic.error")]
(st/emit! (dm/error msg)))))
(defn- on-success
[form]
(let [msg (tr "settings.notifications.password-saved")]
(st/emit! (dm/info msg))))
(defn- on-submit
[event form]
[form event]
(dom/prevent-default event)
(let [data (:clean-data form)
mdata {:on-success #(st/emit! (dm/info (tr "settings.password.password-saved")))
:on-error #(on-error form %)}]
(st/emit! (udu/update-password (with-meta data mdata)))))
(let [params (with-meta (:clean-data form)
{:on-success (partial on-success form)
:on-error (partial on-error form)})]
(st/emit! (udu/update-password params))))
(s/def ::password-1 ::fm/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string)
(s/def ::password-old ::fm/not-empty-string)
(defn password-equality
(defn- password-equality
[data]
(let [password-1 (:password-1 data)
password-2 (:password-2 data)]
(when (and password-1 password-2
(not= password-1 password-2))
{:password-2 {:code ::password-not-equal
:message "profile.password.not-equal"}})))
(cond-> {}
(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
@ -56,54 +67,37 @@
::password-old]))
(mf/defc password-form
[props]
(let [{:keys [data] :as form} (fm/use-form2 :spec ::password-form
:validators [password-equality]
:initial {})]
[:form.password-form {:on-submit #(on-submit % form)}
[:span.settings-label (tr "settings.password.change-password")]
[:input.input-text
{:type "password"
:name "password-old"
:value (:password-old data "")
:class (fm/error-class form :password-old)
:on-blur (fm/on-input-blur form :password-old)
:on-change (fm/on-input-change form :password-old)
:placeholder (tr "settings.password.old-password")}]
[{:keys [locale] :as props}]
[:& form {:class "password-form"
:on-submit on-submit
:spec ::password-form
:validators [password-equality]
:initial {}}
[:h2 (t locale "settings.password-change-title")]
[:& fm/field-error {:form form :field :password-old :type ::api}]
[:& input
{:type "password"
:name :password-old
:label (t locale "settings.old-password-label")}]
[:input.input-text
{:type "password"
:name "password-1"
:value (:password-1 data "")
:class (fm/error-class form :password-1)
:on-blur (fm/on-input-blur form :password-1)
:on-change (fm/on-input-change form :password-1)
:placeholder (tr "settings.password.new-password")}]
[:& input
{:type "password"
:name :password-1
:label (t locale "settings.new-password-label")}]
[:& fm/field-error {:form form :field :password-1}]
[:& input
{:type "password"
:name :password-2
:label (t locale "settings.confirm-password-label")}]
[:input.input-text
{:type "password"
:name "password-2"
:value (:password-2 data "")
:class (fm/error-class form :password-2)
:on-blur (fm/on-input-blur form :password-2)
:on-change (fm/on-input-change form :password-2)
:placeholder (tr "settings.password.confirm-password")}]
[:& fm/field-error {:form form :field :password-2}]
[:input.btn-primary.btn-large
{:type "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "settings.update-settings")}]]))
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]])
;; --- Password Page
(mf/defc password-page
[props]
[:section.settings-password
[:& password-form]])
(let [locale (mf/deref i18n/locale)]
[:section.settings-password.generic-form
[:div.forms-container
[:& password-form {:locale locale}]]]))

View file

@ -2,8 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2016-2017 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2016-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.profile
(:require
@ -13,6 +15,10 @@
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.settings.change-email :refer [change-email-modal]]
[uxbox.main.ui.settings.delete-account :refer [delete-account-modal]]
[uxbox.main.ui.modal :as modal]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.refs :as refs]
@ -21,8 +27,6 @@
[uxbox.util.i18n :as i18n :refer [tr t]]))
(s/def ::fullname ::fm/not-empty-string)
(s/def ::lang (s/nilable ::fm/not-empty-string))
(s/def ::theme ::fm/not-empty-string)
(s/def ::email ::fm/email)
(s/def ::profile-form
@ -30,22 +34,12 @@
(defn- on-error
[error form]
(case (:code error)
:uxbox.services.users/email-already-exists
(swap! form assoc-in [:errors :email]
{:type ::api
:message "errors.api.form.email-already-exists"})
:uxbox.services.users/username-already-exists
(swap! form assoc-in [:errors :username]
{:type ::api
:message "errors.api.form.username-already-exists"})))
(st/emit! (dm/error (tr "errors.generic"))))
(defn- on-submit
[event form]
(dom/prevent-default event)
[form event]
(let [data (:clean-data form)
on-success #(st/emit! (dm/info (tr "settings.profile.profile-saved")))
on-success #(st/emit! (dm/info (tr "settings.notifications.profile-saved")))
on-error #(on-error % form)]
(st/emit! (udu/update-profile (with-meta data
{:on-success on-success
@ -54,64 +48,58 @@
;; --- Profile Form
(mf/defc profile-form
[props]
(let [locale (i18n/use-locale)
form (fm/use-form ::profile-form #(deref refs/profile))
data (:data form)]
[:form.profile-form {:on-submit #(on-submit % form)}
[:span.settings-label (t locale "settings.profile.section-basic-data")]
[:input.input-text
[{:keys [locale] :as props}]
(let [prof (mf/deref refs/profile)]
[:& form {:on-submit on-submit
:class "profile-form"
:spec ::profile-form
:initial prof}
[:& input
{:type "text"
:name "fullname"
:class (fm/error-class form :fullname)
:on-blur (fm/on-input-blur form :fullname)
:on-change (fm/on-input-change form :fullname)
:value (:fullname data "")
:placeholder (t locale "settings.profile.your-name")}]
[:& fm/field-error {:form form
:type #{::api}
:field :fullname}]
:name :fullname
:label (t locale "settings.fullname-label")}]
[:input.input-text
[:& input
{:type "email"
:name "email"
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:value (:email data "")
:placeholder (t locale "settings.profile.your-email")}]
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
:name :email
:disabled true
:help-icon i/at
:label (t locale "settings.email-label")}]
[:span.settings-label (t locale "settings.profile.lang")]
[:select.input-select {:value (:lang data)
:name "lang"
:class (fm/error-class form :lang)
:on-blur (fm/on-input-blur form :lang)
:on-change (fm/on-input-change form :lang)}
[:option {:value "en"} "English"]
[:option {:value "fr"} "Français"]]
(cond
(nil? (:pending-email prof))
[:div.change-email
[:a {:on-click #(modal/show! change-email-modal {})}
(t locale "settings.change-email-label")]]
[:span.user-settings-label (tr "settings.profile.section-theme-data")]
[:select.input-select {:value (:theme data)
:name "theme"
:class (fm/error-class form :theme)
:on-blur (fm/on-input-blur form :theme)
:on-change (fm/on-input-change form :theme)}
[:option {:value "light"} "Default"]]
(not= (:pending-email prof) (:email prof))
[:span.featured-note
[:span.icon i/trash]
[:span.text
[:span "There is a pending change of your email to "]
[:strong (:pending-email prof)]
[:span "."] [:br]
[:a {:on-click #(st/emit! udu/cancel-email-change)}
"Dismiss"]]]
[:input.btn-primary.btn-large
{:type "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (t locale "settings.update-settings")}]]))
:else
[:span.featured-note.warning
[:span.text
[:span "There is a pending email validation."]]])
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]
[:div.links
[:div.link-item
[:a {:on-click #(modal/show! delete-account-modal {})}
(t locale "settings.remove-account-label")]]]]))
;; --- Profile Photo Form
(mf/defc profile-photo-form
[props]
[{:keys [locale] :as props}]
(let [profile (mf/deref refs/profile)
photo (:photo-uri profile)
photo (if (or (str/empty? photo) (nil? photo))
@ -127,10 +115,12 @@
(st/emit! (udu/update-photo {:file file}))
(dom/clean-value! target)))]
[:form.avatar-form
[:img {:src photo}]
[:input {:type "file"
:value ""
:on-change on-change}]]))
[:div.image-change-field
[:span.update-overlay (t locale "settings.update-photo-label")]
[:img {:src photo}]
[:input {:type "file"
:value ""
:on-change on-change}]]]))
;; --- Profile Page
@ -138,7 +128,7 @@
{::mf/wrap-props false}
[props]
(let [locale (i18n/use-locale)]
[:section.settings-profile
[:span.settings-label (t locale "settings.profile.your-avatar")]
[:& profile-photo-form]
[:& profile-form]]))
[:section.settings-profile.generic-form
[:div.forms-container
[:& profile-photo-form {:locale locale}]
[:& profile-form {:locale locale}]]]))

View file

@ -35,7 +35,7 @@
[& params]
(assert (even? (count params)))
(str/join " " (reduce (fn [acc [k v]]
(if (true? v)
(if (true? (boolean v))
(conj acc (name k))
acc))
[]

View file

@ -50,44 +50,25 @@
:else acc))
(defn use-form
[spec initial]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}})
clean-data (s/conform spec (:data state))
problems (when (= ::s/invalid clean-data)
(::s/problems (s/explain-data spec (:data state))))
errors (merge (reduce interpret-problem {} problems)
(:errors state))]
(-> (assoc state
:errors errors
:clean-data (when (not= clean-data ::s/invalid) clean-data)
:valid (and (empty? errors)
(not= clean-data ::s/invalid)))
(impl-mutator update-state))))
(defn use-form2
[& {:keys [spec validators initial]}]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}})
clean-data (s/conform spec (:data state))
problems (when (= ::s/invalid clean-data)
cleaned (s/conform spec (:data state))
problems (when (= ::s/invalid cleaned)
(::s/problems (s/explain-data spec (:data state))))
errors (merge (reduce interpret-problem {} problems)
(when (not= clean-data ::s/invalid)
errors (merge (reduce interpret-problem {} problems)
(reduce (fn [errors vf]
(merge errors (vf clean-data)))
{} validators))
(:errors state))]
(merge errors (vf (:data state))))
{} validators)
(:errors state))]
(-> (assoc state
:errors errors
:clean-data (when (not= clean-data ::s/invalid) clean-data)
:clean-data (when (not= cleaned ::s/invalid) cleaned)
:valid (and (empty? errors)
(not= clean-data ::s/invalid)))
(not= cleaned ::s/invalid)))
(impl-mutator update-state))))
(defn on-input-change

View file

@ -9,8 +9,11 @@
(ns uxbox.util.object
"A collection of helpers for work with javascript objects."
(:refer-clojure :exclude [get get-in assoc!])
(:require [goog.object :as gobj]))
(:refer-clojure :exclude [set! get get-in assoc!])
(:require
[cuerdas.core :as str]
[goog.object :as gobj]
["lodash/omit" :as omit]))
(defn get
([obj k]
@ -32,6 +35,14 @@
(rest keys)
(unchecked-get res key))))))
(defn without
[obj keys]
(let [keys (cond
(vector? keys) (into-array keys)
(array? keys) keys
:else (throw (js/Error. "unexpected input")))]
(omit obj keys)))
(defn merge!
([a b]
(js/Object.assign a b))
@ -42,3 +53,13 @@
[obj key value]
(unchecked-set obj key value)
obj)
(defn- props-key-fn
[key]
(if (or (= key :class) (= key :class-name))
"className"
(str/camel (name key))))
(defn clj->props
[props]
(clj->js props :keyword-fn props-key-fn))