🚧 More work on better forms and data validation.

This commit is contained in:
Andrey Antukh 2019-09-02 20:49:48 +02:00
parent 04a5038ff4
commit 689cc5f3e7
21 changed files with 641 additions and 618 deletions

View file

@ -5,29 +5,28 @@
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.data.auth (ns uxbox.main.data.auth
(:require [cljs.spec.alpha :as s] (:require
[struct.alpha :as st]
[beicon.core :as rx] [beicon.core :as rx]
[potok.core :as ptk] [potok.core :as ptk]
[uxbox.main.store :as st]
[uxbox.main.repo :as rp] [uxbox.main.repo :as rp]
[uxbox.main.store :refer [initial-state]] [uxbox.main.store :refer [initial-state]]
[uxbox.main.data.projects :as udp] [uxbox.main.data.users :as du]
[uxbox.main.data.users :as udu] [uxbox.util.messages :as um]
[uxbox.util.messages :as uum]
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[uxbox.util.spec :as us]
[uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.storage :refer [storage]])) [uxbox.util.storage :refer [storage]]))
(s/def ::username string?)
(s/def ::password string?)
(s/def ::fullname string?)
(s/def ::email us/email?)
(s/def ::token string?)
;; --- Logged In ;; --- Logged In
(defrecord LoggedIn [data] ;; TODO: add spec
(defn logged-in
[data]
(reify
ptk/EventType
(type [_] ::logged-in)
ptk/UpdateEvent ptk/UpdateEvent
(update [this state] (update [this state]
(assoc state :auth data)) (assoc state :auth data))
@ -35,20 +34,23 @@
ptk/WatchEvent ptk/WatchEvent
(watch [this state s] (watch [this state s]
(swap! storage assoc :auth data) (swap! storage assoc :auth data)
(rx/of (udu/fetch-profile) (rx/of (du/fetch-profile)
(rt/navigate :dashboard/projects)))) (rt/navigate :dashboard/projects)))))
(defn logged-in? (defn logged-in?
[v] [v]
(instance? LoggedIn v)) (= (ptk/type v) ::logged-in))
(defn logged-in
[data]
(LoggedIn. data))
;; --- Login ;; --- Login
(defrecord Login [username password] (st/defs ::login
(st/dict :username ::st/string
:password ::st/string))
(defn login
[{:keys [username password] :as data}]
(assert (st/valid? ::login data))
(reify
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(merge state (dissoc initial-state :route :router))) (merge state (dissoc initial-state :route :router)))
@ -58,23 +60,16 @@
(let [params {:username username (let [params {:username username
:password password :password password
:scope "webapp"} :scope "webapp"}
on-error #(rx/of (uum/error (tr "errors.auth.unauthorized")))] on-error #(rx/of (um/error (tr "errors.auth.unauthorized")))]
(->> (rp/req :auth/login params) (->> (rp/req :auth/login params)
(rx/map :payload) (rx/map :payload)
(rx/map logged-in) (rx/map logged-in)
(rx/catch rp/client-error? on-error))))) (rx/catch rp/client-error? on-error))))))
(s/def ::login-event
(s/keys :req-un [::username ::password]))
(defn login
[params]
{:pre [(us/valid? ::login-event params)]}
(map->Login params))
;; --- Logout ;; --- Logout
(defrecord ClearUserData [] (def clear-user-data
(reify
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(merge state (dissoc initial-state :route :router))) (merge state (dissoc initial-state :route :router)))
@ -87,23 +82,29 @@
ptk/EffectEvent ptk/EffectEvent
(effect [_ state s] (effect [_ state s]
(reset! storage {}) (reset! storage {})
(i18n/set-default-locale!))) (i18n/set-default-locale!))))
(defrecord Logout [] (def logout
(reify
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(rx/of (rt/nav :auth/login) (rx/of (rt/nav :auth/login)
(->ClearUserData)))) clear-user-data))))
(defn logout
[]
(->Logout))
;; --- Register ;; --- Register
;; TODO: clean form on success (st/defs ::register
(st/dict :fullname ::st/string
:username ::st/string
:password ::st/string
:email ::st/email))
(defrecord Register [data on-error] (defn register
"Create a register event instance."
[data on-error]
(assert (st/valid? ::register data))
(assert (fn? on-error))
(reify
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(letfn [(handle-error [{payload :payload}] (letfn [(handle-error [{payload :payload}]
@ -117,21 +118,17 @@
(->> stream (->> stream
(rx/filter #(= % ::registered)) (rx/filter #(= % ::registered))
(rx/take 1) (rx/take 1)
(rx/map #(login data))))))) (rx/map #(login data))))))))
(s/def ::register-event
(s/keys :req-un [::fullname ::username ::email ::password]))
(defn register
"Create a register event instance."
[data on-error]
{:pre [(us/valid? ::register-event data)
(fn? on-error)]}
(Register. data on-error))
;; --- Recovery Request ;; --- Recovery Request
(defrecord RecoveryRequest [data] (st/defs ::recovery-request
(st/dict :username ::st/string))
(defn recovery-request
[data]
(assert (st/valid? ::recovery-request data))
(reify
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(letfn [(on-error [{payload :payload}] (letfn [(on-error [{payload :payload}]
@ -144,15 +141,8 @@
(->> stream (->> stream
(rx/filter #(= % ::recovery-requested)) (rx/filter #(= % ::recovery-requested))
(rx/take 1) (rx/take 1)
(rx/map #(uum/info (tr "auth.message.recovery-token-sent")))))))) ;; TODO: this should be moved to the UI part
(rx/map #(um/info (tr "auth.message.recovery-token-sent")))))))))
(s/def ::recovery-request-event
(s/keys :req-un [::username]))
(defn recovery-request
[data]
{:pre [(us/valid? ::recovery-request-event data)]}
(RecoveryRequest. data))
;; --- Check Recovery Token ;; --- Check Recovery Token
@ -162,7 +152,7 @@
(letfn [(on-error [{payload :payload}] (letfn [(on-error [{payload :payload}]
(rx/of (rx/of
(rt/navigate :auth/login) (rt/navigate :auth/login)
(uum/error (tr "errors.auth.invalid-recovery-token"))))] (um/error (tr "errors.auth.invalid-recovery-token"))))]
(->> (rp/req :auth/validate-recovery-token token) (->> (rp/req :auth/validate-recovery-token token)
(rx/ignore) (rx/ignore)
(rx/catch rp/client-error? on-error))))) (rx/catch rp/client-error? on-error)))))
@ -174,23 +164,22 @@
;; --- Recovery (Password) ;; --- Recovery (Password)
(defrecord Recovery [token password] (st/defs ::recovery
ptk/WatchEvent (st/dict :username ::st/string
(watch [_ state stream] :token ::st/string))
(letfn [(on-error [{payload :payload}]
(rx/of (uum/error (tr "errors.auth.invalid-recovery-token"))))
(on-success [{payload :payload}]
(rx/of
(rt/navigate :auth/login)
(uum/info (tr "auth.message.password-recovered"))))]
(->> (rp/req :auth/recovery {:token token :password password})
(rx/mapcat on-success)
(rx/catch rp/client-error? on-error)))))
(s/def ::recovery-event
(s/keys :req-un [::username ::token]))
(defn recovery (defn recovery
[{:keys [token password] :as data}] [{:keys [token password] :as data}]
{:pre [(us/valid? ::recovery-event data)]} (assert (st/valid? ::recovery data))
(Recovery. token password)) (reify
ptk/WatchEvent
(watch [_ state stream]
(letfn [(on-error [{payload :payload}]
(rx/of (um/error (tr "errors.auth.invalid-recovery-token"))))
(on-success [{payload :payload}]
(rx/of
(rt/navigate :auth/login)
(um/info (tr "auth.message.password-recovered"))))]
(->> (rp/req :auth/recovery {:token token :password password})
(rx/mapcat on-success)
(rx/catch rp/client-error? on-error))))))

View file

@ -7,62 +7,65 @@
(ns uxbox.main.data.pages (ns uxbox.main.data.pages
(:require (:require
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[potok.core :as ptk] [potok.core :as ptk]
[struct.alpha :as st]
[uxbox.main.repo :as rp] [uxbox.main.repo :as rp]
[uxbox.main.store :as st]
[uxbox.util.data :refer [index-by-id]] [uxbox.util.data :refer [index-by-id]]
[uxbox.util.spec :as us] [uxbox.util.spec :as us]
[uxbox.util.timers :as ts] [uxbox.util.timers :as ts]
[uxbox.util.uuid :as uuid])) [uxbox.util.uuid :as uuid]))
;; --- Specs ;; --- Struct
(s/def ::grid-x-axis number?) (st/defs ::inst inst?)
(s/def ::grid-y-axis number?) (st/defs ::width (st/&& ::st/number ::st/positive))
(s/def ::grid-color string?) (st/defs ::height (st/&& ::st/number ::st/positive))
(s/def ::background string?)
(s/def ::background-opacity number?)
(s/def ::grid-alignment boolean?)
(s/def ::width number?)
(s/def ::height number?)
(s/def ::layout string?)
(s/def ::metadata (st/defs ::metadata
(s/keys :req-un [::width ::height] (st/dict :width ::width
:opt-un [::grid-y-axis :height ::height
::grid-x-axis :grid-y-axis (st/opt ::st/number)
::grid-color :grid-x-axis (st/opt ::st/number)
::grid-alignment :grid-color (st/opt ::st/string)
::order :order (st/opt ::st/number)
::background :background (st/opt ::st/string)
::background-opacity :background-opacity (st/opt ::st/number)))
::layout]))
(s/def ::id uuid?) (st/defs ::shapes-list
(s/def ::name string?) (st/coll-of ::st/uuid))
(s/def ::version integer?)
(s/def ::project uuid?)
(s/def ::user uuid?)
(s/def ::created-at inst?)
(s/def ::modified-at inst?)
(s/def ::shapes
(-> (s/coll-of uuid? :kind vector?)
(s/nilable)))
(s/def ::page-entity (st/defs ::page-entity
(s/keys :req-un [::id (st/dict :id ::st/uuid
::name :name ::st/string
::project :project ::st/uuid
::version :created-at ::inst
::created-at :modified-at ::inst
::modified-at :user ::st/uuid
::user :metadata ::metadata
::metadata :shapes ::shapes-list))
::shapes]))
;; TODO: add interactions to spec (st/defs ::minimal-shape
(st/dict :id ::st/uuid
:type ::st/keyword
:name ::st/string))
(st/defs ::server-page-data-sapes
(st/coll-of ::minimal-shape))
(st/defs ::server-page-data
(st/dict :shapes ::server-page-data-sapes))
(st/defs ::server-page
(st/dict :id ::st/uuid
:name ::st/string
:project ::st/uuid
:version ::st/integer
:created-at ::inst
:modified-at ::inst
:user ::st/uuid
:metadata ::metadata
:data ::server-page-data))
;; --- Protocols ;; --- Protocols
@ -170,44 +173,39 @@
(declare rehash-pages) (declare rehash-pages)
(deftype PageCreated [data] (st/defs ::page-created
(st/dict :id ::st/uuid
:name ::st/string
:project ::st/uuid
:metadata ::metadata))
(defn page-created
[data]
(assert (st/valid? ::page-created data) "invalid parameters")
(reify
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [project-id (:project data)] (let [pid (:project data)]
(-> (update-in state [:projects project-id :pages] conj (:id data)) (-> state
(update-in [:projects pid :pages] (fnil conj []) (:id data))
(unpack-page data) (unpack-page data)
(assoc-packed-page data)))) (assoc-packed-page data))))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(rx/of (rehash-pages (:project data))))) (rx/of (rehash-pages (:project data))))))
(s/def ::page-created
(s/keys :req-un [::id
::name
::project
::metadata]))
(defn page-created
[data]
{:pre [(us/valid? ::page-created data)]}
(PageCreated. data))
(defn page-created?
[o]
(instance? PageCreated o))
;; --- Create Page ;; --- Create Page
(s/def ::create-page-params (st/defs ::create-page
(s/keys :req-un [::name (st/dict :name ::st/string
::project :project ::st/uuid
::width :width ::width
::height])) :height ::height))
(defn create-page (defn create-page
[{:keys [name project width height layout] :as data}] [{:keys [name project width height layout] :as data}]
{:pre [(us/valid? ::create-page-params data)]} (assert (st/valid? ::create-page data))
(reify (reify
ptk/WatchEvent ptk/WatchEvent
(watch [this state s] (watch [this state s]
@ -231,11 +229,9 @@
;; --- Page Persisted ;; --- Page Persisted
;; TODO: add page spec
(defn page-persisted (defn page-persisted
[data] [data]
{:pre [(map? data)]} (assert (st/valid? ::server-page data))
(reify (reify
cljs.core/IDeref cljs.core/IDeref
(-deref [_] data) (-deref [_] data)
@ -245,6 +241,7 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(prn "page-persisted" data)
(let [{:keys [id version]} data] (let [{:keys [id version]} data]
(-> state (-> state
(assoc-in [:pages id :version] version) (assoc-in [:pages id :version] version)
@ -256,9 +253,17 @@
;; --- Persist Page ;; --- Persist Page
(deftype PersistPage [id on-success] (defn persist-page
([id] (persist-page id identity))
([id on-success]
(assert (uuid? id))
(reify
ptk/EventType
(type [_] ::persist-page)
ptk/WatchEvent ptk/WatchEvent
(watch [this state s] (watch [this state s]
(prn "persist-page" id)
(let [page (get-in state [:pages id])] (let [page (get-in state [:pages id])]
(if (:history page) (if (:history page)
(rx/empty) (rx/empty)
@ -267,19 +272,11 @@
(rx/map :payload) (rx/map :payload)
(rx/do #(when (fn? on-success) (rx/do #(when (fn? on-success)
(ts/schedule-on-idle on-success))) (ts/schedule-on-idle on-success)))
(rx/map page-persisted))))))) (rx/map page-persisted)))))))))
(defn persist-page? (defn persist-page?
[v] [v]
(instance? PersistPage v)) (= ::persist-page (ptk/type v)))
(defn persist-page
([id]
{:pre [(uuid? id)]}
(PersistPage. id (constantly nil)))
([id on-success]
{:pre [(uuid? id)]}
(PersistPage. id on-success)))
;; --- Page Metadata Persisted ;; --- Page Metadata Persisted
@ -288,8 +285,10 @@
(update [_ state] (update [_ state]
(assoc-in state [:pages id :version] (:version data)))) (assoc-in state [:pages id :version] (:version data))))
(s/def ::metadata-persisted-event (st/defs ::version integer?)
(s/keys :req-un [::id ::version])) (st/defs ::metadata-persisted-event
(st/dict :id ::st/uuid
:version ::version))
(defn metadata-persisted? (defn metadata-persisted?
[v] [v]
@ -297,7 +296,7 @@
(defn metadata-persisted (defn metadata-persisted
[{:keys [id] :as data}] [{:keys [id] :as data}]
{:pre [(us/valid? ::metadata-persisted-event data)]} {:pre [(st/valid? ::metadata-persisted-event data)]}
(MetadataPersisted. id data)) (MetadataPersisted. id data))
;; --- Persist Page Metadata ;; --- Persist Page Metadata
@ -327,7 +326,7 @@
(defn update-page (defn update-page
[id data] [id data]
{:pre [(uuid? id) (us/valid? ::page-entity data)]} {:pre [(uuid? id) (st/valid? ::page-entity data)]}
(UpdatePage. id data)) (UpdatePage. id data))
;; --- Update Page Metadata ;; --- Update Page Metadata
@ -340,7 +339,7 @@
(defn update-metadata (defn update-metadata
[id metadata] [id metadata]
{:pre [(uuid? id) (us/valid? ::metadata metadata)]} {:pre [(uuid? id) (st/valid? ::metadata metadata)]}
(UpdateMetadata. id metadata)) (UpdateMetadata. id metadata))
;; --- Rehash Pages ;; --- Rehash Pages
@ -385,12 +384,15 @@
;; A specialized event for persist data ;; A specialized event for persist data
;; from the update page form. ;; from the update page form.
(s/def ::persist-page-update-form-params (st/defs ::persist-page-update-form
(s/keys :req-un [::id ::name ::width ::height])) (st/dict :id ::st/uuid
:name ::st/string
:width ::width
:height ::height))
(defn persist-page-update-form (defn persist-page-update-form
[{:keys [id name width height] :as data}] [{:keys [id name width height] :as data}]
{:pre [(us/valid? ::persist-page-update-form-params data)]} (assert (st/valid? ::persist-page-update-form data))
(reify (reify
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]

View file

@ -5,11 +5,12 @@
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.data.projects (ns uxbox.main.data.projects
(:require [cljs.spec.alpha :as s] (:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[beicon.core :as rx] [beicon.core :as rx]
[potok.core :as ptk] [potok.core :as ptk]
[uxbox.main.store :as st] [struct.core :as st]
[uxbox.main.repo :as rp] [uxbox.main.repo :as rp]
[uxbox.main.data.pages :as udp] [uxbox.main.data.pages :as udp]
[uxbox.util.uuid :as uuid] [uxbox.util.uuid :as uuid]
@ -34,12 +35,21 @@
::created-at ::created-at
::modified-at])) ::modified-at]))
(st/defs project-spec
{:id [st/required st/uuid]
:name [st/required st/string]
:version [st/required st/integer]
:user [st/required st/uuid]
:created-at [st/required inst?]
:modified-at [st/required inst?]})
;; --- Helpers ;; --- Helpers
(defn assoc-project (defn assoc-project
"A reduce function for assoc the project to the state map." "A reduce function for assoc the project to the state map."
[state {:keys [id] :as project}] [state {:keys [id] :as project}]
{:pre [(us/valid? ::project-entity project)]} (assert (st/valid? project-spec project)
"invalid project instance")
(update-in state [:projects id] merge project)) (update-in state [:projects id] merge project))
(defn dissoc-project (defn dissoc-project
@ -160,24 +170,23 @@
;; --- Create Project ;; --- Create Project
(s/def ::create-project-params (st/defs create-project-spec
(s/keys :req-un [::name ::udp/width ::udp/height])) {:name [st/required st/string]
:width [st/required st/number st/positive]
:height [st/required st/number st/positive]})
(defn create-project (defn create-project
[{:keys [name] :as params}] [{:keys [name] :as params}]
{:pre [(us/valid? ::create-project-params params)]} (assert (st/valid? create-project-spec params)
"invalid params for create project event")
(reify (reify
ptk/WatchEvent ptk/WatchEvent
(watch [this state stream] (watch [this state stream]
(rx/merge
(->> (rp/req :create/project {:name name}) (->> (rp/req :create/project {:name name})
(rx/map :payload) (rx/map :payload)
(rx/map (fn [{:keys [id] :as project}] (rx/mapcat (fn [{:keys [id] :as project}]
(udp/create-page (assoc params :project id))))) (rx/of #(assoc-project % project)
(->> stream (udp/create-page (assoc params :project id)))))))))
(rx/filter udp/page-created?)
(rx/take 1)
(rx/map #(fetch-projects)))))))
;; --- Go To Project ;; --- Go To Project

View file

@ -92,12 +92,12 @@
(let [route (mf/deref route-iref)] (let [route (mf/deref route-iref)]
(case (get-in route [:data :name]) (case (get-in route [:data :name])
:auth/login (mf/element auth/login-page) :auth/login (mf/element auth/login-page)
:auth/register (auth/register-page) :auth/register (mf/element auth/register-page)
:auth/recovery-request (auth/recovery-request-page) ;; :auth/recovery-request (auth/recovery-request-page)
:auth/recovery ;; :auth/recovery
(let [token (get-in route [:params :path :token])] ;; (let [token (get-in route [:params :path :token])]
(auth/recovery-page token)) ;; (auth/recovery-page token))
(:settings/profile (:settings/profile
:settings/password :settings/password

View file

@ -7,10 +7,10 @@
(ns uxbox.main.ui.auth (ns uxbox.main.ui.auth
(:require [uxbox.main.ui.auth.login :as login] (:require [uxbox.main.ui.auth.login :as login]
[uxbox.main.ui.auth.register :as register] [uxbox.main.ui.auth.register :as register]
[uxbox.main.ui.auth.recovery-request :as recovery-request] #_[uxbox.main.ui.auth.recovery-request :as recovery-request]
[uxbox.main.ui.auth.recovery :as recovery])) #_[uxbox.main.ui.auth.recovery :as recovery]))
(def login-page login/login-page) (def login-page login/login-page)
(def register-page register/register-page) (def register-page register/register-page)
(def recovery-page recovery/recovery-page) ;; (def recovery-page recovery/recovery-page)
(def recovery-request-page recovery-request/recovery-request-page) ;; (def recovery-request-page recovery-request/recovery-request-page)

View file

@ -8,7 +8,7 @@
(ns uxbox.main.ui.auth.login (ns uxbox.main.ui.auth.login
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[struct.core :as s] [struct.alpha :as s]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.main.data.auth :as da] [uxbox.main.data.auth :as da]
@ -19,9 +19,9 @@
[uxbox.util.i18n :refer [tr]] [uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt])) [uxbox.util.router :as rt]))
(s/defs login-form-spec (s/defs ::login-form
{:username [fm/required fm/string] (s/dict :username (s/&& ::s/string ::fm/not-empty-string)
:password [fm/required fm/string]}) :password (s/&& ::s/string ::fm/not-empty-string)))
(defn- on-submit (defn- on-submit
[event form] [event form]
@ -42,7 +42,7 @@
(mf/defc login-form (mf/defc login-form
[] []
(let [{:keys [data] :as form} (fm/use-form {:initial {} :spec login-form-spec})] (let [{:keys [data] :as form} (fm/use-form ::login-form {})]
[:form {:on-submit #(on-submit % form)} [:form {:on-submit #(on-submit % form)}
[:div.login-content [:div.login-content
(when cfg/isdemo (when cfg/isdemo
@ -84,6 +84,6 @@
[] []
[:div.login [:div.login
[:div.login-body [:div.login-body
(messages-widget) [:& messages-widget]
[:a i/logo] [:a i/logo]
[:& login-form]]]) [:& login-form]]])

View file

@ -21,62 +21,62 @@
[uxbox.util.i18n :refer (tr)] [uxbox.util.i18n :refer (tr)]
[uxbox.util.router :as rt])) [uxbox.util.router :as rt]))
(def form-data (fm/focus-data :recovery st/state)) ;; (def form-data (fm/focus-data :recovery st/state))
(def form-errors (fm/focus-errors :recovery st/state)) ;; (def form-errors (fm/focus-errors :recovery st/state))
(def assoc-value (partial fm/assoc-value :recovery)) ;; (def assoc-value (partial fm/assoc-value :recovery))
(def assoc-errors (partial fm/assoc-errors :recovery)) ;; (def assoc-errors (partial fm/assoc-errors :recovery))
(def clear-form (partial fm/clear-form :recovery)) ;; (def clear-form (partial fm/clear-form :recovery))
;; --- Recovery Form ;; ;; --- Recovery Form
(s/def ::password ::fm/non-empty-string) ;; (s/def ::password ::fm/non-empty-string)
(s/def ::recovery-form ;; (s/def ::recovery-form
(s/keys :req-un [::password])) ;; (s/keys :req-un [::password]))
(mx/defc recovery-form ;; (mx/defc recovery-form
{:mixins [mx/static mx/reactive]} ;; {:mixins [mx/static mx/reactive]}
[token] ;; [token]
(let [data (merge (mx/react form-data) {:token token}) ;; (let [data (merge (mx/react form-data) {:token token})
valid? (fm/valid? ::recovery-form data)] ;; valid? (fm/valid? ::recovery-form data)]
(letfn [(on-change [field event] ;; (letfn [(on-change [field event]
(let [value (dom/event->value event)] ;; (let [value (dom/event->value event)]
(st/emit! (assoc-value field value)))) ;; (st/emit! (assoc-value field value))))
(on-submit [event] ;; (on-submit [event]
(dom/prevent-default event) ;; (dom/prevent-default event)
(st/emit! (uda/recovery data) ;; (st/emit! (uda/recovery data)
(clear-form)))] ;; (clear-form)))]
[:form {:on-submit on-submit} ;; [:form {:on-submit on-submit}
[:div.login-content ;; [:div.login-content
[:input.input-text ;; [:input.input-text
{:name "password" ;; {:name "password"
:value (:password data "") ;; :value (:password data "")
:on-change (partial on-change :password) ;; :on-change (partial on-change :password)
:placeholder (tr "recover.password.placeholder") ;; :placeholder (tr "recover.password.placeholder")
:type "password"}] ;; :type "password"}]
[:input.btn-primary ;; [:input.btn-primary
{:name "login" ;; {:name "login"
:class (when-not valid? "btn-disabled") ;; :class (when-not valid? "btn-disabled")
:disabled (not valid?) ;; :disabled (not valid?)
:value (tr "recover.recover-password") ;; :value (tr "recover.recover-password")
:type "submit"}] ;; :type "submit"}]
[:div.login-links ;; [:div.login-links
[:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recover.go-back")]]]]))) ;; [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recover.go-back")]]]])))
;; --- Recovery Page ;; ;; --- Recovery Page
(defn- recovery-page-init ;; (defn- recovery-page-init
[own] ;; [own]
(let [[token] (::mx/args own)] ;; (let [[token] (::mx/args own)]
(st/emit! (uda/validate-recovery-token token)) ;; (st/emit! (uda/validate-recovery-token token))
own)) ;; own))
(mx/defc recovery-page ;; (mx/defc recovery-page
{:mixins [mx/static (fm/clear-mixin st/store :recovery)] ;; {:mixins [mx/static (fm/clear-mixin st/store :recovery)]
:init recovery-page-init} ;; :init recovery-page-init}
[token] ;; [token]
[:div.login ;; [:div.login
[:div.login-body ;; [:div.login-body
(messages-widget) ;; (messages-widget)
[:a i/logo] ;; [:a i/logo]
(recovery-form token)]]) ;; (recovery-form token)]])

View file

@ -20,52 +20,52 @@
[rumext.core :as mx :include-macros true] [rumext.core :as mx :include-macros true]
[uxbox.util.router :as rt])) [uxbox.util.router :as rt]))
(def form-data (fm/focus-data :recovery-request st/state)) ;; (def form-data (fm/focus-data :recovery-request st/state))
(def form-errors (fm/focus-errors :recovery-request st/state)) ;; (def form-errors (fm/focus-errors :recovery-request st/state))
(def assoc-value (partial fm/assoc-value :profile-password)) ;; (def assoc-value (partial fm/assoc-value :profile-password))
(def assoc-errors (partial fm/assoc-errors :profile-password)) ;; (def assoc-errors (partial fm/assoc-errors :profile-password))
(def clear-form (partial fm/clear-form :profile-password)) ;; (def clear-form (partial fm/clear-form :profile-password))
(s/def ::username ::fm/non-empty-string) ;; (s/def ::username ::fm/non-empty-string)
(s/def ::recovery-request-form (s/keys :req-un [::username])) ;; (s/def ::recovery-request-form (s/keys :req-un [::username]))
(mx/defc recovery-request-form ;; (mx/defc recovery-request-form
{:mixins [mx/static mx/reactive]} ;; {:mixins [mx/static mx/reactive]}
[] ;; []
(let [data (mx/react form-data) ;; (let [data (mx/react form-data)
valid? (fm/valid? ::recovery-request-form data)] ;; valid? (fm/valid? ::recovery-request-form data)]
(letfn [(on-change [event] ;; (letfn [(on-change [event]
(let [value (dom/event->value event)] ;; (let [value (dom/event->value event)]
(st/emit! (assoc-value :username value)))) ;; (st/emit! (assoc-value :username value))))
(on-submit [event] ;; (on-submit [event]
(dom/prevent-default event) ;; (dom/prevent-default event)
(st/emit! (uda/recovery-request data) ;; (st/emit! (uda/recovery-request data)
(clear-form)))] ;; (clear-form)))]
[:form {:on-submit on-submit} ;; [:form {:on-submit on-submit}
[:div.login-content ;; [:div.login-content
[:input.input-text ;; [:input.input-text
{:name "username" ;; {:name "username"
:value (:username data "") ;; :value (:username data "")
:on-change on-change ;; :on-change on-change
:placeholder (tr "recovery-request.username-or-email.placeholder") ;; :placeholder (tr "recovery-request.username-or-email.placeholder")
:type "text"}] ;; :type "text"}]
[:input.btn-primary ;; [:input.btn-primary
{:name "login" ;; {:name "login"
:class (when-not valid? "btn-disabled") ;; :class (when-not valid? "btn-disabled")
:disabled (not valid?) ;; :disabled (not valid?)
:value (tr "recovery-request.recover-password") ;; :value (tr "recovery-request.recover-password")
:type "submit"}] ;; :type "submit"}]
[:div.login-links ;; [:div.login-links
[:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recovery-request.go-back")]]]]))) ;; [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recovery-request.go-back")]]]])))
;; --- Recovery Request Page ;; ;; --- Recovery Request Page
(mx/defc recovery-request-page ;; (mx/defc recovery-request-page
{:mixins [mx/static (fm/clear-mixin st/store :recovery-request)]} ;; {:mixins [mx/static (fm/clear-mixin st/store :recovery-request)]}
[] ;; []
[:div.login ;; [:div.login
[:div.login-body ;; [:div.login-body
(messages-widget) ;; (messages-widget)
[:a i/logo] ;; [:a i/logo]
(recovery-request-form)]]) ;; (recovery-request-form)]])

View file

@ -6,118 +6,131 @@
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.auth.register (ns uxbox.main.ui.auth.register
(:require [cljs.spec.alpha :as s :include-macros true] (:require
[lentes.core :as l]
[cuerdas.core :as str] [cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[struct.alpha :as s]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.store :as st]
[uxbox.main.data.auth :as uda] [uxbox.main.data.auth :as uda]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages-widget]] [uxbox.main.ui.messages :refer [messages-widget]]
[uxbox.main.ui.navigation :as nav] [uxbox.main.ui.navigation :as nav]
[uxbox.util.i18n :refer (tr)]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.forms :as fm] [uxbox.util.forms :as fm]
[rumext.core :as mx :include-macros true] [uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt])) [uxbox.util.router :as rt]))
(def form-data (fm/focus-data :register st/state)) (s/defs ::register-form
(def form-errors (fm/focus-errors :register st/state)) (s/dict :username (s/&& ::s/string ::fm/not-empty-string)
:fullname (s/&& ::s/string ::fm/not-empty-string)
:password (s/&& ::s/string ::fm/not-empty-string)
:email ::s/email))
(def assoc-value (partial fm/assoc-value :register)) (defn- on-error
(def assoc-error (partial fm/assoc-error :register)) [error form]
(def clear-form (partial fm/clear-form :register)) (case (:code error)
;; TODO: add better password validation
(s/def ::username ::fm/non-empty-string)
(s/def ::fullname ::fm/non-empty-string)
(s/def ::password ::fm/non-empty-string)
(s/def ::email ::fm/email)
(s/def ::register-form
(s/keys :req-un [::username
::fullname
::email
::password]))
(mx/defc register-form
{:mixins [mx/static mx/reactive
(fm/clear-mixin st/store :register)]}
[]
(let [data (mx/react form-data)
errors (mx/react form-errors)
valid? (fm/valid? ::register-form data)]
(letfn [(on-change [field event]
(let [value (dom/event->value event)]
(st/emit! (assoc-value field value))))
(on-error [{:keys [type code] :as payload}]
(case code
:uxbox.services.users/registration-disabled :uxbox.services.users/registration-disabled
(st/emit! (tr "errors.api.form.registration-disabled")) (st/emit! (tr "errors.api.form.registration-disabled"))
:uxbox.services.users/email-already-exists :uxbox.services.users/email-already-exists
(st/emit! (assoc-error :email (tr "errors.api.form.email-already-exists"))) (swap! form assoc-in [:errors :email]
{:type ::api
:message "errors.api.form.email-already-exists"})
:uxbox.services.users/username-already-exists :uxbox.services.users/username-already-exists
(st/emit! (assoc-error :username (tr "errors.api.form.username-already-exists"))))) (swap! form assoc-in [:errors :username]
(on-submit [event] {:type ::api
:message "errors.api.form.username-already-exists"})))
(defn- on-submit
[event form]
(dom/prevent-default event) (dom/prevent-default event)
(st/emit! (uda/register data on-error)))] (let [data (:clean-data form)
[:form {:on-submit on-submit} 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 {})]
(prn "register-form" form)
[:form {:on-submit #(on-submit % form)}
[:div.login-content [:div.login-content
[:input.input-text [:input.input-text
{:name "fullname" {:name "fullname"
:tab-index "2" :tab-index "1"
:value (:fullname data "") :value (:fullname data "")
:on-change (partial on-change :fullname) :class (fm/error-class form :fullname)
:on-blur (fm/on-input-blur form :fullname)
:on-change (fm/on-input-change form :fullname)
:placeholder (tr "register.fullname.placeholder") :placeholder (tr "register.fullname.placeholder")
:type "text"}] :type "text"}]
(fm/input-error errors :fullname)
[:& fm/field-error {:form form
:type #{::api}
:field :fullname}]
[:input.input-text [:input.input-text
{:name "username" {:type "text"
:tab-index "3" :name "username"
:tab-index "2"
:class (fm/error-class form :username)
:on-blur (fm/on-input-blur form :username)
:on-change (fm/on-input-change form :username)
:value (:username data "") :value (:username data "")
:on-change (partial on-change :username) :placeholder (tr "settings.profile.your-username")}]
:placeholder (tr "register.username.placeholder")
:type "text"}] [:& fm/field-error {:form form
(fm/input-error errors :username) :type #{::api}
:field :username}]
[:input.input-text [:input.input-text
{:name "email" {:type "email"
:tab-index "4" :name "email"
:ref "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 "") :value (:email data "")
:on-change (partial on-change :email) :placeholder (tr "settings.profile.your-email")}]
:placeholder (tr "register.email.placeholder")
:type "text"}] [:& fm/field-error {:form form
(fm/input-error errors :email) :type #{::api}
:field :email}]
[:input.input-text [:input.input-text
{:name "password" {:name "password"
:tab-index "5" :tab-index "4"
:ref "password"
:value (:password data "") :value (:password data "")
:on-change (partial on-change :password) :class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (tr "register.password.placeholder") :placeholder (tr "register.password.placeholder")
:type "password"}] :type "password"}]
(fm/input-error errors :password)
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
[:input.btn-primary [:input.btn-primary
{:name "login" {:type "submit"
:tab-index "6" :tab-index "5"
:class (when-not valid? "btn-disabled") :class (when-not (:valid form) "btn-disabled")
:disabled (not valid?) :disabled (not (:valid form))
:value (tr "register.get-started") :value (tr "register.get-started")}]
:type "submit"}]
[:div.login-links [:div.login-links
[:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "register.already-have-account")]]]]))) [:a {:on-click #(st/emit! (rt/nav :auth/login))}
(tr "register.already-have-account")]]]]))
;; --- Register Page ;; --- Register Page
(mx/defc register-page (mf/defc register-page
{:mixins [mx/static]} [props]
[own]
[:div.login [:div.login
[:div.login-body [:div.login-body
(messages-widget) (messages-widget)
[:a i/logo] [:a i/logo]
(register-form)]]) [:& register-form]]])

View file

@ -25,7 +25,7 @@
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [[section type id] (parse-route route)] (let [[section type id] (parse-route route)]
[:main.dashboard-main [:main.dashboard-main
(messages-widget) [:& messages-widget]
[:& header {:section section}] [:& header {:section section}]
(case section (case section
:dashboard/icons :dashboard/icons

View file

@ -40,7 +40,6 @@
(-> (l/key :projects) (-> (l/key :projects)
(l/derive st/state))) (l/derive st/state)))
;; --- Helpers ;; --- Helpers
(defn sort-projects-by (defn sort-projects-by

View file

@ -7,7 +7,7 @@
(ns uxbox.main.ui.dashboard.projects-forms (ns uxbox.main.ui.dashboard.projects-forms
(:require (:require
[cljs.spec.alpha :as s] [struct.alpha :as s]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.projects :as udp] [uxbox.main.data.projects :as udp]
@ -17,10 +17,10 @@
[uxbox.util.forms :as fm] [uxbox.util.forms :as fm]
[uxbox.util.i18n :as t :refer [tr]])) [uxbox.util.i18n :as t :refer [tr]]))
(def project-form-spec (s/defs ::project-form
{:name [fm/required fm/string] (s/dict :name (s/&& ::s/string ::fm/not-empty-string)
:width [fm/required fm/number-str] :width ::s/number-str
:height [fm/required fm/number-str]}) :height ::s/number-str))
(def defaults (def defaults
{:name "" {:name ""
@ -44,13 +44,14 @@
(mf/defc create-project-form (mf/defc create-project-form
[props] [props]
(let [{:keys [data errors] :as form} (fm/use-form {:initial defaults :spec project-form-spec})] (let [{:keys [data] :as form} (fm/use-form ::project-form defaults)]
[:form {:on-submit #(on-submit % form)} [:form {:on-submit #(on-submit % form)}
[:input.input-text [:input.input-text
{:placeholder "New project name" {:placeholder "New project name"
:type "text" :type "text"
:name "name" :name "name"
:value (:name data) :value (:name data)
:class (fm/error-class form :name)
:on-blur (fm/on-input-blur form :name) :on-blur (fm/on-input-blur form :name)
:on-change (fm/on-input-change form :name) :on-change (fm/on-input-change form :name)
:auto-focus true}] :auto-focus true}]
@ -63,6 +64,7 @@
:type "number" :type "number"
:min 0 :min 0
:max 5000 :max 5000
:class (fm/error-class form :width)
:on-blur (fm/on-input-blur form :width) :on-blur (fm/on-input-blur form :width)
:on-change (fm/on-input-change form :width) :on-change (fm/on-input-change form :width)
:value (:width data)}]] :value (:width data)}]]
@ -75,6 +77,7 @@
:name "height" :name "height"
:min 0 :min 0
:max 5000 :max 5000
:class (fm/error-class form :height)
:on-blur (fm/on-input-blur form :height) :on-blur (fm/on-input-blur form :height)
:on-change (fm/on-input-change form :height) :on-change (fm/on-input-change form :height)
:value (:height data)}]]] :value (:height data)}]]]

View file

@ -1,16 +1,17 @@
(ns uxbox.main.ui.messages (ns uxbox.main.ui.messages
(:require [lentes.core :as l] (:require
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.util.messages :as uum] [uxbox.util.messages :as um]))
[rumext.core :as mx :include-macros true]))
(def ^:private message-ref (def ^:private message-iref
(-> (l/key :message) (-> (l/key :message)
(l/derive st/state))) (l/derive st/state)))
(mx/defc messages-widget (mf/defc messages-widget
{:mixins [mx/static mx/reactive]}
[] []
(let [message (mx/react message-ref) (let [message (mf/deref message-iref)
on-close #(st/emit! (uum/hide))] on-close #(st/emit! (um/hide))]
(uum/messages-widget (assoc message :on-close on-close)))) [:& um/messages-widget {:message message
:on-close on-close}]))

View file

@ -22,7 +22,7 @@
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [section (get-in route [:data :name])] (let [section (get-in route [:data :name])]
[:main.dashboard-main [:main.dashboard-main
(messages-widget) [:& messages-widget]
[:& header {:section section}] [:& header {:section section}]
(case section (case section
:settings/profile (mf/element profile/profile-page) :settings/profile (mf/element profile/profile-page)

View file

@ -8,7 +8,7 @@
(ns uxbox.main.ui.settings.password (ns uxbox.main.ui.settings.password
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[struct.core :as s] [struct.alpha :as s]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.users :as udu] [uxbox.main.data.users :as udu]
[uxbox.main.store :as st] [uxbox.main.store :as st]
@ -36,14 +36,14 @@
:on-error on-error}] :on-error on-error}]
(st/emit! (udu/update-password data opts))))) (st/emit! (udu/update-password data opts)))))
(s/defs password-form-spec (s/defs ::password-form
{:password-1 [s/required s/string] (s/dict :password-1 (s/&& ::s/string ::fm/not-empty-string)
:password-2 [s/required s/string [s/identical-to :password-1]] :password-2 (s/&& ::s/string ::fm/not-empty-string)
:password-old [s/required s/string]}) :password-old (s/&& ::s/string ::fm/not-empty-string)))
(mf/defc password-form (mf/defc password-form
[props] [props]
(let [{:keys [data] :as form} (fm/use-form {:initial {} :spec password-form-spec})] (let [{:keys [data] :as form} (fm/use-form ::password-form {})]
[:form.password-form {:on-submit #(on-submit % form)} [:form.password-form {:on-submit #(on-submit % form)}
[:span.user-settings-label (tr "settings.password.change-password")] [:span.user-settings-label (tr "settings.password.change-password")]
[:input.input-text [:input.input-text

View file

@ -10,7 +10,7 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[lentes.core :as l] [lentes.core :as l]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[struct.core :as s] [struct.alpha :as s]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.users :as udu] [uxbox.main.data.users :as udu]
[uxbox.main.store :as st] [uxbox.main.store :as st]
@ -18,27 +18,28 @@
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.forms :as fm] [uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.interop :refer [iterable->seq]])) [uxbox.util.interop :refer [iterable->seq]]
[uxbox.util.messages :as um]))
(defn profile->form
(defn- profile->form
[profile] [profile]
(let [language (get-in profile [:metadata :language])] (let [language (get-in profile [:metadata :language])]
(-> (select-keys profile [:fullname :username :email]) (-> (select-keys profile [:fullname :username :email])
(cond-> language (assoc :language language))))) (cond-> language (assoc :language language)))))
(def profile-ref (def ^:private profile-ref
(-> (l/key :profile) (-> (l/key :profile)
(l/derive st/state))) (l/derive st/state)))
(s/defs profile-form-spec (s/defs ::profile-form
{:fullname [fm/required fm/string] (s/dict :fullname (s/&& ::s/string ::fm/not-empty-string)
:username [fm/required fm/string] :username (s/&& ::s/string ::fm/not-empty-string)
:email [fm/required fm/email] :language (s/&& ::s/string ::fm/not-empty-string)
:language [fm/required fm/string]}) :email ::s/email))
(defn- on-error (defn- on-error
[error form] [error form]
(prn "on-error" error form)
(case (:code error) (case (:code error)
:uxbox.services.users/email-already-exists :uxbox.services.users/email-already-exists
(swap! form assoc-in [:errors :email] (swap! form assoc-in [:errors :email]
@ -57,18 +58,20 @@
(defn- on-submit (defn- on-submit
[event form] [event form]
(prn "on-submit" form)
(dom/prevent-default event) (dom/prevent-default event)
(let [data (:clean-data form) (let [data (:clean-data form)
opts {:on-success #(prn "On Success" %) on-success #(st/emit! (um/info (tr "settings.profile.profile-saved")))
:on-error #(on-error % form)}] on-error #(on-error % form)
opts {:on-success on-success
:on-error on-error}]
(st/emit! (udu/update-profile data opts)))) (st/emit! (udu/update-profile data opts))))
;; --- Profile Form ;; --- Profile Form
(mf/defc profile-form (mf/defc profile-form
[props] [props]
(let [{:keys [data] :as form} (fm/use-form {:initial initial-data (let [{:keys [data] :as form} (fm/use-form ::profile-form initial-data)]
:spec profile-form-spec})]
(prn "profile-form" form)
[:form.profile-form {:on-submit #(on-submit % form)} [:form.profile-form {:on-submit #(on-submit % form)}
[:span.user-settings-label (tr "settings.profile.section-basic-data")] [:span.user-settings-label (tr "settings.profile.section-basic-data")]
[:input.input-text [:input.input-text

View file

@ -38,7 +38,7 @@
[:li {:on-click #(on-click % :settings/notifications)} [:li {:on-click #(on-click % :settings/notifications)}
i/mail i/mail
[:span (tr "ds.user.notifications")]] [:span (tr "ds.user.notifications")]]
[:li {:on-click #(on-click % (da/logout))} [:li {:on-click #(on-click % da/logout)}
i/exit i/exit
[:span (tr "ds.user.exit")]]])) [:span (tr "ds.user.exit")]]]))

View file

@ -9,7 +9,6 @@
(:require (:require
[beicon.core :as rx] [beicon.core :as rx]
[lentes.core :as l] [lentes.core :as l]
[rumext.core :as mx]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.main.constants :as c] [uxbox.main.constants :as c]
[uxbox.main.data.history :as udh] [uxbox.main.data.history :as udh]
@ -86,7 +85,7 @@
(mf/use-effect #(subscribe canvas page) (mf/use-effect #(subscribe canvas page)
#js [(:id page)]) #js [(:id page)])
[:* [:*
(messages-widget) [:& messages-widget]
[:& header {:page page [:& header {:page page
:flags flags :flags flags
:key (:id page)}] :key (:id page)}]

View file

@ -2,12 +2,13 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; 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/. ;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; ;;
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; Copyright (c) 2015-2019 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.workspace.sidebar.sitemap-forms (ns uxbox.main.ui.workspace.sidebar.sitemap-forms
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[struct.alpha :as s]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.constants :as c] [uxbox.main.constants :as c]
[uxbox.main.data.pages :as udp] [uxbox.main.data.pages :as udp]
@ -17,12 +18,12 @@
[uxbox.util.forms :as fm] [uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]])) [uxbox.util.i18n :refer [tr]]))
(def page-form-spec (s/defs ::page-form
{:id [fm/uuid] (s/dict :id (s/opt ::s/uuid)
:project [fm/uuid] :project ::s/uuid
:name [fm/required fm/string] :name (s/&& ::s/string ::fm/not-empty-string)
:width [fm/required fm/number-str] :width ::s/number-str
:height [fm/required fm/number-str]}) :height ::s/number-str))
(def defaults (def defaults
{:name "" {:name ""
@ -52,13 +53,13 @@
(mf/defc page-form (mf/defc page-form
[{:keys [page] :as props}] [{:keys [page] :as props}]
(let [{:keys [data errors] :as form} (fm/use-form {:initial #(initial-data page) (let [{:keys [data] :as form} (fm/use-form ::page-form #(initial-data page))]
:spec page-form-spec})]
[:form {:on-submit #(on-submit % form)} [:form {:on-submit #(on-submit % form)}
[:input.input-text [:input.input-text
{:placeholder "Page name" {:placeholder "Page name"
:type "text" :type "text"
:name "name" :name "name"
:class (fm/error-class form :name)
:on-blur (fm/on-input-blur form :name) :on-blur (fm/on-input-blur form :name)
:on-change (fm/on-input-change form :name) :on-change (fm/on-input-change form :name)
:value (:name data) :value (:name data)
@ -72,6 +73,7 @@
:type "number" :type "number"
:min 0 :min 0
:max 5000 :max 5000
:class (fm/error-class form :width)
:on-blur (fm/on-input-blur form :width) :on-blur (fm/on-input-blur form :width)
:on-change (fm/on-input-change form :width) :on-change (fm/on-input-change form :width)
:value (:width data)}]] :value (:width data)}]]
@ -84,12 +86,14 @@
:type "number" :type "number"
:min 0 :min 0
:max 5000 :max 5000
:class (fm/error-class form :height)
:on-blur (fm/on-input-blur form :height) :on-blur (fm/on-input-blur form :height)
:on-change (fm/on-input-change form :height) :on-change (fm/on-input-change form :height)
:value (:height data)}]]] :value (:height data)}]]]
[:input.btn-primary [:input.btn-primary
{:value "Go go go!" {:value "Go go go!"
:type "submit" :type "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))}]])) :disabled (not (:valid form))}]]))
(mf/defc page-form-dialog (mf/defc page-form-dialog

View file

@ -8,26 +8,16 @@
(:refer-clojure :exclude [uuid]) (:refer-clojure :exclude [uuid])
(:require (:require
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s :include-macros true] [cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[lentes.core :as l] [lentes.core :as l]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[rumext.core :as mx] [rumext.core :as mx]
[struct.core :as st] [struct.alpha :as st]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.i18n :refer [tr]])) [uxbox.util.i18n :refer [tr]]))
;; --- Main Api
(defn validate
[data spec]
(st/validate data spec))
(defn valid?
[data spec]
(st/valid? data spec))
;; --- Handlers Helpers ;; --- Handlers Helpers
(defn- impl-mutator (defn- impl-mutator
@ -45,8 +35,8 @@
([self f x y more] (update-fn #(apply f % x y more)))))) ([self f x y more] (update-fn #(apply f % x y more))))))
(defn- translate-error-type (defn- translate-error-type
[code] [name]
(case code (case name
::st/string "errors.form.string" ::st/string "errors.form.string"
::st/number "errors.form.number" ::st/number "errors.form.number"
::st/number-str "errors.form.number" ::st/number-str "errors.form.number"
@ -54,31 +44,35 @@
::st/integer-str "errors.form.integer" ::st/integer-str "errors.form.integer"
::st/required "errors.form.required" ::st/required "errors.form.required"
::st/email "errors.form.email" ::st/email "errors.form.email"
::st/identical-to "errors.form.does-not-match" ;; ::st/identical-to "errors.form.does-not-match"
"errors.undefined-error")) "errors.undefined-error"))
(defn- translate-errors (defn- process-errors
[errors] [errors]
(reduce-kv (fn [acc key val] (reduce (fn [acc {:keys [path name] :as error}]
(if (string? (:message val)) (let [message (translate-error-type name)]
(assoc acc key val) (assoc-in acc path
(->> (translate-error-type (:code val)) (-> (assoc error :message message)
(assoc val :message) (dissoc :path)))))
(assoc acc key))))
{} errors)) {} errors))
(defn use-form (defn use-form
[{:keys [initial spec] :as opts}] [spec initial]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
:errors {} :errors {}
:touched {}}) :touched {}})
[errors clean-data] (validate spec (:data state)) cdata (st/conform spec (:data state))
errors (merge (translate-errors errors) errors' (when (= ::st/invalid cdata)
(st/explain spec (:data state)))
errors (merge (process-errors errors')
(:errors state))] (:errors state))]
(-> (assoc state (-> (assoc state
:errors errors :errors errors
:clean-data clean-data :clean-data (when (not= cdata ::st/invalid) cdata)
:valid (not (seq errors))) :valid (and (empty? errors)
(not= cdata ::st/invalid)))
(impl-mutator update-state)))) (impl-mutator update-state))))
(defn on-input-change (defn on-input-change
@ -123,15 +117,17 @@
;; --- Additional Validators ;; --- Additional Validators
(def string (assoc st/string :message "errors.should-be-string")) (st/defs ::not-empty-string #(not (empty? %)))
(def number (assoc st/number :message "errors.should-be-number"))
(def number-str (assoc st/number-str :message "errors.should-be-number")) ;; (def string (assoc st/string :message "errors.should-be-string"))
(def integer (assoc st/integer :message "errors.should-be-integer")) ;; (def number (assoc st/number :message "errors.should-be-number"))
(def integer-str (assoc st/integer-str :message "errors.should-be-integer")) ;; (def number-str (assoc st/number-str :message "errors.should-be-number"))
(def required (assoc st/required :message "errors.required")) ;; (def integer (assoc st/integer :message "errors.should-be-integer"))
(def email (assoc st/email :message "errors.should-be-valid-email")) ;; (def integer-str (assoc st/integer-str :message "errors.should-be-integer"))
(def uuid (assoc st/uuid :message "errors.should-be-uuid")) ;; (def required (assoc st/required :message "errors.required"))
(def uuid-str (assoc st/uuid-str :message "errors.should-be-valid-uuid")) ;; (def email (assoc st/email :message "errors.should-be-valid-email"))
;; (def uuid (assoc st/uuid :message "errors.should-be-uuid"))
;; (def uuid-str (assoc st/uuid-str :message "errors.should-be-valid-uuid"))
;; DEPRECATED ;; DEPRECATED
@ -154,6 +150,9 @@
(s/def ::non-empty-string (s/def ::non-empty-string
(s/and string? #(not (str/empty? %)))) (s/and string? #(not (str/empty? %))))
(s/def ::not-empty #(not (str/empty? %)))
(defn- parse-number (defn- parse-number
[v] [v]
(cond (cond
@ -167,6 +166,8 @@
(s/def ::color (s/def ::color
(s/and string? #(boolean (re-matches color-re %)))) (s/and string? #(boolean (re-matches color-re %))))
;; --- Form State Events ;; --- Form State Events
;; --- Assoc Error ;; --- Assoc Error

View file

@ -6,15 +6,16 @@
(ns uxbox.util.messages (ns uxbox.util.messages
"Messages notifications." "Messages notifications."
(:require [lentes.core :as l] (:require
[cuerdas.core :as str]
[beicon.core :as rx] [beicon.core :as rx]
[cuerdas.core :as str]
[lentes.core :as l]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.util.timers :as ts]
[rumext.core :as mx :include-macros true]
[uxbox.util.data :refer [classnames]] [uxbox.util.data :refer [classnames]]
[uxbox.util.dom :as dom])) [uxbox.util.dom :as dom]
[uxbox.util.timers :as ts]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Events ;; Data Events
@ -29,7 +30,12 @@
(declare hide) (declare hide)
(declare show?) (declare show?)
(deftype Show [data] (defn show
[data]
(reify
ptk/EventType
(type [_] ::show)
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [message (assoc data :state :visible)] (let [message (assoc data :state :visible)]
@ -41,15 +47,11 @@
(rx/take 1))] (rx/take 1))]
(->> (rx/of (hide)) (->> (rx/of (hide))
(rx/delay (:timeout data)) (rx/delay (:timeout data))
(rx/take-until stoper))))) (rx/take-until stoper))))))
(defn show
[message]
(Show. message))
(defn show? (defn show?
[v] [v]
(instance? Show v)) (= ::show (ptk/type v)))
(defn error (defn error
[message & {:keys [timeout] :or {timeout 3000}}] [message & {:keys [timeout] :or {timeout 3000}}]
@ -73,25 +75,25 @@
;; --- Hide Message ;; --- Hide Message
(deftype Hide [^:mutable canceled?] (defn hide
[]
(let [canceled? (volatile! {})]
(reify
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :message (update state :message
(fn [v] (fn [v]
(if (nil? v) (if (nil? v)
(do (set! canceled? true) nil) (do (vreset! canceled? true) nil)
(assoc v :state :hide))))) (assoc v :state :hide)))))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(if canceled? (if @canceled?
(rx/empty) (rx/empty)
(->> (rx/of #(dissoc state :message)) (->> (rx/of #(dissoc % :message))
(rx/delay +animation-timeout+))))) (rx/delay +animation-timeout+)))))))
(defn hide
[]
(Hide. false))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UI Components ;; UI Components
@ -99,10 +101,10 @@
;; --- Notification Component ;; --- Notification Component
(mx/defc notification-box (mf/defc notification-box
{:mixins [mx/static]} [{:keys [message on-close] :as message}]
[{:keys [type on-close] :as message}] (let [type (:type message)
(let [classes (classnames :error (= type :error) classes (classnames :error (= type :error)
:info (= type :info) :info (= type :info)
:hide-message (= (:state message) :hide) :hide-message (= (:state message) :hide)
:quick true)] :quick true)]
@ -113,9 +115,8 @@
;; --- Dialog Component ;; --- Dialog Component
(mx/defc dialog-box (mf/defc dialog-box
{:mixins [mx/static mx/reactive]} [{:keys [on-accept on-cancel on-close message] :as props}]
[{:keys [on-accept on-cancel on-close] :as message}]
(let [classes (classnames :info true (let [classes (classnames :info true
:hide-message (= (:state message) :hide))] :hide-message (= (:state message) :hide))]
(letfn [(accept [event] (letfn [(accept [event]
@ -142,11 +143,10 @@
;; --- Main Component (entry point) ;; --- Main Component (entry point)
(mx/defc messages-widget (mf/defc messages-widget
{:mixins [mx/static mx/reactive]} [{:keys [message] :as props}]
[message]
(case (:type message) (case (:type message)
:error (notification-box message) :error (mf/element notification-box props)
:info (notification-box message) :info (mf/element notification-box props)
:dialog (dialog-box message) :dialog (mf/element dialog-box props)
nil)) nil))