0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-15 17:21:17 -05:00

🚧 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,154 +5,144 @@
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.data.auth
(:require [cljs.spec.alpha :as s]
[beicon.core :as rx]
[potok.core :as ptk]
[uxbox.main.store :as st]
[uxbox.main.repo :as rp]
[uxbox.main.store :refer [initial-state]]
[uxbox.main.data.projects :as udp]
[uxbox.main.data.users :as udu]
[uxbox.util.messages :as uum]
[uxbox.util.router :as rt]
[uxbox.util.spec :as us]
[uxbox.util.i18n :as i18n :refer [tr]]
[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?)
(:require
[struct.alpha :as st]
[beicon.core :as rx]
[potok.core :as ptk]
[uxbox.main.repo :as rp]
[uxbox.main.store :refer [initial-state]]
[uxbox.main.data.users :as du]
[uxbox.util.messages :as um]
[uxbox.util.router :as rt]
[uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.storage :refer [storage]]))
;; --- Logged In
(defrecord LoggedIn [data]
ptk/UpdateEvent
(update [this state]
(assoc state :auth data))
ptk/WatchEvent
(watch [this state s]
(swap! storage assoc :auth data)
(rx/of (udu/fetch-profile)
(rt/navigate :dashboard/projects))))
(defn logged-in?
[v]
(instance? LoggedIn v))
;; TODO: add spec
(defn logged-in
[data]
(LoggedIn. data))
(reify
ptk/EventType
(type [_] ::logged-in)
ptk/UpdateEvent
(update [this state]
(assoc state :auth data))
ptk/WatchEvent
(watch [this state s]
(swap! storage assoc :auth data)
(rx/of (du/fetch-profile)
(rt/navigate :dashboard/projects)))))
(defn logged-in?
[v]
(= (ptk/type v) ::logged-in))
;; --- Login
(defrecord Login [username password]
ptk/UpdateEvent
(update [_ state]
(merge state (dissoc initial-state :route :router)))
ptk/WatchEvent
(watch [this state s]
(let [params {:username username
:password password
:scope "webapp"}
on-error #(rx/of (uum/error (tr "errors.auth.unauthorized")))]
(->> (rp/req :auth/login params)
(rx/map :payload)
(rx/map logged-in)
(rx/catch rp/client-error? on-error)))))
(s/def ::login-event
(s/keys :req-un [::username ::password]))
(st/defs ::login
(st/dict :username ::st/string
:password ::st/string))
(defn login
[params]
{:pre [(us/valid? ::login-event params)]}
(map->Login params))
[{:keys [username password] :as data}]
(assert (st/valid? ::login data))
(reify
ptk/UpdateEvent
(update [_ state]
(merge state (dissoc initial-state :route :router)))
ptk/WatchEvent
(watch [this state s]
(let [params {:username username
:password password
:scope "webapp"}
on-error #(rx/of (um/error (tr "errors.auth.unauthorized")))]
(->> (rp/req :auth/login params)
(rx/map :payload)
(rx/map logged-in)
(rx/catch rp/client-error? on-error))))))
;; --- Logout
(defrecord ClearUserData []
ptk/UpdateEvent
(update [_ state]
(merge state (dissoc initial-state :route :router)))
(def clear-user-data
(reify
ptk/UpdateEvent
(update [_ state]
(merge state (dissoc initial-state :route :router)))
ptk/WatchEvent
(watch [_ state s]
(->> (rp/req :auth/logout)
(rx/ignore)))
ptk/WatchEvent
(watch [_ state s]
(->> (rp/req :auth/logout)
(rx/ignore)))
ptk/EffectEvent
(effect [_ state s]
(reset! storage {})
(i18n/set-default-locale!)))
ptk/EffectEvent
(effect [_ state s]
(reset! storage {})
(i18n/set-default-locale!))))
(defrecord Logout []
ptk/WatchEvent
(watch [_ state s]
(rx/of (rt/nav :auth/login)
(->ClearUserData))))
(defn logout
[]
(->Logout))
(def logout
(reify
ptk/WatchEvent
(watch [_ state s]
(rx/of (rt/nav :auth/login)
clear-user-data))))
;; --- Register
;; TODO: clean form on success
(defrecord Register [data on-error]
ptk/WatchEvent
(watch [_ state stream]
(letfn [(handle-error [{payload :payload}]
(on-error payload)
(rx/empty))]
(rx/merge
(->> (rp/req :auth/register data)
(rx/map :payload)
(rx/map (constantly ::registered))
(rx/catch rp/client-error? handle-error))
(->> stream
(rx/filter #(= % ::registered))
(rx/take 1)
(rx/map #(login data)))))))
(s/def ::register-event
(s/keys :req-un [::fullname ::username ::email ::password]))
(st/defs ::register
(st/dict :fullname ::st/string
:username ::st/string
:password ::st/string
:email ::st/email))
(defn register
"Create a register event instance."
[data on-error]
{:pre [(us/valid? ::register-event data)
(fn? on-error)]}
(Register. data on-error))
(assert (st/valid? ::register data))
(assert (fn? on-error))
(reify
ptk/WatchEvent
(watch [_ state stream]
(letfn [(handle-error [{payload :payload}]
(on-error payload)
(rx/empty))]
(rx/merge
(->> (rp/req :auth/register data)
(rx/map :payload)
(rx/map (constantly ::registered))
(rx/catch rp/client-error? handle-error))
(->> stream
(rx/filter #(= % ::registered))
(rx/take 1)
(rx/map #(login data))))))))
;; --- Recovery Request
(defrecord RecoveryRequest [data]
ptk/WatchEvent
(watch [_ state stream]
(letfn [(on-error [{payload :payload}]
(println "on-error" payload)
(rx/empty))]
(rx/merge
(->> (rp/req :auth/recovery-request data)
(rx/map (constantly ::recovery-requested))
(rx/catch rp/client-error? on-error))
(->> stream
(rx/filter #(= % ::recovery-requested))
(rx/take 1)
(rx/map #(uum/info (tr "auth.message.recovery-token-sent"))))))))
(s/def ::recovery-request-event
(s/keys :req-un [::username]))
(st/defs ::recovery-request
(st/dict :username ::st/string))
(defn recovery-request
[data]
{:pre [(us/valid? ::recovery-request-event data)]}
(RecoveryRequest. data))
(assert (st/valid? ::recovery-request data))
(reify
ptk/WatchEvent
(watch [_ state stream]
(letfn [(on-error [{payload :payload}]
(println "on-error" payload)
(rx/empty))]
(rx/merge
(->> (rp/req :auth/recovery-request data)
(rx/map (constantly ::recovery-requested))
(rx/catch rp/client-error? on-error))
(->> stream
(rx/filter #(= % ::recovery-requested))
(rx/take 1)
;; TODO: this should be moved to the UI part
(rx/map #(um/info (tr "auth.message.recovery-token-sent")))))))))
;; --- Check Recovery Token
@ -162,7 +152,7 @@
(letfn [(on-error [{payload :payload}]
(rx/of
(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)
(rx/ignore)
(rx/catch rp/client-error? on-error)))))
@ -174,23 +164,22 @@
;; --- Recovery (Password)
(defrecord Recovery [token password]
ptk/WatchEvent
(watch [_ state stream]
(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]))
(st/defs ::recovery
(st/dict :username ::st/string
:token ::st/string))
(defn recovery
[{:keys [token password] :as data}]
{:pre [(us/valid? ::recovery-event data)]}
(Recovery. token password))
(assert (st/valid? ::recovery data))
(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
(:require
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.core :as ptk]
[struct.alpha :as st]
[uxbox.main.repo :as rp]
[uxbox.main.store :as st]
[uxbox.util.data :refer [index-by-id]]
[uxbox.util.spec :as us]
[uxbox.util.timers :as ts]
[uxbox.util.uuid :as uuid]))
;; --- Specs
;; --- Struct
(s/def ::grid-x-axis number?)
(s/def ::grid-y-axis number?)
(s/def ::grid-color string?)
(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?)
(st/defs ::inst inst?)
(st/defs ::width (st/&& ::st/number ::st/positive))
(st/defs ::height (st/&& ::st/number ::st/positive))
(s/def ::metadata
(s/keys :req-un [::width ::height]
:opt-un [::grid-y-axis
::grid-x-axis
::grid-color
::grid-alignment
::order
::background
::background-opacity
::layout]))
(st/defs ::metadata
(st/dict :width ::width
:height ::height
:grid-y-axis (st/opt ::st/number)
:grid-x-axis (st/opt ::st/number)
:grid-color (st/opt ::st/string)
:order (st/opt ::st/number)
:background (st/opt ::st/string)
:background-opacity (st/opt ::st/number)))
(s/def ::id uuid?)
(s/def ::name string?)
(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)))
(st/defs ::shapes-list
(st/coll-of ::st/uuid))
(s/def ::page-entity
(s/keys :req-un [::id
::name
::project
::version
::created-at
::modified-at
::user
::metadata
::shapes]))
(st/defs ::page-entity
(st/dict :id ::st/uuid
:name ::st/string
:project ::st/uuid
:created-at ::inst
:modified-at ::inst
:user ::st/uuid
:metadata ::metadata
:shapes ::shapes-list))
;; 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
@ -170,44 +173,39 @@
(declare rehash-pages)
(deftype PageCreated [data]
ptk/UpdateEvent
(update [_ state]
(let [project-id (:project data)]
(-> (update-in state [:projects project-id :pages] conj (:id data))
(unpack-page data)
(assoc-packed-page data))))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (rehash-pages (:project data)))))
(s/def ::page-created
(s/keys :req-un [::id
::name
::project
::metadata]))
(st/defs ::page-created
(st/dict :id ::st/uuid
:name ::st/string
:project ::st/uuid
:metadata ::metadata))
(defn page-created
[data]
{:pre [(us/valid? ::page-created data)]}
(PageCreated. data))
(assert (st/valid? ::page-created data) "invalid parameters")
(reify
ptk/UpdateEvent
(update [_ state]
(let [pid (:project data)]
(-> state
(update-in [:projects pid :pages] (fnil conj []) (:id data))
(unpack-page data)
(assoc-packed-page data))))
(defn page-created?
[o]
(instance? PageCreated o))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (rehash-pages (:project data))))))
;; --- Create Page
(s/def ::create-page-params
(s/keys :req-un [::name
::project
::width
::height]))
(st/defs ::create-page
(st/dict :name ::st/string
:project ::st/uuid
:width ::width
:height ::height))
(defn create-page
[{:keys [name project width height layout] :as data}]
{:pre [(us/valid? ::create-page-params data)]}
(assert (st/valid? ::create-page data))
(reify
ptk/WatchEvent
(watch [this state s]
@ -231,11 +229,9 @@
;; --- Page Persisted
;; TODO: add page spec
(defn page-persisted
[data]
{:pre [(map? data)]}
(assert (st/valid? ::server-page data))
(reify
cljs.core/IDeref
(-deref [_] data)
@ -245,6 +241,7 @@
ptk/UpdateEvent
(update [_ state]
(prn "page-persisted" data)
(let [{:keys [id version]} data]
(-> state
(assoc-in [:pages id :version] version)
@ -256,30 +253,30 @@
;; --- Persist Page
(deftype PersistPage [id on-success]
ptk/WatchEvent
(watch [this state s]
(let [page (get-in state [:pages id])]
(if (:history page)
(rx/empty)
(let [page (pack-page state id)]
(->> (rp/req :update/page page)
(rx/map :payload)
(rx/do #(when (fn? on-success)
(ts/schedule-on-idle on-success)))
(rx/map page-persisted)))))))
(defn persist-page
([id] (persist-page id identity))
([id on-success]
(assert (uuid? id))
(reify
ptk/EventType
(type [_] ::persist-page)
ptk/WatchEvent
(watch [this state s]
(prn "persist-page" id)
(let [page (get-in state [:pages id])]
(if (:history page)
(rx/empty)
(let [page (pack-page state id)]
(->> (rp/req :update/page page)
(rx/map :payload)
(rx/do #(when (fn? on-success)
(ts/schedule-on-idle on-success)))
(rx/map page-persisted)))))))))
(defn persist-page?
[v]
(instance? PersistPage v))
(defn persist-page
([id]
{:pre [(uuid? id)]}
(PersistPage. id (constantly nil)))
([id on-success]
{:pre [(uuid? id)]}
(PersistPage. id on-success)))
(= ::persist-page (ptk/type v)))
;; --- Page Metadata Persisted
@ -288,8 +285,10 @@
(update [_ state]
(assoc-in state [:pages id :version] (:version data))))
(s/def ::metadata-persisted-event
(s/keys :req-un [::id ::version]))
(st/defs ::version integer?)
(st/defs ::metadata-persisted-event
(st/dict :id ::st/uuid
:version ::version))
(defn metadata-persisted?
[v]
@ -297,7 +296,7 @@
(defn metadata-persisted
[{:keys [id] :as data}]
{:pre [(us/valid? ::metadata-persisted-event data)]}
{:pre [(st/valid? ::metadata-persisted-event data)]}
(MetadataPersisted. id data))
;; --- Persist Page Metadata
@ -327,7 +326,7 @@
(defn update-page
[id data]
{:pre [(uuid? id) (us/valid? ::page-entity data)]}
{:pre [(uuid? id) (st/valid? ::page-entity data)]}
(UpdatePage. id data))
;; --- Update Page Metadata
@ -340,7 +339,7 @@
(defn update-metadata
[id metadata]
{:pre [(uuid? id) (us/valid? ::metadata metadata)]}
{:pre [(uuid? id) (st/valid? ::metadata metadata)]}
(UpdateMetadata. id metadata))
;; --- Rehash Pages
@ -385,12 +384,15 @@
;; A specialized event for persist data
;; from the update page form.
(s/def ::persist-page-update-form-params
(s/keys :req-un [::id ::name ::width ::height]))
(st/defs ::persist-page-update-form
(st/dict :id ::st/uuid
:name ::st/string
:width ::width
:height ::height))
(defn persist-page-update-form
[{: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
ptk/WatchEvent
(watch [_ state stream]

View file

@ -5,17 +5,18 @@
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.data.projects
(:require [cljs.spec.alpha :as s]
[cuerdas.core :as str]
[beicon.core :as rx]
[potok.core :as ptk]
[uxbox.main.store :as st]
[uxbox.main.repo :as rp]
[uxbox.main.data.pages :as udp]
[uxbox.util.uuid :as uuid]
[uxbox.util.spec :as us]
[uxbox.util.time :as dt]
[uxbox.util.router :as rt]))
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[beicon.core :as rx]
[potok.core :as ptk]
[struct.core :as st]
[uxbox.main.repo :as rp]
[uxbox.main.data.pages :as udp]
[uxbox.util.uuid :as uuid]
[uxbox.util.spec :as us]
[uxbox.util.time :as dt]
[uxbox.util.router :as rt]))
;; --- Specs
@ -34,12 +35,21 @@
::created-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
(defn assoc-project
"A reduce function for assoc the project to the state map."
[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))
(defn dissoc-project
@ -160,24 +170,23 @@
;; --- Create Project
(s/def ::create-project-params
(s/keys :req-un [::name ::udp/width ::udp/height]))
(st/defs create-project-spec
{:name [st/required st/string]
:width [st/required st/number st/positive]
:height [st/required st/number st/positive]})
(defn create-project
[{: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
ptk/WatchEvent
(watch [this state stream]
(rx/merge
(->> (rp/req :create/project {:name name})
(rx/map :payload)
(rx/map (fn [{:keys [id] :as project}]
(udp/create-page (assoc params :project id)))))
(->> stream
(rx/filter udp/page-created?)
(rx/take 1)
(rx/map #(fetch-projects)))))))
(->> (rp/req :create/project {:name name})
(rx/map :payload)
(rx/mapcat (fn [{:keys [id] :as project}]
(rx/of #(assoc-project % project)
(udp/create-page (assoc params :project id)))))))))
;; --- Go To Project

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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