From 1aa236e81271cdbaf9e37fad724a966d6b33b2c8 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 8 Mar 2017 16:58:00 +0100 Subject: [PATCH] Replace funcool/struct with cljs.spec. As a result, one dependency less. --- frontend/project.clj | 9 +- frontend/src/uxbox/main/data/users.cljs | 61 ++--- frontend/src/uxbox/main/ui/auth/login.cljs | 39 +-- frontend/src/uxbox/main/ui/auth/recovery.cljs | 36 +-- .../uxbox/main/ui/auth/recovery_request.cljs | 36 +-- frontend/src/uxbox/main/ui/auth/register.cljs | 61 ++--- .../src/uxbox/main/ui/dashboard/projects.cljs | 2 +- .../ui/dashboard/projects_createform.cljs | 176 +++++++------- .../src/uxbox/main/ui/settings/password.cljs | 69 ++++-- .../src/uxbox/main/ui/settings/profile.cljs | 57 +++-- .../workspace/sidebar/sitemap_pageform.cljs | 60 +++-- frontend/src/uxbox/util/forms.cljs | 225 ++++++++---------- 12 files changed, 440 insertions(+), 391 deletions(-) diff --git a/frontend/project.clj b/frontend/project.clj index 6373f108a..34f10d116 100644 --- a/frontend/project.clj +++ b/frontend/project.clj @@ -10,7 +10,7 @@ :profiles {:dev {:source-paths ["dev"]}} :dependencies [[org.clojure/clojure "1.9.0-alpha14" :scope "provided"] - [org.clojure/clojurescript "1.9.494" :scope "provided"] + [org.clojure/clojurescript "1.9.495" :scope "provided"] ;; Build [figwheel-sidecar "0.5.9" :scope "provided"] @@ -24,12 +24,11 @@ [cljsjs/react-dom "15.4.2-2"] [cljsjs/react-dom-server "15.4.2-2"] - [funcool/potok "2.0.0"] - [funcool/struct "1.0.0"] - [funcool/lentes "1.2.0"] [funcool/beicon "3.1.1"] + [funcool/bide "1.4.0"] [funcool/cuerdas "2.0.3"] - [funcool/bide "1.4.0"]] + [funcool/lentes "1.2.0"] + [funcool/potok "2.0.0"]] :plugins [[lein-ancient "0.6.10"]] :clean-targets ^{:protect false} ["resources/public/js" "target"] ) diff --git a/frontend/src/uxbox/main/data/users.cljs b/frontend/src/uxbox/main/data/users.cljs index 02a52dac0..e10ce950f 100644 --- a/frontend/src/uxbox/main/data/users.cljs +++ b/frontend/src/uxbox/main/data/users.cljs @@ -13,11 +13,6 @@ [uxbox.util.i18n :refer (tr)] [uxbox.util.messages :as uum])) -(s/def ::fullname string?) -(s/def ::email us/email?) -(s/def ::username string?) -(s/def ::theme string?) - ;; --- Profile Fetched (deftype ProfileFetched [data] @@ -68,48 +63,53 @@ (rx/map profile-updated) (rx/catch rp/client-error? handle-error))))) -(s/def ::update-profile-event - (s/keys :req-un [::fullname ::email ::username ::theme])) +(s/def ::fullname string?) +(s/def ::email us/email?) +(s/def ::username string?) +(s/def ::theme string?) + +(s/def ::update-profile + (s/keys :req-un [::fullname + ::email + ::username + ::theme])) (defn update-profile [data on-success on-error] - {:pre [(us/valid? ::update-profile-event data) + {:pre [(us/valid? ::update-profile data) (fn? on-error) (fn? on-success)]} (UpdateProfile. data on-success on-error)) -;; --- Password Updated - -(deftype PasswordUpdated [] - ptk/WatchEvent - (watch [_ state stream] - (rx/of (uum/info (tr "settings.password-saved"))))) - -(defn password-updated - [] - (PasswordUpdated.)) - ;; --- Update Password (Form) -(deftype UpdatePassword [data] +(deftype UpdatePassword [data on-success on-error] ptk/WatchEvent (watch [_ state s] - (let [params {:old-password (:old-password data) + (let [params {:old-password (:password-old data) :password (:password-1 data)}] - (->> (rp/req :update/profile-password params) - (rx/map password-updated))))) + (->> (rp/req :update/profile-password params) + (rx/catch rp/client-error? (fn [e] + (on-error (:payload e)) + (rx/empty))) + (rx/do on-success) + (rx/ignore))))) (s/def ::password-1 string?) (s/def ::password-2 string?) -(s/def ::old-password string?) +(s/def ::password-old string?) -(s/def ::update-password-event - (s/keys :req-un [::password-1 ::password-2 ::old-password])) +(s/def ::update-password + (s/keys :req-un [::password-1 + ::password-2 + ::password-old])) (defn update-password - [data] - {:pre [(us/valid? ::update-password-event data)]} - (UpdatePassword. data)) + [data & {:keys [on-success on-error]}] + {:pre [(us/valid? ::update-password data) + (fn? on-success) + (fn? on-error)]} + (UpdatePassword. data on-success on-error)) ;; --- Update Photo @@ -123,5 +123,6 @@ (defn update-photo ([file] (update-photo file (constantly nil))) ([file done] - {:pre [(us/file? file) (fn? done)]} + {:pre [(us/file? file) + (fn? done)]} (UpdatePhoto. file done))) diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs index c59514baf..2bb241b9e 100644 --- a/frontend/src/uxbox/main/ui/auth/login.cljs +++ b/frontend/src/uxbox/main/ui/auth/login.cljs @@ -6,38 +6,45 @@ ;; Copyright (c) 2015-2016 Juan de la Cruz (ns uxbox.main.ui.auth.login - (:require [lentes.core :as l] + (:require [cljs.spec :as s :include-macros true] + [lentes.core :as l] [cuerdas.core :as str] - [potok.core :as ptk] [uxbox.builtins.icons :as i] [uxbox.config :as cfg] [uxbox.main.store :as st] [uxbox.main.data.auth :as da] [uxbox.main.ui.messages :refer [messages-widget]] [uxbox.main.ui.navigation :as nav] - [uxbox.util.mixins :as mx :include-macros true] - [uxbox.util.router :as rt] [uxbox.util.dom :as dom] - [uxbox.util.forms :as forms])) + [uxbox.util.forms :as fm] + [uxbox.util.mixins :as mx :include-macros true] + [uxbox.util.router :as rt])) -(def form-data (forms/focus-data :login st/state)) -(def set-value! (partial forms/set-value! st/store :login)) +(def form-data (fm/focus-data :login st/state)) +(def form-errors (fm/focus-errors :login st/state)) + +(def assoc-value (partial fm/assoc-value :login)) +(def assoc-errors (partial fm/assoc-errors :login)) +(def clear-form (partial fm/clear-form :login)) + +(s/def ::username ::fm/non-empty-string) +(s/def ::password ::fm/non-empty-string) + +(s/def ::login-form + (s/keys :req-un [::username ::password])) -(def +login-form+ - {:email [forms/required forms/string] - :password [forms/required forms/string]}) (mx/defc login-form {:mixins [mx/static mx/reactive]} [] (let [data (mx/react form-data) - valid? (forms/valid? data +login-form+)] + valid? (fm/valid? ::login-form data)] (letfn [(on-change [event field] (let [value (dom/event->value event)] - (set-value! field value))) + (st/emit! (assoc-value field value)))) (on-submit [event] (dom/prevent-default event) - (st/emit! (da/login {:username (:email data) + (st/emit! (da/login {:username (:username data) :password (:password data)})))] [:form {:on-submit on-submit} [:div.login-content @@ -52,8 +59,8 @@ {:name "email" :tab-index "2" :ref "email" - :value (:email data "") - :on-change #(on-change % :email) + :value (:username data "") + :on-change #(on-change % :username) :placeholder "Email or Username" :type "text"}] [:input.input-text @@ -80,7 +87,7 @@ "Don't have an account?"]]]]))) (mx/defc login-page - {:mixins [mx/static (forms/clear-mixin st/store :login)] + {:mixins [mx/static (fm/clear-mixin st/store :login)] :will-mount (fn [own] (when @st/auth-ref (st/emit! (rt/navigate :dashboard/projects))) diff --git a/frontend/src/uxbox/main/ui/auth/recovery.cljs b/frontend/src/uxbox/main/ui/auth/recovery.cljs index 085206376..bc82f88f7 100644 --- a/frontend/src/uxbox/main/ui/auth/recovery.cljs +++ b/frontend/src/uxbox/main/ui/auth/recovery.cljs @@ -6,42 +6,44 @@ ;; Copyright (c) 2015-2017 Juan de la Cruz (ns uxbox.main.ui.auth.recovery - (:require [lentes.core :as l] + (:require [cljs.spec :as s :include-macros true] + [lentes.core :as l] [cuerdas.core :as str] - [potok.core :as ptk] [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.router :as rt] - [uxbox.util.forms :as forms] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] [uxbox.util.mixins :as mx :include-macros true] - [uxbox.util.dom :as dom])) + [uxbox.util.router :as rt])) +(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)) ;; --- Recovery Form -(def form-data (forms/focus-data :recovery st/state)) -(def set-value! (partial forms/set-value! st/store :recovery)) - -(def +recovery-form+ - {:password [forms/required forms/string]}) +(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? (forms/valid? data +recovery-form+)] + (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)] - (set-value! field value))) + (st/emit! (assoc-value field value)))) (on-submit [event] (dom/prevent-default event) (st/emit! (uda/recovery data) - (forms/clear-form :recovery) - (forms/clear-errors :recovery)))] + (clear-form)))] [:form {:on-submit on-submit} [:div.login-content [:input.input-text @@ -68,7 +70,7 @@ own)) (mx/defc recovery-page - {:mixins [mx/static (forms/clear-mixin st/store :recovery)] + {:mixins [mx/static (fm/clear-mixin st/store :recovery)] :will-mount recovery-page-will-mount} [token] [:div.login diff --git a/frontend/src/uxbox/main/ui/auth/recovery_request.cljs b/frontend/src/uxbox/main/ui/auth/recovery_request.cljs index a6722bd67..a0c53a65d 100644 --- a/frontend/src/uxbox/main/ui/auth/recovery_request.cljs +++ b/frontend/src/uxbox/main/ui/auth/recovery_request.cljs @@ -6,45 +6,47 @@ ;; Copyright (c) 2015-2017 Juan de la Cruz (ns uxbox.main.ui.auth.recovery-request - (:require [lentes.core :as l] + (:require [cljs.spec :as s :include-macros true] + [lentes.core :as l] [cuerdas.core :as str] - [potok.core :as ptk] + [uxbox.builtins.icons :as i] [uxbox.main.store :as st] [uxbox.main.data.auth :as uda] - [uxbox.builtins.icons :as i] [uxbox.main.ui.messages :refer [messages-widget]] [uxbox.main.ui.navigation :as nav] - [uxbox.util.router :as rt] - [uxbox.util.forms :as forms] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] [uxbox.util.mixins :as mx :include-macros true] - [uxbox.util.dom :as dom])) + [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 (forms/focus-data :recovery-request st/state)) -(def set-value! (partial forms/set-value! st/store :recovery-request)) +(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 +recovery-request-form+ - {:username [forms/required forms/string]}) +(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? (forms/valid? data +recovery-request-form+)] - (letfn [(on-change [field event] + valid? (fm/valid? ::recovery-request-form data)] + (letfn [(on-change [event] (let [value (dom/event->value event)] - (set-value! field value))) + (st/emit! (assoc-value :username value)))) (on-submit [event] (dom/prevent-default event) (st/emit! (uda/recovery-request data) - (forms/clear-form :recovery-request) - (forms/clear-errors :recovery-request)))] + (clear-form)))] [:form {:on-submit on-submit} [:div.login-content [:input.input-text {:name "username" :value (:username data "") - :on-change (partial on-change :username) + :on-change on-change :placeholder "username or email address" :type "text"}] [:input.btn-primary @@ -59,7 +61,7 @@ ;; --- Recovery Request Page (mx/defc recovery-request-page - {:mixins [mx/static (forms/clear-mixin st/store :recovery-request)]} + {:mixins [mx/static (fm/clear-mixin st/store :recovery-request)]} [] [:div.login [:div.login-body diff --git a/frontend/src/uxbox/main/ui/auth/register.cljs b/frontend/src/uxbox/main/ui/auth/register.cljs index e46d6aa45..66c4fe8b5 100644 --- a/frontend/src/uxbox/main/ui/auth/register.cljs +++ b/frontend/src/uxbox/main/ui/auth/register.cljs @@ -2,52 +2,59 @@ ;; 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 -;; Copyright (c) 2015-2016 Juan de la Cruz +;; Copyright (c) 2015-2017 Andrey Antukh +;; Copyright (c) 2015-2017 Juan de la Cruz (ns uxbox.main.ui.auth.register - (:require [lentes.core :as l] + (:require [cljs.spec :as s :include-macros true] + [lentes.core :as l] [cuerdas.core :as str] - [potok.core :as ptk] + [uxbox.builtins.icons :as i] [uxbox.main.store :as st] [uxbox.main.data.auth :as uda] - [uxbox.builtins.icons :as i] [uxbox.main.ui.messages :refer [messages-widget]] [uxbox.main.ui.navigation :as nav] - [uxbox.util.router :as rt] - [uxbox.util.forms :as forms] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] [uxbox.util.mixins :as mx :include-macros true] - [uxbox.util.dom :as dom])) + [uxbox.util.router :as rt])) -;; --- Register Form +(def form-data (fm/focus-data :register st/state)) +(def form-errors (fm/focus-errors :register st/state)) -(def form-data (forms/focus-data :register st/state)) -(def form-errors (forms/focus-errors :register st/state)) -(def set-value! (partial forms/set-value! st/store :register)) -(def set-error! (partial forms/set-error! st/store :register)) +(def assoc-value (partial fm/assoc-value :register)) +(def assoc-error (partial fm/assoc-error :register)) +(def clear-form (partial fm/clear-form :register)) -(def +register-form+ - {:username [forms/required forms/string] - :fullname [forms/required forms/string] - :email [forms/required forms/email] - :password [forms/required forms/string]}) +;; 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 - (forms/clear-mixin st/store :register)]} + (fm/clear-mixin st/store :register)]} [] (let [data (mx/react form-data) errors (mx/react form-errors) - valid? (forms/valid? data +register-form+)] + valid? (fm/valid? ::register-form data)] (letfn [(on-change [field event] (let [value (dom/event->value event)] - (set-value! field value))) + (st/emit! (assoc-value field value)))) (on-error [{:keys [type code] :as payload}] (case code :uxbox.services.users/email-already-exists - (set-error! :email "Email already exists") + (st/emit! (assoc-error :email "Email already exists")) :uxbox.services.users/username-already-exists - (set-error! :username "Username already exists"))) + (st/emit! (assoc-error :username "Username already exists")))) (on-submit [event] (dom/prevent-default event) (st/emit! (uda/register data on-error)))] @@ -60,7 +67,7 @@ :on-change (partial on-change :fullname) :placeholder "Full Name" :type "text"}] - (forms/input-error errors :fullname) + (fm/input-error errors :fullname) [:input.input-text {:name "username" @@ -69,7 +76,7 @@ :on-change (partial on-change :username) :placeholder "Username" :type "text"}] - (forms/input-error errors :username) + (fm/input-error errors :username) [:input.input-text {:name "email" @@ -79,7 +86,7 @@ :on-change (partial on-change :email) :placeholder "Email" :type "text"}] - (forms/input-error errors :email) + (fm/input-error errors :email) [:input.input-text {:name "password" @@ -89,7 +96,7 @@ :on-change (partial on-change :password) :placeholder "Password" :type "password"}] - (forms/input-error errors :password) + (fm/input-error errors :password) [:input.btn-primary {:name "login" diff --git a/frontend/src/uxbox/main/ui/dashboard/projects.cljs b/frontend/src/uxbox/main/ui/dashboard/projects.cljs index becf85963..842f78b07 100644 --- a/frontend/src/uxbox/main/ui/dashboard/projects.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/projects.cljs @@ -209,7 +209,7 @@ (sort-projects-by ordering))] (letfn [(on-click [e] (dom/prevent-default e) - (udl/open! :new-project))] + (udl/open! :create-project))] [:section.dashboard-grid [:h2 "Your projects"] [:div.dashboard-grid-content diff --git a/frontend/src/uxbox/main/ui/dashboard/projects_createform.cljs b/frontend/src/uxbox/main/ui/dashboard/projects_createform.cljs index 556fd9368..feaac3b96 100644 --- a/frontend/src/uxbox/main/ui/dashboard/projects_createform.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/projects_createform.cljs @@ -6,42 +6,44 @@ ;; Copyright (c) 2015-2017 Juan de la Cruz (ns uxbox.main.ui.dashboard.projects-createform - (:require [lentes.core :as l] + (:require [cljs.spec :as s :include-macros true] + [lentes.core :as l] [cuerdas.core :as str] - [potok.core :as ptk] + [uxbox.builtins.icons :as i] [uxbox.main.store :as st] [uxbox.main.constants :as c] - [uxbox.main.exports :as exports] [uxbox.main.data.projects :as udp] [uxbox.main.data.lightbox :as udl] - [uxbox.builtins.icons :as i] - [uxbox.main.ui.dashboard.header :refer [header]] [uxbox.main.ui.lightbox :as lbx] - [uxbox.main.ui.keyboard :as kbd] + [uxbox.util.data :refer [read-string parse-int]] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] [uxbox.util.i18n :as t :refer [tr]] [uxbox.util.router :as r] - [uxbox.util.forms :as forms] - [uxbox.util.data :refer [read-string]] - [uxbox.util.dom :as dom] - [uxbox.util.blob :as blob] [uxbox.util.mixins :as mx :include-macros true] [uxbox.util.time :as dt])) -(def form-data (forms/focus-data :create-project st/state)) -(def form-errors (forms/focus-errors :create-project st/state)) -(def set-value! (partial forms/set-value! st/store :create-project)) -(def set-error! (partial forms/set-error! st/store :create-project)) -(def clear! (partial forms/clear! st/store :create-project)) +(def form-data (fm/focus-data :create-project st/state)) +(def form-errors (fm/focus-errors :create-project st/state)) -(def ^:private create-project-form - {:name [forms/required forms/string] - :width [forms/required forms/integer] - :height [forms/required forms/integer] - :layout [forms/required forms/string]}) +(def assoc-value (partial fm/assoc-value :create-project)) +(def clear-form (partial fm/clear-form :create-project)) -;; --- Lightbox: Layout input +(s/def ::name ::fm/non-empty-string) +(s/def ::layout ::fm/non-empty-string) +(s/def ::width number?) +(s/def ::height number?) + +(s/def ::project-form + (s/keys :req-un [::name + ::width + ::height + ::layout])) + +;; --- Create Project Form (mx/defc layout-input + {:mixins [mx/static]} [data layout-id] (let [layout (get c/page-layouts layout-id)] [:div @@ -51,17 +53,15 @@ :name "project-layout" :value (:name layout) :checked (when (= layout-id (:layout data)) "checked") - :on-change #(do - (set-value! :layout layout-id) - (set-value! :width (:width layout)) - (set-value! :height (:height layout)))}] + :on-change #(st/emit! (assoc-value :layout layout-id) + (assoc-value :width (:width layout)) + (assoc-value :height (:height layout)))}] [:label {:value (:name layout) :for layout-id} (:name layout)]])) -;; --- Lightbox: Layout selector - (mx/defc layout-selector + {:mixins [mx/static]} [data] [:div.input-radio.radio-primary (layout-input data "mobile") @@ -69,70 +69,84 @@ (layout-input data "notebook") (layout-input data "desktop")]) -;; -- New Project Lightbox - -(mx/defcs new-project-lightbox - {:mixins [mx/static mx/reactive - (forms/clear-mixin st/store :create-project)]} - [own] +(mx/defc create-project-form + {:mixins [mx/reactive mx/static]} + [] (let [data (merge c/project-defaults (mx/react form-data)) errors (mx/react form-errors) - valid? (forms/valid? data create-project-form)] + valid? (fm/valid? ::project-form data)] + (println data) + (println valid?) (letfn [(on-submit [event] (dom/prevent-default event) (when valid? (st/emit! (udp/create-project data)) (udl/close!))) - (set-value [event attr] - (set-value! attr (dom/event->value event))) + + (update-size [field e] + (let [value (dom/event->value e) + value (parse-int value)] + (st/emit! (assoc-value field value)))) + + (update-name [e] + (let [value (dom/event->value e)] + (st/emit! (assoc-value :name value)))) (swap-size [] - (set-value! :width (:height data)) - (set-value! :height (:width data))) - (close [] - (udl/close!) - (clear!))] - [:div.lightbox-body - [:h3 "New project"] - [:form {:on-submit on-submit} - [:input#project-name.input-text - {:placeholder "New project name" - :type "text" - :value (:name data) - :auto-focus true - :on-change #(set-value % :name)}] - [:div.project-size - [:div.input-element.pixels - [:span "Width"] - [:input#project-witdh.input-text - {:placeholder "Width" - :type "number" - :min 0 ;;TODO check this value - :max 666666 ;;TODO check this value - :value (:width data) - :on-change #(set-value % :width)}]] - [:a.toggle-layout {:on-click swap-size} i/toggle] - [:div.input-element.pixels - [:span "Height"] - [:input#project-height.input-text - {:placeholder "Height" - :type "number" - :min 0 ;;TODO check this value - :max 666666 ;;TODO check this value - :value (:height data) - :on-change #(set-value % :height)}]]] + (st/emit! (assoc-value :width (:height data)) + (assoc-value :height (:width data))))] + [:form {:on-submit on-submit} + [:input#project-name.input-text + {:placeholder "New project name" + :type "text" + :value (:name data) + :auto-focus true + :on-change update-name}] + [:div.project-size + [:div.input-element.pixels + [:span "Width"] + [:input#project-witdh.input-text + {:placeholder "Width" + :type "number" + :min 0 ;;TODO check this value + :max 666666 ;;TODO check this value + :value (:width data) + :on-change (partial update-size :width)}]] + [:a.toggle-layout {:on-click swap-size} i/toggle] + [:div.input-element.pixels + [:span "Height"] + [:input#project-height.input-text + {:placeholder "Height" + :type "number" + :min 0 ;;TODO check this value + :max 666666 ;;TODO check this value + :value (:height data) + :on-change (partial update-size :height)}]]] - ;; Layout selector - (layout-selector data) + ;; Layout selector + (layout-selector data) - ;; Submit - [:input#project-btn.btn-primary - {:value "Go go go!" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :type "submit"}]] - [:a.close {:on-click #(udl/close!)} i/close]]))) + ;; Submit + [:input#project-btn.btn-primary + {:value "Go go go!" + :class (when-not valid? "btn-disabled") + :disabled (not valid?) + :type "submit"}]]))) -(defmethod lbx/render-lightbox :new-project +;; --- Create Project Lightbox + +(mx/defcs create-project-lightbox + {:mixins [mx/static mx/reactive + (fm/clear-mixin st/store :create-project)]} + [own] + (letfn [(close [] + (udl/close!) + (st/emit! (clear-form)))] + [:div.lightbox-body + [:h3 "New project"] + (create-project-form) + [:a.close {:on-click #(udl/close!)} i/close]])) + +(defmethod lbx/render-lightbox :create-project [_] - (new-project-lightbox)) + (create-project-lightbox)) diff --git a/frontend/src/uxbox/main/ui/settings/password.cljs b/frontend/src/uxbox/main/ui/settings/password.cljs index 11f34a464..1b631a556 100644 --- a/frontend/src/uxbox/main/ui/settings/password.cljs +++ b/frontend/src/uxbox/main/ui/settings/password.cljs @@ -2,11 +2,12 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2016 Andrey Antukh -;; Copyright (c) 2016 Juan de la Cruz +;; Copyright (c) 2016-2017 Andrey Antukh +;; Copyright (c) 2016-2017 Juan de la Cruz (ns uxbox.main.ui.settings.password - (:require [lentes.core :as l] + (:require [cljs.spec :as s :include-macros true] + [lentes.core :as l] [cuerdas.core :as str] [potok.core :as ptk] [uxbox.main.store :as st] @@ -14,57 +15,75 @@ [uxbox.builtins.icons :as i] [uxbox.main.ui.messages :refer [messages-widget]] [uxbox.main.ui.settings.header :refer [header]] - [uxbox.util.forms :as forms] + [uxbox.util.i18n :refer [tr]] + [uxbox.util.forms :as fm] [uxbox.util.dom :as dom] + [uxbox.util.messages :as um] [uxbox.util.mixins :as mx :include-macros true])) +(def form-data (fm/focus-data :profile-password st/state)) +(def form-errors (fm/focus-errors :profile-password st/state)) -(def form-data (forms/focus-data :profile-password st/state)) -(def form-errors (forms/focus-errors :profile-password st/state)) -(def set-value! (partial forms/set-value! st/store :profile-password)) -(def set-errors! (partial forms/set-errors! st/store :profile-password)) +(def assoc-value (partial fm/assoc-value :profile-password)) +(def assoc-error (partial fm/assoc-error :profile-password)) +(def clear-form (partial fm/clear-form :profile-password)) -(def +password-form+ - [[:password-1 forms/required forms/string [forms/min-len 6]] - [:password-2 forms/required forms/string - [forms/identical-to :password-1 :message "errors.form.password-not-match"]] - [:old-password forms/required forms/string]]) +;; TODO: add better password validation + +(s/def ::password-1 ::fm/non-empty-string) +(s/def ::password-2 ::fm/non-empty-string) +(s/def ::password-old ::fm/non-empty-string) + +(s/def ::password-form + (s/keys :req-un [::password-1 + ::password-2 + ::password-old])) (mx/defc password-form {:mixins [mx/reactive mx/static]} [] (let [data (mx/react form-data) errors (mx/react form-errors) - valid? (forms/valid? data +password-form+)] + valid? (fm/valid? ::password-form data)] (letfn [(on-change [field event] (let [value (dom/event->value event)] - (set-value! field value))) + (st/emit! (assoc-value field value)))) + (on-success [] + (st/emit! (um/info (tr "settings.password-saved")))) + (on-error [{:keys [code] :as payload}] + (case code + :uxbox.services.users/old-password-not-match + (st/emit! (assoc-error :password-old "Wrong old password")) + + :else + (throw (ex-info "unexpected" {:error payload})))) (on-submit [event] - (println "on-submit" data) - #_(st/emit! (udu/update-password form)))] + (st/emit! (udu/update-password data + :on-success on-success + :on-error on-error)))] [:form.password-form [:span.user-settings-label "Change password"] [:input.input-text {:type "password" - :class (forms/error-class errors :old-password) - :value (:old-password data "") - :on-change (partial on-change :old-password) + :class (fm/error-class errors :password-old) + :value (:password-old data "") + :on-change (partial on-change :password-old) :placeholder "Old password"}] - (forms/input-error errors :old-password) + (fm/input-error errors :password-old) [:input.input-text {:type "password" - :class (forms/error-class errors :password-1) + :class (fm/error-class errors :password-1) :value (:password-1 data "") :on-change (partial on-change :password-1) :placeholder "New password"}] - (forms/input-error errors :password-1) + (fm/input-error errors :password-1) [:input.input-text {:type "password" - :class (forms/error-class errors :password-2) + :class (fm/error-class errors :password-2) :value (:password-2 data "") :on-change (partial on-change :password-2) :placeholder "Confirm password"}] - (forms/input-error errors :password-2) + (fm/input-error errors :password-2) [:input.btn-primary {:type "button" :class (when-not valid? "btn-disabled") diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index caffccf5b..f228dc8bd 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -6,7 +6,8 @@ ;; Copyright (c) 2016-2017 Juan de la Cruz (ns uxbox.main.ui.settings.profile - (:require [cuerdas.core :as str] + (:require [cljs.spec :as s :include-macros true] + [cuerdas.core :as str] [lentes.core :as l] [potok.core :as ptk] [uxbox.main.store :as st] @@ -14,52 +15,58 @@ [uxbox.main.ui.settings.header :refer [header]] [uxbox.main.ui.messages :refer [messages-widget]] [uxbox.main.data.users :as udu] - [uxbox.util.forms :as forms] + [uxbox.util.forms :as fm] [uxbox.util.router :as r] [uxbox.util.mixins :as mx :include-macros true] [uxbox.util.interop :refer [iterable->seq]] [uxbox.util.dom :as dom])) -(def form-data (forms/focus-data :profile st/state)) -(def form-errors (forms/focus-errors :profile st/state)) -(def set-value! (partial forms/set-value! st/store :profile)) -(def set-error! (partial forms/set-error! st/store :profile)) -(def clear! (partial forms/clear! st/store :profile)) +(def form-data (fm/focus-data :profile st/state)) +(def form-errors (fm/focus-errors :profile st/state)) + +(def assoc-value (partial fm/assoc-value :profile)) +(def assoc-error (partial fm/assoc-error :profile)) +(def clear-form (partial fm/clear-form :profile)) (def profile-ref (-> (l/key :profile) (l/derive st/state))) -(def +profile-form+ - {:fullname [forms/required forms/string] - :email [forms/required forms/email] - :username [forms/required forms/string]}) +(s/def ::fullname ::fm/non-empty-string) +(s/def ::username ::fm/non-empty-string) +(s/def ::email ::fm/email) + +(s/def ::profile-form + (s/keys :req-un [::fullname + ::username + ::email])) ;; --- Profile Form (mx/defc profile-form {:mixins [mx/static mx/reactive - (forms/clear-mixin st/store :profile)]} + (fm/clear-mixin st/store :profile)]} [] - ;; TODO: properly persist theme (let [data (merge {:theme "light"} (mx/react profile-ref) (mx/react form-data)) errors (mx/react form-errors) - valid? (forms/valid? data +profile-form+) + valid? (fm/valid? ::profile-form data) theme (:theme data)] (letfn [(on-change [field event] (let [value (dom/event->value event)] - (set-value! field value))) + (st/emit! (assoc-value field value)))) (on-error [{:keys [code] :as payload}] (case code :uxbox.services.users/email-already-exists - (set-error! :email "Email already exists") + (st/emit! (assoc-error :email "Email already exists")) :uxbox.services.users/username-already-exists - (set-error! :username "Username already exists"))) + (st/emit! (assoc-error :username "Username already exists")))) + (on-success [_] + (st/emit! (clear-form))) (on-submit [event] - (st/emit! (udu/update-profile data clear! on-error)))] + (st/emit! (udu/update-profile data on-success on-error)))] [:form.profile-form [:span.user-settings-label "Name, username and email"] [:input.input-text @@ -72,14 +79,14 @@ :on-change (partial on-change :username) :value (:username data "") :placeholder "Your username"}] - (forms/input-error errors :username) + (fm/input-error errors :username) - [:input.input-text - {:type "email" - :on-change (partial on-change :email) - :value (:email data "") - :placeholder "Your email"}] - (forms/input-error errors :email) + [:input.input-text + {:type "email" + :on-change (partial on-change :email) + :value (:email data "") + :placeholder "Your email"}] + (fm/input-error errors :email) #_[:span.user-settings-label "Choose a color theme"] #_[:div.input-radio.radio-primary diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs index 69cc41df3..d1c07dfaf 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs @@ -6,41 +6,49 @@ ;; Copyright (c) 2015-2016 Juan de la Cruz (ns uxbox.main.ui.workspace.sidebar.sitemap-pageform - (:require [lentes.core :as l] - [cuerdas.core :as str] - [potok.core :as ptk] + (:require [cljs.spec :as s :include-macros true] + [lentes.core :as l] + [uxbox.builtins.icons :as i] [uxbox.main.store :as st] [uxbox.main.constants :as c] [uxbox.main.data.pages :as udp] - [uxbox.main.data.workspace :as dw] [uxbox.main.data.lightbox :as udl] - [uxbox.builtins.icons :as i] [uxbox.main.ui.lightbox :as lbx] - [uxbox.util.i18n :refer (tr)] + [uxbox.util.data :refer [parse-int]] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] + [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as r] - [uxbox.util.forms :as forms] - [uxbox.util.mixins :as mx :include-macros true] - [uxbox.util.data :refer (deep-merge parse-int)] - [uxbox.util.dom :as dom])) + [uxbox.util.mixins :as mx :include-macros true])) -(def form-data (forms/focus-data :workspace-page-form st/state)) -(def set-value! (partial forms/set-value! st/store :workspace-page-form)) + +(def form-data (fm/focus-data :workspace-page-form st/state)) +(def form-errors (fm/focus-errors :workspace-page-form st/state)) + +(def assoc-value (partial fm/assoc-value :workspace-page-form)) +(def assoc-error (partial fm/assoc-error :workspace-page-form)) +(def clear-form (partial fm/clear-form :workspace-page-form)) ;; --- Lightbox -(def +page-form+ - {:name [forms/required forms/string] - :width [forms/required forms/number] - :height [forms/required forms/number] - :layout [forms/required forms/string]}) +(s/def ::name ::fm/non-empty-string) +(s/def ::layout ::fm/non-empty-string) +(s/def ::width number?) +(s/def ::height number?) + +(s/def ::page-form + (s/keys :req-un [::name + ::width + ::height + ::layout])) (mx/defc layout-input [data id] (let [{:keys [id name width height]} (get c/page-layouts id)] (letfn [(on-change [event] - (set-value! :layout id) - (set-value! :width width) - (set-value! :height height))] + (st/emit! (assoc-value :layout id) + (assoc-value :width width) + (assoc-value :height height)))] [:div [:input {:type "radio" :id id @@ -57,18 +65,18 @@ (select-keys page [:name :id :project]) (select-keys metadata [:width :height :layout]) (mx/react form-data)) - valid? (forms/valid? data +page-form+)] + valid? (fm/valid? ::page-form data)] (letfn [(update-size [field e] (let [value (dom/event->value e) value (parse-int value)] - (set-value! field value))) + (st/emit! (assoc-value field value)))) (update-name [e] (let [value (dom/event->value e)] - (set-value! :name value))) + (st/emit! (assoc-value :name value)))) (toggle-sizes [] (let [{:keys [width height]} data] - (set-value! :width height) - (set-value! :height width))) + (st/emit! (assoc-value :width width) + (assoc-value :height height)))) (on-cancel [e] (dom/prevent-default e) (udl/close!)) @@ -119,7 +127,7 @@ :type "button"}]]))) (mx/defc page-form-lightbox - {:mixins [mx/static (forms/clear-mixin st/store :workspace-page-form)]} + {:mixins [mx/static (fm/clear-mixin st/store :workspace-page-form)]} [{:keys [id] :as page}] (letfn [(on-cancel [event] (dom/prevent-default event) diff --git a/frontend/src/uxbox/util/forms.cljs b/frontend/src/uxbox/util/forms.cljs index 664e6cb79..5c3c91032 100644 --- a/frontend/src/uxbox/util/forms.cljs +++ b/frontend/src/uxbox/util/forms.cljs @@ -2,173 +2,155 @@ ;; 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 -;; Copyright (c) 2015-2016 Juan de la Cruz +;; Copyright (c) 2015-2017 Andrey Antukh (ns uxbox.util.forms - (:refer-clojure :exclude [keyword uuid vector boolean map set]) - (:require [struct.core :as f] + (:require [cljs.spec :as s :include-macros true] + [cuerdas.core :as str] [lentes.core :as l] [beicon.core :as rx] [potok.core :as ptk] [uxbox.util.mixins :as mx :include-macros true] - [uxbox.util.i18n :refer (tr)])) + [uxbox.util.i18n :refer [tr]])) -;; TODO: rewrite form stuff using cljs.spec +;; --- Form Validation Api -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Form Validation -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn- interpret-problem + [acc {:keys [path pred val via in] :as problem}] + (cond + (and (empty? path) + (= (first pred) 'contains?)) + (let [path (conj path (last pred))] + (update-in acc path assoc :missing)) -;; --- Form Validators + (and (seq path) + (= 1 (count path))) + (update-in acc path assoc :invalid) -(def required - (assoc f/required :message "errors.form.required")) - -(def string - (assoc f/string :message "errors.form.string")) - -(def number - (assoc f/number :message "errors.form.number")) - -(def integer - (assoc f/integer :message "errors.form.integer")) - -(def boolean - (assoc f/boolean :message "errors.form.bool")) - -(def identical-to - (assoc f/identical-to :message "errors.form.identical-to")) - -(def in-range f/in-range) -(def uuid f/uuid) -(def keyword f/keyword) -(def integer-str f/integer-str) -(def number-str f/number-str) -(def email f/email) -(def positive f/positive) - -(def max-len - {:message "errors.form.max-len" - :optional true - :validate (fn [v n] - (let [len (count v)] - (>= len v)))}) - -(def min-len - {:message "errors.form.min-len" - :optional true - :validate (fn [v n] - (>= (count v) n))}) - -(def color - {:message "errors.form.color" - :optional true - :validate #(not (nil? (re-find #"^#[0-9A-Fa-f]{6}$" %)))}) - -;; --- Public Validation Api + :else acc)) (defn validate - ([data schema] - (validate data schema nil)) - ([data schema opts] - (f/validate data schema opts))) - -(defn validate! - ([data schema] - (validate! data schema nil)) - ([data schema opts] - (let [[errors data] (validate data schema opts)] - (if errors - (throw (ex-info "Invalid data" errors)) - data)))) + [spec data] + (when-not (s/valid? spec data) + (let [report (s/explain-data spec data)] + (reduce interpret-problem {} (::s/problems report))))) (defn valid? - [data schema] - (let [[errors data] (validate data schema)] - (not errors))) + [spec data] + (s/valid? spec data)) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Form Events -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; --- Form Specs and Conformers -;; --- Set Error +(def ^:private email-re + #"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") -(defrecord SetError [type field error] +(def ^:private number-re + #"^[-+]?[0-9]*\.?[0-9]+$") + +(def ^:private color-re + #"^#[0-9A-Fa-f]{6}$") + +(s/def ::email + (s/and string? #(boolean (re-matches email-re %)))) + +(s/def ::non-empty-string + (s/and string? #(not (str/empty? %)))) + +(defn- parse-number + [v] + (cond + (re-matches number-re v) (js/parseFloat v) + (number? v) v + :else ::s/invalid)) + +(s/def ::string-number + (s/conformer parse-number str)) + +(s/def ::color + (s/and string? #(boolean (re-matches color-re %)))) + +;; --- Form State Events + +;; --- Assoc Error + +(defrecord AssocError [type field error] ptk/UpdateEvent (update [_ state] (assoc-in state [:errors type field] error))) -(defn set-error +(defn assoc-error ([type field] - (set-error type field nil)) + (assoc-error type field nil)) ([type field error] {:pre [(keyword? type) (keyword? field) (any? error)]} - (SetError. type field error))) + (AssocError. type field error))) -(defn set-error! - [store & args] - (ptk/emit! store (apply set-error args))) +;; --- Assoc Errors -;; --- Set Errors - -(defrecord SetErrors [type errors] +(defrecord AssocErrors [type errors] ptk/UpdateEvent (update [_ state] (assoc-in state [:errors type] errors))) -(defn set-errors +(defn assoc-errors ([type] - (set-errors type nil)) + (assoc-errors type nil)) ([type errors] {:pre [(keyword? type) (or (map? errors) (nil? errors))]} - (SetErrors. type errors))) + (AssocErrors. type errors))) -(defn set-errors! - [store & args] - (ptk/emit! store (apply set-errors args))) +;; --- Assoc Value -;; --- Set Value +(declare clear-error) -(defrecord SetValue [type field value] +(defrecord AssocValue [type field value] ptk/UpdateEvent (update [_ state] - (let [form-path (into [:forms type] (if (coll? field) field [field])) - errors-path (into [:errors type] (if (coll? field) field [field]))] - (-> state - (assoc-in form-path value) - (update-in (butlast errors-path) dissoc (last errors-path)))))) + (let [form-path (into [:forms type] (if (coll? field) field [field]))] + (assoc-in state form-path value))) -(defn set-value + ptk/WatchEvent + (watch [_ state stream] + (rx/of (clear-error type field)))) + +(defn assoc-value [type field value] {:pre [(keyword? type) (keyword? field) (any? value)]} - (SetValue. type field value)) + (AssocValue. type field value)) -(defn set-value! - [store type field value] - (ptk/emit! store (set-value type field value))) +;; --- Clear Values -;; --- Clear Form - -(defrecord ClearForm [type] +(defrecord ClearValues [type] ptk/UpdateEvent (update [_ state] (assoc-in state [:forms type] nil))) -(defn clear-form +(defn clear-values [type] {:pre [(keyword? type)]} - (ClearForm. type)) + (ClearValues. type)) -(defn clear-form! - [store type] - (ptk/emit! store (clear-form type))) +;; --- Clear Error + +(deftype ClearError [type field] + ptk/UpdateEvent + (update [_ state] + (let [errors (get-in state [:errors type])] + (if (map? errors) + (assoc-in state [:errors type] (dissoc errors field)) + (update state :errors dissoc type))))) + +(defn clear-error + [type field] + {:pre [(keyword? type) + (keyword? field)]} + (ClearError. type field)) ;; --- Clear Errors @@ -182,17 +164,18 @@ {:pre [(keyword? type)]} (ClearErrors. type)) -(defn clear-errors! - [store type] - (ptk/emit! store (clear-errors type))) +;; --- Clear Form -;; --- Clear +(deftype ClearForm [type] + ptk/WatchEvent + (watch [_ state stream] + (rx/of (clear-values type) + (clear-errors type)))) -(defn clear! - [store type] - (ptk/emit! store - (clear-form type) - (clear-errors type))) +(defn clear-form + [type] + {:pre [(keyword? type)]} + (ClearForm. type)) ;; --- Helpers @@ -224,5 +207,5 @@ (defn clear-mixin [store type] {:will-unmount (fn [own] - (clear! store type) + (ptk/emit! store (clear-form type)) own)})