From fce36cfdd929fc5fa3bac58883b829138ea6fc8d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sun, 13 Nov 2016 15:42:44 +0100 Subject: [PATCH] Introduce cljs.spec and refactor all forms. --- src/uxbox/main/data/auth.cljs | 75 ++--- src/uxbox/main/data/dashboard.cljs | 2 +- src/uxbox/main/data/forms.cljs | 89 ------ src/uxbox/main/data/history.cljs | 6 +- src/uxbox/main/data/pages.cljs | 230 +++++++++------ src/uxbox/main/data/projects.cljs | 4 +- src/uxbox/main/data/shapes.cljs | 4 +- src/uxbox/main/data/users.cljs | 79 +++--- src/uxbox/main/data/workspace.cljs | 51 ++-- src/uxbox/main/repo/pages.cljs | 48 +--- src/uxbox/main/repo/projects.cljs | 24 +- src/uxbox/main/repo/users.cljs | 17 +- src/uxbox/main/ui.cljs | 3 +- src/uxbox/main/ui/auth/login.cljs | 125 ++++----- src/uxbox/main/ui/auth/recovery.cljs | 119 +++----- src/uxbox/main/ui/auth/recovery_request.cljs | 115 +++----- src/uxbox/main/ui/auth/register.cljs | 168 ++++++----- src/uxbox/main/ui/colorpicker.cljs | 2 +- src/uxbox/main/ui/dashboard/colors.cljs | 4 +- src/uxbox/main/ui/dashboard/icons.cljs | 2 +- src/uxbox/main/ui/forms.cljs | 16 -- src/uxbox/main/ui/settings/password.cljs | 149 +++++----- src/uxbox/main/ui/settings/profile.cljs | 171 +++++------- src/uxbox/main/ui/workspace/base.cljs | 4 +- src/uxbox/main/ui/workspace/grid.cljs | 6 +- src/uxbox/main/ui/workspace/settings.cljs | 163 ++++++----- .../main/ui/workspace/sidebar/sitemap.cljs | 103 +------ .../workspace/sidebar/sitemap_pageform.cljs | 145 ++++++++++ src/uxbox/util/forms.cljs | 264 ++++++++++++++++++ src/uxbox/util/router.cljs | 5 +- src/uxbox/util/schema.cljs | 85 ------ src/uxbox/util/spec.cljs | 47 ++++ src/uxbox/view/data/viewer.cljs | 2 +- src/uxbox/worker/align.cljs | 4 +- 34 files changed, 1187 insertions(+), 1144 deletions(-) delete mode 100644 src/uxbox/main/data/forms.cljs delete mode 100644 src/uxbox/main/ui/forms.cljs create mode 100644 src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs create mode 100644 src/uxbox/util/forms.cljs delete mode 100644 src/uxbox/util/schema.cljs create mode 100644 src/uxbox/util/spec.cljs diff --git a/src/uxbox/main/data/auth.cljs b/src/uxbox/main/data/auth.cljs index 814d77e4f..a6bd9a482 100644 --- a/src/uxbox/main/data/auth.cljs +++ b/src/uxbox/main/data/auth.cljs @@ -6,19 +6,25 @@ ;; Copyright (c) 2015-2016 Juan de la Cruz (ns uxbox.main.data.auth - (:require [beicon.core :as rx] + (:require [cljs.spec :as s] + [beicon.core :as rx] [uxbox.main.repo :as rp] [uxbox.util.rstore :as rs] [uxbox.util.router :as rt] - [uxbox.main.state :as st] - [uxbox.util.schema :as us] + [uxbox.util.spec :as us] [uxbox.util.i18n :refer (tr)] + [uxbox.main.state :as st] [uxbox.main.data.projects :as udp] [uxbox.main.data.users :as udu] [uxbox.main.data.messages :as udm] - [uxbox.main.data.forms :as udf] [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 (defrecord LoggedIn [data] @@ -58,8 +64,12 @@ (rx/map logged-in) (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 @@ -80,40 +90,33 @@ ;; --- Register -(defrecord Register [data] +;; TODO: clean form on success + +(defrecord Register [data on-error] rs/WatchEvent (-apply-watch [_ state stream] - (letfn [(on-error [{payload :payload}] - (->> (:payload payload) - (udf/assign-errors :register) - (rx/of)))] + (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? on-error)) + (rx/catch rp/client-error? handle-error)) (->> stream (rx/filter #(= % ::registered)) (rx/take 1) - (rx/map #(login data))) - (->> stream - (rx/filter logged-in?) - (rx/take 1) - (rx/map #(udf/clean :register))))))) + (rx/map #(login data))))))) -(def register-schema - {:username [us/required us/string] - :fullname [us/required us/string] - :email [us/required us/email] - :password [us/required us/string]}) +(s/def ::register-event + (s/keys :req-un [::fullname ::username ::email ::password])) (defn register "Create a register event instance." - [data] - (let [[errors data] (us/validate data register-schema)] - (if errors - (udf/assign-errors :register errors) - (Register. data)))) + [data on-error] + {:pre [(us/valid? ::register-event data) + (fn? on-error)]} + (Register. data on-error)) ;; --- Recovery Request @@ -122,9 +125,7 @@ (-apply-watch [_ state stream] (letfn [(on-error [{payload :payload}] (println "on-error" payload) - (->> (:payload payload) - (udf/assign-errors :recovery-request) - (rx/of)))] + (rx/empty))] (rx/merge (->> (rp/req :auth/recovery-request data) (rx/map (constantly ::recovery-requested)) @@ -132,11 +133,14 @@ (->> stream (rx/filter #(= % ::recovery-requested)) (rx/take 1) - (rx/do #(udm/info! (tr "auth.message.recovery-token-sent"))) - (rx/map #(udf/clean :recovery-request))))))) + (rx/do #(udm/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 @@ -153,8 +157,9 @@ (rx/catch rp/client-error? on-error))))) (defn validate-recovery-token - [data] - (ValidateRecoveryToken. data)) + [token] + {:pre [(string? token)]} + (ValidateRecoveryToken. token)) ;; --- Recovery (Password) @@ -171,8 +176,10 @@ (rx/mapcat on-success) (rx/catch rp/client-error? on-error))))) +(s/def ::recovery-event + (s/keys :req-un [::username ::token])) (defn recovery - [{:keys [token password]}] + [{:keys [token password] :as data}] + {:pre [(us/valid? ::recovery-event data)]} (Recovery. token password)) - diff --git a/src/uxbox/main/data/dashboard.cljs b/src/uxbox/main/data/dashboard.cljs index 967782339..c60f68f17 100644 --- a/src/uxbox/main/data/dashboard.cljs +++ b/src/uxbox/main/data/dashboard.cljs @@ -11,7 +11,7 @@ [uxbox.util.rstore :as rs] [uxbox.util.router :as r] [uxbox.main.state :as st] - [uxbox.util.schema :as sc] + [uxbox.util.forms :as sc] [uxbox.main.repo :as rp] [uxbox.main.data.projects :as dp] [uxbox.main.data.colors :as dc] diff --git a/src/uxbox/main/data/forms.cljs b/src/uxbox/main/data/forms.cljs deleted file mode 100644 index dd0b8616b..000000000 --- a/src/uxbox/main/data/forms.cljs +++ /dev/null @@ -1,89 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2016 Andrey Antukh - -(ns uxbox.main.data.forms - (:require [beicon.core :as rx] - [lentes.core :as l] - [uxbox.main.repo :as rp] - [uxbox.util.rstore :as rs] - [uxbox.main.state :as st] - [uxbox.util.schema :as sc] - [uxbox.util.i18n :refer (tr)])) - -;; --- Assign Errors - -(defrecord AssignErrors [type errors] - rs/UpdateEvent - (-apply-update [_ state] - (assoc-in state [:errors type] errors))) - -(defn assign-errors - ([type] (assign-errors type nil)) - ([type errors] - (AssignErrors. type errors))) - -;; --- Assign Field Value - -(defrecord AssignFieldValue [type field value] - rs/UpdateEvent - (-apply-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)))))) - -(defn assign-field-value - [type field value] - (AssignFieldValue. type field value)) - -;; --- Clean Errors - -(defrecord CleanErrors [type] - rs/UpdateEvent - (-apply-update [_ state] - (assoc-in state [:errors type] nil))) - -(defn clean-errors - [type] - (CleanErrors. type)) - -;; --- Clean Form - -(defrecord CleanForm [type] - rs/UpdateEvent - (-apply-update [_ state] - (assoc-in state [:forms type] nil))) - -(defn clean-form - [type] - (CleanForm. type)) - -;; --- Clean - -(defrecord Clean [type] - rs/WatchEvent - (-apply-watch [_ state s] - (rx/of (clean-form type) - (clean-errors type)))) - -(defn clean - [type] - (Clean. type)) - -;; --- Helpers - -(defn focus-form-data - [type] - (-> (l/in [:forms type]) - (l/derive st/state))) - -(defn focus-form-errors - [type] - (-> (l/in [:errors type]) - (l/derive st/state))) - - diff --git a/src/uxbox/main/data/history.cljs b/src/uxbox/main/data/history.cljs index 311e3fa07..7a79418fb 100644 --- a/src/uxbox/main/data/history.cljs +++ b/src/uxbox/main/data/history.cljs @@ -12,7 +12,7 @@ [uxbox.util.router :as r] [uxbox.main.repo :as rp] [uxbox.util.i18n :refer (tr)] - [uxbox.util.schema :as sc] + [uxbox.util.forms :as sc] [uxbox.main.data.pages :as udp] [uxbox.main.state :as st] [uxbox.util.datetime :as dt] @@ -35,7 +35,7 @@ (rs/emit! (fetch-page-history id) (fetch-pinned-page-history id)))] (as-> rs/stream $ - (rx/filter udp/page-synced? $) + (rx/filter udp/page-persisted? $) (rx/delay 500 $) (rx/map (comp :id :page) $) (rx/on-value $ on-value)))) @@ -137,7 +137,7 @@ rs/WatchEvent (-apply-watch [_ state s] - (rx/of (udp/update-page id)))) + (rx/of (udp/persist-page id)))) (defn apply-selected-history [id] diff --git a/src/uxbox/main/data/pages.cljs b/src/uxbox/main/data/pages.cljs index efdb095fa..845512be6 100644 --- a/src/uxbox/main/data/pages.cljs +++ b/src/uxbox/main/data/pages.cljs @@ -3,21 +3,43 @@ ;; 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 (ns uxbox.main.data.pages - (:require [cuerdas.core :as str] + (:require [cljs.spec :as s] + [cuerdas.core :as str] [beicon.core :as rx] [lentes.core :as l] + [uxbox.main.repo :as rp] + [uxbox.main.state :as st] + [uxbox.util.spec :as us] [uxbox.util.rstore :as rs] [uxbox.util.router :as r] - [uxbox.main.repo :as rp] [uxbox.util.i18n :refer (tr)] - [uxbox.util.schema :as sc] - [uxbox.main.state :as st] + [uxbox.util.forms :as sc] [uxbox.util.datetime :as dt] [uxbox.util.data :refer (without-keys replace-by-id)])) +;; --- Specs + +(s/def ::id uuid?) +(s/def ::name string?) +(s/def ::project uuid?) +(s/def ::grid-x-axis number?) +(s/def ::grid-y-axis number?) +(s/def ::grid-color us/color?) +(s/def ::grid-alignment boolean?) +(s/def ::width number?) +(s/def ::height number?) +(s/def ::layout string?) + +(s/def ::metadata + (s/keys :req-un [::width ::height] + :opt-un [::grid-y-axis + ::grid-x-axis + ::grid-color + ::grid-alignment + ::layout])) + ;; --- Protocols @@ -99,75 +121,84 @@ [projectid] (FetchPages. projectid)) +;; --- Page Created + +(defrecord PageCreated [data] + rs/UpdateEvent + (-apply-update [_ state] + (-> state + (assoc-page data) + (assoc-packed-page data)))) + +(s/def ::page-created-event + (s/keys :req-un [::id ::name ::project ::metadata])) + +(defn page-created + [data] + {:pre [(us/valid? ::page-created-event data)]} + (PageCreated. data)) + ;; --- Create Page -(defrecord CreatePage [name project metadata] +(defrecord CreatePage [name project width height layout] rs/WatchEvent (-apply-watch [this state s] - (letfn [(on-created [{page :payload}] - (rx/of - #(assoc-page % page) - #(assoc-packed-page % page)))] - (let [params {:name name - :project project - :data {} - :metadata metadata}] - (->> (rp/req :create/page params) - (rx/mapcat on-created)))))) + (let [params {:name name + :project project + :data {} + :metadata {:width width + :height height + :layout layout}}] + (->> (rp/req :create/page params) + (rx/map :payload) + (rx/map page-created))))) -(def ^:private create-page-schema - {:name [sc/required sc/string] - :metadata [sc/required] - :project [sc/required sc/uuid]}) +(s/def ::create-page-event + (s/keys :req-un [::name ::project ::width ::height ::layout])) (defn create-page [data] - (-> (sc/validate! data create-page-schema) - (map->CreatePage))) + {:pre [(us/valid? ::create-page-event data)]} + (map->CreatePage data)) -;; --- Page Synced +;; --- Page Persisted -(defrecord PageSynced [page] +(defrecord PagePersisted [data] rs/UpdateEvent - (-apply-update [this state] - (-> state - (assoc-in [:pages (:id page) :version] (:version page)) - (assoc-page page)))) + (-apply-update [_ state] + (assoc-page state data))) -(defn- page-synced? +(defn- page-persisted? [event] - (instance? PageSynced event)) + (instance? PagePersisted event)) -;; --- Sync Page +;; TODO: add specs -(defrecord SyncPage [id] - rs/WatchEvent - (-apply-watch [this state s] - (let [page (pack-page state id)] - (->> (rp/req :update/page page) - (rx/map (comp ->PageSynced :payload)))))) +(defn page-persisted + [data] + {:pre [(map? data)]} + (PagePersisted. data)) -(defn sync-page - [id] - (SyncPage. id)) +;; --- Persist Page -;; --- Update Page - -(defrecord UpdatePage [id] +(defrecord PersistPage [id] rs/WatchEvent (-apply-watch [this state s] (let [page (get-in state [:pages id])] (if (:history page) (rx/empty) - (rx/of (sync-page id)))))) + (let [page (pack-page state id)] + (->> (rp/req :update/page page) + (rx/map :payload) + (rx/map page-persisted))))))) -(defn update-page? +(defn persist-page? [v] - (instance? UpdatePage v)) + (instance? PersistPage v)) -(defn update-page +(defn persist-page [id] - (UpdatePage. id)) + (PersistPage. id)) (defn watch-page-changes "A function that starts watching for `IPageUpdate` @@ -182,59 +213,82 @@ (as-> rs/stream $ (rx/filter #(satisfies? IPageUpdate %) $) (rx/debounce 1000 $) - (rx/on-next $ #(rs/emit! (update-page id))))) + (rx/on-next $ #(rs/emit! (persist-page id))))) -;; --- Update Page Metadata +;; --- Page Metadata Persisted -;; This is a simplified version of `UpdatePage` event +(defrecord MetadataPersisted [id data] + rs/UpdateEvent + (-apply-update [_ state] + ;; TODO: page-data update + (assoc-in state [:pages id :version] (:version data)))) + +(s/def ::metadata-persisted-event + (s/keys :req-un [::id ::version])) + +(defn metadata-persisted + [{:keys [id] :as data}] + {:pre [(us/valid? ::metadata-persisted-event data)]} + (MetadataPersisted. id data)) + +;; --- Persist Page Metadata + +;; This is a simplified version of `PersistPage` event ;; that does not sends the heavyweiht `:data` attribute ;; and only serves for update other page data. -;; TODO: sync also with the pagedata-by-id index. - -(defrecord UpdatePageMetadata [id name width height layout options] - rs/UpdateEvent - (-apply-update [_ state] - (letfn [(updater [page] - (merge page - (when options {:options options}) - (when width {:width width}) - (when height {:height height}) - (when name {:name name})))] - (update-in state [:pages id] updater))) - +(defrecord PersistMetadata [id] rs/WatchEvent - (-apply-watch [this state s] - (letfn [(on-success [{page :payload}] - #(assoc-in % [:pages id :version] (:version page)))] - (->> (rp/req :update/page-metadata (into {} this)) - (rx/map on-success))))) + (-apply-watch [_ state stream] + (let [page (get-in state [:pages id])] + (->> (rp/req :update/page-metadata page) + (rx/map :payload) + (rx/map metadata-persisted))))) -(def ^:private update-page-schema - {:id [sc/required] - :project [sc/required] - :version [sc/required] - :name [sc/required sc/string] - :metadata [sc/required]}) - -(defn update-page-metadata - [data] - (-> (sc/validate! data update-page-schema {:strip false}) - (dissoc data :data) - (map->UpdatePageMetadata))) +(defn persist-metadata + [id] + {:pre [(uuid? id)]} + (PersistMetadata. id)) ;; --- Update Page Options -(defrecord UpdatePageOptions [id options] +(defrecord UpdateMetadata [id metadata] + rs/UpdateEvent + (-apply-update [_ state] + (assoc-in state [:pages id :metadata] metadata)) + rs/WatchEvent (-apply-watch [this state s] - (let [page (get-in state [:pages id]) - page (assoc page :options options)] - (rx/of (map->UpdatePageMetadata page))))) + (rx/of (persist-metadata id)))) -(defn update-page-options - [id options] - (UpdatePageOptions. id options)) +(defn update-metadata + [id metadata] + {:pre [(uuid? id) (us/valid? ::metadata metadata)]} + (UpdateMetadata. id metadata)) + +;; --- Update Page + +(defrecord UpdatePage [id name width height layout] + rs/UpdateEvent + (-apply-update [this state] + (println "update-page" this) + (-> state + (assoc-in [:pages id :name] name) + (assoc-in [:pages id :metadata :width] width) + (assoc-in [:pages id :metadata :height] height) + (assoc-in [:pages id :metadata :layout] layout))) + + rs/WatchEvent + (-apply-watch [_ state stream] + (rx/of (persist-metadata id)))) + +(s/def ::update-page-event + (s/keys :req-un [::name ::width ::height ::layout])) + +(defn update-page + [id {:keys [name width height layout] :as data}] + {:pre [(uuid? id) (us/valid? ::update-page-event data)]} + (UpdatePage. id name width height layout)) ;; --- Delete Page (by id) diff --git a/src/uxbox/main/data/projects.cljs b/src/uxbox/main/data/projects.cljs index f72e442ad..eb6d9a3ac 100644 --- a/src/uxbox/main/data/projects.cljs +++ b/src/uxbox/main/data/projects.cljs @@ -14,7 +14,7 @@ [uxbox.util.rstore :as rs] [uxbox.util.router :as r] [uxbox.util.i18n :refer (tr)] - [uxbox.util.schema :as sc] + [uxbox.util.forms :as sc] [uxbox.main.data.pages :as udp])) ;; --- Helpers @@ -25,6 +25,7 @@ (let [page {:id (:page-id project) :name (:page-name project) :version (:page-version project) + :project (:id project) :data (:page-data project) :created-at (:page-created-at project) :modified-at (:page-modified-at project) @@ -176,7 +177,6 @@ :width [sc/required sc/integer] :height [sc/required sc/integer] :layout [sc/required sc/string]}) - (defn create-project [params] (-> (sc/validate! params create-project-schema) diff --git a/src/uxbox/main/data/shapes.cljs b/src/uxbox/main/data/shapes.cljs index 56eca9200..3c59d2668 100644 --- a/src/uxbox/main/data/shapes.cljs +++ b/src/uxbox/main/data/shapes.cljs @@ -10,7 +10,7 @@ [uxbox.util.uuid :as uuid] [uxbox.util.rstore :as rs] [uxbox.util.router :as r] - [uxbox.util.schema :as sc] + [uxbox.util.forms :as sc] [uxbox.util.workers :as uw] [uxbox.main.constants :as c] [uxbox.main.geom :as geom] @@ -594,6 +594,6 @@ (defn align-point [point] - (let [message {:cmd :grid/align :point point}] + (let [message {:cmd :grid-align :point point}] (->> (uw/ask! worker message) (rx/map :point)))) diff --git a/src/uxbox/main/data/users.cljs b/src/uxbox/main/data/users.cljs index d12bb3558..c925b6838 100644 --- a/src/uxbox/main/data/users.cljs +++ b/src/uxbox/main/data/users.cljs @@ -5,15 +5,20 @@ ;; Copyright (c) 2016 Andrey Antukh (ns uxbox.main.data.users - (:require [beicon.core :as rx] - [uxbox.main.repo :as rp] + (:require [cljs.spec :as s] + [beicon.core :as rx] [uxbox.util.rstore :as rs] - [uxbox.main.state :as st] - [uxbox.util.schema :as sc] + [uxbox.util.spec :as us] [uxbox.util.i18n :refer (tr)] - [uxbox.main.data.forms :as udf] + [uxbox.main.state :as st] + [uxbox.main.repo :as rp] [uxbox.main.data.messages :as udm])) +(s/def ::fullname string?) +(s/def ::email us/email?) +(s/def ::username string?) +(s/def ::theme string?) + ;; --- Profile Fetched (defrecord ProfileFetched [data] @@ -55,37 +60,31 @@ ;; --- Update Profile -(defrecord UpdateProfile [data] +(defrecord UpdateProfile [data on-success on-error] rs/WatchEvent (-apply-watch [_ state s] - (letfn [(on-error [{payload :payload}] - (->> (:payload payload) - (udf/assign-errors :profile/main) - (rx/of)))] + (letfn [(handle-error [{payload :payload}] + (on-error payload) + (rx/empty))] (->> (rp/req :update/profile data) (rx/map :payload) + (rx/do on-success) (rx/map profile-updated) - (rx/catch rp/client-error? on-error))))) + (rx/catch rp/client-error? handle-error))))) -(def update-profile-schema - {:fullname [sc/required sc/string] - :email [sc/required sc/email] - :username [sc/required sc/string]}) +(s/def ::update-profile-event + (s/keys :req-un [::fullname ::email ::username ::theme])) (defn update-profile - [data] - (let [[errors data] (sc/validate data update-profile-schema)] - (if errors - (udf/assign-errors :profile/main errors) - (UpdateProfile. data)))) + [data on-success on-error] + {:pre [(us/valid? ::update-profile-event data) + (fn? on-error) + (fn? on-success)]} + (UpdateProfile. data on-success on-error)) ;; --- Password Updated (defrecord PasswordUpdated [] - rs/UpdateEvent - (-apply-update [_ state] - (assoc-in state [:forms :profile/password] {})) - rs/EffectEvent (-apply-effect [_ state] (udm/info! (tr "settings.password-saved")))) @@ -99,28 +98,22 @@ (defrecord UpdatePassword [data] rs/WatchEvent (-apply-watch [_ state s] - (letfn [(on-error [{payload :payload}] - (->> (:payload payload) - (udf/assign-errors :profile/password) - (rx/of)))] - (let [params {:old-password (:old-password data) - :password (:password-1 data)}] + (let [params {:old-password (:old-password data) + :password (:password-1 data)}] (->> (rp/req :update/profile-password params) - (rx/map password-updated) - (rx/catch rp/client-error? on-error)))))) + (rx/map password-updated))))) -(def update-password-schema - [[:password-1 sc/required sc/string [sc/min-len 6]] - [:password-2 sc/required sc/string - [sc/identical-to :password-1 :message "errors.form.password-not-match"]] - [:old-password sc/required sc/string]]) +(s/def ::password-1 string?) +(s/def ::password-2 string?) +(s/def ::old-password string?) + +(s/def ::update-password-event + (s/keys :req-un [::password-1 ::password-2 ::old-password])) (defn update-password [data] - (let [[errors data] (sc/validate data update-password-schema)] - (if errors - (udf/assign-errors :profile/password errors) - (UpdatePassword. data)))) + {:pre [(us/valid? ::update-password-event data)]} + (UpdatePassword. data)) ;; --- Update Photo @@ -132,7 +125,7 @@ (rx/map fetch-profile)))) (defn update-photo - ([file] - (UpdatePhoto. file (constantly nil))) + ([file] (update-photo file (constantly nil))) ([file done] + {:pre [(us/file? file) (fn? done)]} (UpdatePhoto. file done))) diff --git a/src/uxbox/main/data/workspace.cljs b/src/uxbox/main/data/workspace.cljs index 3507ebe67..af9d7b85c 100644 --- a/src/uxbox/main/data/workspace.cljs +++ b/src/uxbox/main/data/workspace.cljs @@ -5,11 +5,13 @@ ;; Copyright (c) 2015-2016 Andrey Antukh (ns uxbox.main.data.workspace - (:require [beicon.core :as rx] + (:require [cljs.spec :as s] + [beicon.core :as rx] [uxbox.util.uuid :as uuid] [uxbox.main.constants :as c] [uxbox.util.rstore :as rs] - [uxbox.util.schema :as sc] + [uxbox.util.spec :as us] + [uxbox.util.forms :as sc] [uxbox.util.geom.point :as gpt] [uxbox.util.workers :as uw] [uxbox.main.state :as st] @@ -18,7 +20,6 @@ [uxbox.main.data.pages :as udp] [uxbox.main.data.shapes :as uds] [uxbox.main.data.shapes-impl :as shimpl] - [uxbox.main.data.forms :as udf] [uxbox.main.data.lightbox :as udl] [uxbox.main.data.history :as udh] [uxbox.util.datetime :as dt] @@ -210,44 +211,32 @@ (-apply-watch [_ state s] (let [page (get-in state [:pages id]) opts (:options page) - message {:cmd :grid/init + message {:cmd :grid-init :width c/viewport-width :height c/viewport-height - :x-axis (:grid/x-axis opts c/grid-x-axis) - :y-axis (:grid/y-axis opts c/grid-y-axis)}] + :x-axis (:grid-x-axis opts c/grid-x-axis) + :y-axis (:grid-y-axis opts c/grid-y-axis)}] (rx/merge (->> (uw/send! worker message) - (rx/map #(activate-flag :grid/indexed))) - (when (:grid/alignment opts) - (rx/of (activate-flag :grid/alignment))))))) + (rx/map #(activate-flag :grid-indexed))) + (when (:grid-alignment opts) + (rx/of (activate-flag :grid-alignment))))))) (defn initialize-alignment-index [id] (InitializeAlignmentIndex. id)) -;; --- Update Workspace Settings (Form) +;; --- Update Metadata -(defrecord SubmitWorkspaceSettings [id options] +;; Is a workspace aware wrapper over uxbox.data.pages/UpdateMetadata event. + +(defrecord UpdateMetadata [id metadata] rs/WatchEvent (-apply-watch [_ state s] - (rx/of (udp/update-page-options id options) - (initialize-alignment-index id) - (udf/clean :workspace/settings))) + (rx/of (udp/update-metadata id metadata) + (initialize-alignment-index id)))) - rs/EffectEvent - (-apply-effect [_ state] - (udl/close!))) - -(def submit-workspace-settings-schema - {:grid/y-axis [sc/required sc/integer [sc/in-range 2 100]] - :grid/x-axis [sc/required sc/integer [sc/in-range 2 100]] - :grid/alignment [sc/boolean] - :grid/color [sc/required sc/color]}) - -(defn submit-workspace-settings - [id data] - (let [schema submit-workspace-settings-schema - [errors data] (sc/validate data schema)] - (if errors - (udf/assign-errors :workspace/settings errors) - (SubmitWorkspaceSettings. id data)))) +(defn update-metadata + [id metadata] + {:pre [(uuid? id) (us/valid? ::udp/metadata metadata)]} + (UpdateMetadata. id metadata)) diff --git a/src/uxbox/main/repo/pages.cljs b/src/uxbox/main/repo/pages.cljs index eda61a444..05af57a12 100644 --- a/src/uxbox/main/repo/pages.cljs +++ b/src/uxbox/main/repo/pages.cljs @@ -11,38 +11,23 @@ [uxbox.main.repo.impl :refer (request send!)] [uxbox.util.transit :as t])) -(defn decode-page - [{:keys [data metadata] :as page}] - (merge page - (when data {:data (t/decode data)}) - (when metadata {:metadata (t/decode metadata)}))) - -(defn decode-payload - [{:keys [payload] :as rsp}] - (if (sequential? payload) - (assoc rsp :payload (mapv decode-page payload)) - (assoc rsp :payload (decode-page payload)))) - (defmethod request :fetch/pages [type data] (let [params {:url (str url "/pages") :method :get}] - (->> (send! params) - (rx/map decode-payload)))) + (send! params))) (defmethod request :fetch/pages-by-project [type {:keys [project] :as params}] (let [url (str url "/projects/" project "/pages")] - (->> (send! {:method :get :url url}) - (rx/map decode-payload)))) + (send! {:method :get :url url}))) (defmethod request :fetch/page-history [type {:keys [page] :as params}] (let [url (str url "/pages/" page "/history") query (select-keys params [:max :since :pinned]) params {:method :get :url url :query query}] - (->> (send! params) - (rx/map decode-payload)))) + (send! params))) (defmethod request :delete/page [_ id] @@ -51,41 +36,30 @@ :method :delete}))) (defmethod request :create/page - [type {:keys [data metadata] :as body}] - (let [body (assoc body - :data (t/encode data) - :metadata (t/encode metadata)) - params {:url (str url "/pages") + [type body] + (let [params {:url (str url "/pages") :method :post :body body}] - (->> (send! params) - (rx/map decode-payload)))) + (send! params))) (defmethod request :update/page - [type {:keys [id data metadata] :as body}] - (let [body (assoc body - :data (t/encode data) - :metadata (t/encode metadata)) - params {:url (str url "/pages/" id) + [type {:keys [id] :as body}] + (let [params {:url (str url "/pages/" id) :method :put :body body}] - (->> (send! params) - (rx/map decode-payload)))) + (send! params))) (defmethod request :update/page-history [type {:keys [id page] :as data}] (let [params {:url (str url "/pages/" page "/history/" id) :method :put :body data}] - (->> (send! params) - (rx/map decode-payload)))) + (send! params))) (defmethod request :update/page-metadata [type {:keys [id metadata] :as body}] (let [body (dissoc body :data) - body (assoc body :metadata (t/encode metadata)) params {:url (str url "/pages/" id "/metadata") :method :put :body body}] - (->> (send! params) - (rx/map decode-payload)))) + (send! params))) diff --git a/src/uxbox/main/repo/projects.cljs b/src/uxbox/main/repo/projects.cljs index 7e94db8b3..1353c5214 100644 --- a/src/uxbox/main/repo/projects.cljs +++ b/src/uxbox/main/repo/projects.cljs @@ -14,27 +14,15 @@ (defmethod request :fetch/projects [type data] - (letfn [(decode-payload [{:keys [payload] :as response}] - (assoc response :payload (mapv decode-page payload))) - (decode-page [{:keys [page-metadata page-data] :as project}] - (assoc project - :page-metadata (t/decode page-metadata) - :page-data (t/decode page-data)))] - ;; Obtain the list of projects and decode the embedded - ;; page data in order to have it usable. - (->> (send! {:url (str url "/projects") - :method :get}) - (rx/map decode-payload)))) + ;; Obtain the list of projects and decode the embedded + ;; page data in order to have it usable. + (send! {:url (str url "/projects") + :method :get})) (defmethod request :fetch/project-by-token [_ token] - (letfn [(decode-pages [response] - (let [pages (->> (get-in response [:payload :pages]) - (mapv pages/decode-page))] - (assoc-in response [:payload :pages] pages)))] - (->> (send! {:url (str url "/projects-by-token/" token) - :method :get}) - (rx/map decode-pages)))) + (send! {:url (str url "/projects-by-token/" token) + :method :get})) (defmethod request :create/project [_ data] diff --git a/src/uxbox/main/repo/users.cljs b/src/uxbox/main/repo/users.cljs index 48929156d..c4b2e2feb 100644 --- a/src/uxbox/main/repo/users.cljs +++ b/src/uxbox/main/repo/users.cljs @@ -11,27 +11,18 @@ [uxbox.main.repo.impl :refer (request send!)] [uxbox.util.transit :as t])) -(defn- decode-payload - [{:keys [payload] :as rsp}] - (let [metadata (:metadata payload)] - (assoc rsp :payload - (assoc payload :metadata (t/decode metadata))))) - (defmethod request :fetch/profile [type _] (let [url (str url "/profile/me") params {:method :get :url url}] - (->> (send! params) - (rx/map decode-payload)))) + (send! params))) (defmethod request :update/profile - [type {:keys [metadata] :as body}] - (let [body (assoc body :metadata (t/encode metadata)) - params {:url (str url "/profile/me") + [type body] + (let [params {:url (str url "/profile/me") :method :put :body body}] - (->> (send! params) - (rx/map decode-payload)))) + (send! params))) (defmethod request :update/profile-password [type data] diff --git a/src/uxbox/main/ui.cljs b/src/uxbox/main/ui.cljs index e61985d38..fbad10945 100644 --- a/src/uxbox/main/ui.cljs +++ b/src/uxbox/main/ui.cljs @@ -138,6 +138,8 @@ (def routes [["/auth/login" :auth/login] + ["/auth/register" :auth/register] + ["/auth/recovery/request" :auth/recovery-request] ["/auth/recovery/token/:token" :auth/recovery] ["/settings/profile" :settings/profile] ["/settings/password" :settings/password] @@ -145,7 +147,6 @@ ["/dashboard/projects" :dashboard/projects] ["/dashboard/elements" :dashboard/elements] - ["/dashboard/icons" :dashboard/icons] ["/dashboard/icons/:type/:id" :dashboard/icons] ["/dashboard/icons/:type" :dashboard/icons] diff --git a/src/uxbox/main/ui/auth/login.cljs b/src/uxbox/main/ui/auth/login.cljs index 6b42b116f..65a32864e 100644 --- a/src/uxbox/main/ui/auth/login.cljs +++ b/src/uxbox/main/ui/auth/login.cljs @@ -5,74 +5,22 @@ ;; Copyright (c) 2016 Andrey Antukh (ns uxbox.main.ui.auth.login - (:require [sablono.core :as html :refer-macros [html]] - [lentes.core :as l] + (:require [lentes.core :as l] [cuerdas.core :as str] - [rum.core :as rum] [uxbox.util.router :as rt] - [uxbox.main.state :as st] + [uxbox.util.dom :as dom] [uxbox.util.rstore :as rs] + [uxbox.util.forms :as forms] + [uxbox.util.mixins :as mx :include-macros true] + [uxbox.main.state :as st] [uxbox.main.data.auth :as da] [uxbox.main.data.messages :as udm] - [uxbox.util.dom :as dom] [uxbox.main.ui.icons :as i] [uxbox.main.ui.messages :as uum] - [uxbox.main.ui.navigation :as nav] - [uxbox.util.mixins :as mx :include-macros true])) + [uxbox.main.ui.navigation :as nav])) -(defn- login-submit - [event local] - (dom/prevent-default event) - (let [form (:form @local)] - (rs/emit! (da/login {:username (:email form) - :password (:password form)})))) - -(defn- login-submit-enabled? - [local] - (let [form (:form @local)] - (and (not (str/empty? (:email form ""))) - (not (str/empty? (:password form "")))))) - -(defn- login-field-change - [local field event] - (let [value (str/trim (dom/event->value event))] - (swap! local assoc-in [:form field] value))) - -(defn- login-page-render - [own local] - (let [on-submit #(login-submit % local) - submit-enabled? (login-submit-enabled? local) - form (:form @local)] - (html - [:div.login - [:div.login-body - (uum/messages) - [:a i/logo] - [:form {:on-submit on-submit} - [:div.login-content - [:input.input-text - {:name "email" - :ref "email" - :value (:email form "") - :on-change #(login-field-change local :email %) - :placeholder "Email or Username" - :type "text"}] - [:input.input-text - {:name "password" - :ref "password" - :value (:password form "") - :on-change #(login-field-change local :password %) - :placeholder "Password" - :type "password"}] - [:input.btn-primary - {:name "login" - :class (when-not submit-enabled? "btn-disabled") - :disabled (not submit-enabled?) - :value "Continue" - :type "submit"}] - [:div.login-links - [:a {:on-click #(rt/go :auth/recovery-request)} "Forgot your password?"] - [:a {:on-click #(rt/go :auth/register)} "Don't have an account?"]]]]]]))) +(def form-data (forms/focus-data :login st/state)) +(def set-value! (partial forms/set-value! :login)) (defn- login-page-will-mount [own] @@ -80,9 +28,54 @@ (rt/go :dashboard/projects)) own) -(def login-page - (mx/component - {:render #(login-page-render % (:rum/local %)) - :will-mount login-page-will-mount - :name "login-page" - :mixins [(mx/local)]})) +(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+)] + (letfn [(on-change [event field] + (let [value (dom/event->value event)] + (set-value! field value))) + (on-submit [event] + (dom/prevent-default event) + (rs/emit! (da/login {:username (:email data) + :password (:password data)})))] + [:form {:on-submit on-submit} + [:div.login-content + [:input.input-text + {:name "email" + :ref "email" + :value (:email data "") + :on-change #(on-change % :email) + :placeholder "Email or Username" + :type "text"}] + [:input.input-text + {:name "password" + :ref "password" + :value (:password data "") + :on-change #(on-change % :password) + :placeholder "Password" + :type "password"}] + [:input.btn-primary + {:name "login" + :class (when-not valid? "btn-disabled") + :disabled (not valid?) + :value "Continue" + :type "submit"}] + [:div.login-links + [:a {:on-click #(rt/go :auth/recovery-request)} "Forgot your password?"] + [:a {:on-click #(rt/go :auth/register)} "Don't have an account?"]]]]))) + +(mx/defc login-page + {:mixins [mx/static] + :will-mount login-page-will-mount} + [] + [:div.login + [:div.login-body + (uum/messages) + [:a i/logo] + (login-form)]]) diff --git a/src/uxbox/main/ui/auth/recovery.cljs b/src/uxbox/main/ui/auth/recovery.cljs index b75702c62..1ec34c8df 100644 --- a/src/uxbox/main/ui/auth/recovery.cljs +++ b/src/uxbox/main/ui/auth/recovery.cljs @@ -5,81 +5,59 @@ ;; Copyright (c) 2016 Andrey Antukh (ns uxbox.main.ui.auth.recovery - (:require [sablono.core :as html :refer-macros [html]] - [lentes.core :as l] + (:require [lentes.core :as l] [cuerdas.core :as str] - [rum.core :as rum] [uxbox.util.router :as rt] [uxbox.main.state :as st] [uxbox.util.rstore :as rs] - [uxbox.util.schema :as us] + [uxbox.util.forms :as forms] + [uxbox.util.mixins :as mx :include-macros true] + [uxbox.util.dom :as dom] [uxbox.main.data.auth :as uda] [uxbox.main.data.messages :as udm] - [uxbox.main.data.forms :as udf] - [uxbox.main.ui.forms :as forms] [uxbox.main.ui.icons :as i] [uxbox.main.ui.messages :as uum] - [uxbox.main.ui.navigation :as nav] - [uxbox.util.mixins :as mx :include-macros true] - [uxbox.util.dom :as dom])) + [uxbox.main.ui.navigation :as nav])) -;; --- Constants +;; --- Recovery Form -(def form-data - (-> (l/in [:forms :recovery]) - (l/derive st/state))) +(def form-data (forms/focus-data :recovery st/state)) +(def set-value! (partial forms/set-value! :recovery)) -(def form-errors - (-> (l/in [:errors :recovery]) - (l/derive st/state))) +(def +recovery-form+ + {:password [forms/required forms/string]}) -(def set-value! - (partial udf/assign-field-value :recovery)) - -;; --- Recovery Request Form - -(def schema - {:password [us/required us/string]}) - -(defn- form-render - [own token] - (let [form (mx/react form-data) - errors (mx/react form-errors) - valid? (us/valid? form schema)] +(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+)] (letfn [(on-change [field event] (let [value (dom/event->value event)] - (rs/emit! (set-value! field value)))) + (set-value! field value))) (on-submit [event] (dom/prevent-default event) - (rs/emit! (uda/recovery (assoc form :token token))))] - (html - [:form {:on-submit on-submit} - [:div.login-content + (rs/emit! (uda/recovery data) + (forms/clear :recovery)))] + [:form {:on-submit on-submit} + [:div.login-content + [:input.input-text + {:name "password" + :value (:password data "") + :on-change (partial on-change :password) + :placeholder "Password" + :type "password"}] + [:input.btn-primary + {:name "login" + :class (when-not valid? "btn-disabled") + :disabled (not valid?) + :value "Recover password" + :type "submit"}] + [:div.login-links + [:a {:on-click #(rt/go :auth/login)} "Go back!"]]]]))) - [:input.input-text - {:name "password" - :value (:password form "") - :on-change (partial on-change :password) - :placeholder "Password" - :type "password"}] - (forms/input-error errors :password) - - [:input.btn-primary - {:name "login" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :value "Recover password" - :type "submit"}] - [:div.login-links - [:a {:on-click #(rt/go :auth/login)} "Go back!"]]]])))) - -(def form - (mx/component - {:render form-render - :name "form" - :mixins [mx/static mx/reactive]})) - -;; --- Recovery Request Page +;; --- Recovery Page (defn- recovery-page-will-mount [own] @@ -87,18 +65,13 @@ (rs/emit! (uda/validate-recovery-token token)) own)) -(defn- recovery-page-render - [own token] - (html - [:div.login - [:div.login-body - (uum/messages) - [:a i/logo] - (form token)]])) - -(def recovery-page - (mx/component - {:render recovery-page-render - :will-mount recovery-page-will-mount - :name "recovery-page" - :mixins [mx/static]})) +(mx/defc recovery-page + {:mixins [mx/static] + :will-mount recovery-page-will-mount + :will-unmount (forms/cleaner-fn :recovery)} + [token] + [:div.login + [:div.login-body + (uum/messages) + [:a i/logo] + (recovery-form token)]]) diff --git a/src/uxbox/main/ui/auth/recovery_request.cljs b/src/uxbox/main/ui/auth/recovery_request.cljs index 7cb4896a3..b186d850f 100644 --- a/src/uxbox/main/ui/auth/recovery_request.cljs +++ b/src/uxbox/main/ui/auth/recovery_request.cljs @@ -5,92 +5,63 @@ ;; Copyright (c) 2016 Andrey Antukh (ns uxbox.main.ui.auth.recovery-request - (:require [sablono.core :as html :refer-macros [html]] - [lentes.core :as l] + (:require [lentes.core :as l] [cuerdas.core :as str] - [rum.core :as rum] [uxbox.util.router :as rt] - [uxbox.main.state :as st] [uxbox.util.rstore :as rs] - [uxbox.util.schema :as us] + [uxbox.util.forms :as forms] + [uxbox.util.mixins :as mx :include-macros true] + [uxbox.util.dom :as dom] + [uxbox.main.state :as st] [uxbox.main.data.auth :as uda] [uxbox.main.data.messages :as udm] - [uxbox.main.data.forms :as udf] - [uxbox.main.ui.forms :as forms] [uxbox.main.ui.icons :as i] [uxbox.main.ui.messages :as uum] - [uxbox.main.ui.navigation :as nav] - [uxbox.util.mixins :as mx :include-macros true] - [uxbox.util.dom :as dom])) + [uxbox.main.ui.navigation :as nav])) -;; --- Recovery Request Constants +(def form-data (forms/focus-data :recovery-request st/state)) +(def set-value! (partial forms/set-value! :recovery-request)) -(def form-data - (-> (l/in [:forms :recovery-request]) - (l/derive st/state))) +(def +recovery-request-form+ + {:username [forms/required forms/string]}) -(def form-errors - (-> (l/in [:errors :recovery-request]) - (l/derive st/state))) - -(def set-value! - (partial udf/assign-field-value :recovery-request)) - -;; --- Recovery Request Form - -(def schema - {:username [us/required us/string]}) - -(defn- form-render - [own] - (let [form (mx/react form-data) - errors (mx/react form-errors) - valid? (us/valid? form schema)] +(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] (let [value (dom/event->value event)] - (rs/emit! (set-value! field value)))) + (set-value! field value))) (on-submit [event] (dom/prevent-default event) - (rs/emit! (uda/recovery-request form)))] - (html - [:form {:on-submit on-submit} - [:div.login-content - [:input.input-text - {:name "username" - :value (:username form "") - :on-change (partial on-change :username) - :placeholder "username or email address" - :type "text"}] - (forms/input-error errors :username) - - [:input.btn-primary - {:name "login" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :value "Recover password" - :type "submit"}] - [:div.login-links - [:a {:on-click #(rt/go :auth/login)} "Go back!"]]]])))) - -(def form - (mx/component - {:render form-render - :name "form" - :mixins [mx/static mx/reactive]})) + (rs/emit! (uda/recovery-request data) + (forms/clear :recovery-request)))] + [:form {:on-submit on-submit} + [:div.login-content + [:input.input-text + {:name "username" + :value (:username data "") + :on-change (partial on-change :username) + :placeholder "username or email address" + :type "text"}] + [:input.btn-primary + {:name "login" + :class (when-not valid? "btn-disabled") + :disabled (not valid?) + :value "Recover password" + :type "submit"}] + [:div.login-links + [:a {:on-click #(rt/go :auth/login)} "Go back!"]]]]))) ;; --- Recovery Request Page -(defn- recovery-request-page-render - [own] - (html - [:div.login - [:div.login-body - (uum/messages) - [:a i/logo] - (form)]])) - -(def recovery-request-page - (mx/component - {:render recovery-request-page-render - :name "recovery-request-page" - :mixins [mx/static]})) +(mx/defc recovery-request-page + {:mixins [mx/static] + :will-unmount (forms/cleaner-fn :recovery-request)} + [] + [:div.login + [:div.login-body + (uum/messages) + [:a i/logo] + (recovery-request-form)]]) diff --git a/src/uxbox/main/ui/auth/register.cljs b/src/uxbox/main/ui/auth/register.cljs index 9fe19155e..2fd37592f 100644 --- a/src/uxbox/main/ui/auth/register.cljs +++ b/src/uxbox/main/ui/auth/register.cljs @@ -5,116 +5,104 @@ ;; Copyright (c) 2016 Andrey Antukh (ns uxbox.main.ui.auth.register - (:require [sablono.core :as html :refer-macros [html]] - [lentes.core :as l] + (:require [lentes.core :as l] [cuerdas.core :as str] - [rum.core :as rum] [uxbox.util.router :as rt] - [uxbox.main.state :as st] [uxbox.util.rstore :as rs] - [uxbox.util.schema :as us] + [uxbox.util.forms :as forms] + [uxbox.util.mixins :as mx :include-macros true] + [uxbox.util.dom :as dom] + [uxbox.main.state :as st] [uxbox.main.data.auth :as uda] [uxbox.main.data.messages :as udm] - [uxbox.main.data.forms :as udf] - [uxbox.main.ui.forms :as forms] [uxbox.main.ui.icons :as i] [uxbox.main.ui.messages :as uum] - [uxbox.main.ui.navigation :as nav] - [uxbox.util.mixins :as mx :include-macros true] - [uxbox.util.dom :as dom])) - -;; --- Constants - -(def form-data - (-> (l/in [:forms :register]) - (l/derive st/state))) - -(def form-errors - (-> (l/in [:errors :register]) - (l/derive st/state))) - -(def set-value! - (partial udf/assign-field-value :register)) + [uxbox.main.ui.navigation :as nav])) ;; --- Register Form -(defn- register-form-render - [own] - (let [form (mx/react form-data) +(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! :register)) +(def set-error! (partial forms/set-error! :register)) + +(def +register-form+ + {:username [forms/required forms/string] + :fullname [forms/required forms/string] + :email [forms/required forms/email] + :password [forms/required forms/string]}) + +(mx/defc register-form + {:mixins [mx/static mx/reactive]} + [] + (let [data (mx/react form-data) errors (mx/react form-errors) - valid? (us/valid? form uda/register-schema)] + valid? (forms/valid? data +register-form+)] (letfn [(on-change [field event] (let [value (dom/event->value event)] - (rs/emit! (set-value! field value)))) + (set-value! field value))) + (on-error [{:keys [type code] :as payload}] + (case code + :uxbox.services.users/email-already-exists + (set-error! :email "Email already exists") + :uxbox.services.users/username-already-exists + (set-error! :username "Username already exists"))) (on-submit [event] (dom/prevent-default event) - (rs/emit! (uda/register form)))] - (html - [:form {:on-submit on-submit} - [:div.login-content - [:input.input-text - {:name "fullname" - :value (:fullname form "") - :on-change (partial on-change :fullname) - :placeholder "Full Name" - :type "text"}] - (forms/input-error errors :fullname) + (rs/emit! (uda/register data on-error)))] + [:form {:on-submit on-submit} + [:div.login-content + [:input.input-text + {:name "fullname" + :value (:fullname data "") + :on-change (partial on-change :fullname) + :placeholder "Full Name" + :type "text"}] + (forms/input-error errors :fullname) - [:input.input-text - {:name "username" - :value (:username form "") - :on-change (partial on-change :username) - :placeholder "Username" - :type "text"}] - (forms/input-error errors :username) + [:input.input-text + {:name "username" + :value (:username data "") + :on-change (partial on-change :username) + :placeholder "Username" + :type "text"}] + (forms/input-error errors :username) - [:input.input-text - {:name "email" - :ref "email" - :value (:email form "") - :on-change (partial on-change :email) - :placeholder "Email" - :type "text"}] - (forms/input-error errors :email) + [:input.input-text + {:name "email" + :ref "email" + :value (:email data "") + :on-change (partial on-change :email) + :placeholder "Email" + :type "text"}] + (forms/input-error errors :email) - [:input.input-text - {:name "password" - :ref "password" - :value (:password form "") - :on-change (partial on-change :password) - :placeholder "Password" - :type "password"}] - (forms/input-error errors :password) + [:input.input-text + {:name "password" + :ref "password" + :value (:password data "") + :on-change (partial on-change :password) + :placeholder "Password" + :type "password"}] + (forms/input-error errors :password) - [:input.btn-primary - {:name "login" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :value "Register" - :type "submit"}] - [:div.login-links - ;; [:a {:on-click #(rt/go :auth/recover-password)} "Forgot your password?"] - [:a {:on-click #(rt/go :auth/login)} "Already have an account?"]]]])))) - -(def register-form - (mx/component - {:render register-form-render - :name "register-form" - :mixins [mx/static mx/reactive]})) + [:input.btn-primary + {:name "login" + :class (when-not valid? "btn-disabled") + :disabled (not valid?) + :value "Register" + :type "submit"}] + [:div.login-links + ;; [:a {:on-click #(rt/go :auth/recover-password)} "Forgot your password?"] + [:a {:on-click #(rt/go :auth/login)} "Already have an account?"]]]]))) ;; --- Register Page -(defn- register-page-render +(mx/defc register-page + {:mixins [mx/static]} [own] - (html - [:div.login - [:div.login-body - (uum/messages) - [:a i/logo] - (register-form)]])) - -(def register-page - (mx/component - {:render register-page-render - :name "register-page" - :mixins [mx/static]})) + [:div.login + [:div.login-body + (uum/messages) + [:a i/logo] + (register-form)]]) diff --git a/src/uxbox/main/ui/colorpicker.cljs b/src/uxbox/main/ui/colorpicker.cljs index c140d9ab3..86ede27d6 100644 --- a/src/uxbox/main/ui/colorpicker.cljs +++ b/src/uxbox/main/ui/colorpicker.cljs @@ -9,7 +9,7 @@ [rum.core :as rum] [lentes.core :as l] [goog.events :as events] - [uxbox.util.schema :as sc] + [uxbox.util.forms :as sc] [uxbox.util.mixins :as mx :include-macros true] [uxbox.util.math :as mth] [uxbox.util.data :as data] diff --git a/src/uxbox/main/ui/dashboard/colors.cljs b/src/uxbox/main/ui/dashboard/colors.cljs index b16b0cc9d..e8bc0a322 100644 --- a/src/uxbox/main/ui/dashboard/colors.cljs +++ b/src/uxbox/main/ui/dashboard/colors.cljs @@ -15,7 +15,6 @@ [uxbox.main.ui.messages :as uum] [uxbox.main.ui.colorpicker :refer (colorpicker)] [uxbox.main.ui.dashboard.header :refer (header)] - [uxbox.main.ui.forms :as form] [uxbox.main.ui.icons :as i] [uxbox.main.ui.keyboard :as k] [uxbox.main.ui.lightbox :as lbx] @@ -24,8 +23,7 @@ [uxbox.util.i18n :as t :refer (tr)] [uxbox.util.lens :as ul] [uxbox.util.mixins :as mx :include-macros true] - [uxbox.util.rstore :as rs] - [uxbox.util.schema :as sc])) + [uxbox.util.rstore :as rs])) ;; --- Refs diff --git a/src/uxbox/main/ui/dashboard/icons.cljs b/src/uxbox/main/ui/dashboard/icons.cljs index 92485410d..83164e155 100644 --- a/src/uxbox/main/ui/dashboard/icons.cljs +++ b/src/uxbox/main/ui/dashboard/icons.cljs @@ -21,7 +21,7 @@ [uxbox.util.mixins :as mx :include-macros true] [uxbox.util.datetime :as dt] [uxbox.util.rstore :as rs] - [uxbox.util.schema :as sc] + [uxbox.util.forms :as sc] [uxbox.util.lens :as ul] [uxbox.util.i18n :refer (tr)] [uxbox.util.dom :as dom])) diff --git a/src/uxbox/main/ui/forms.cljs b/src/uxbox/main/ui/forms.cljs deleted file mode 100644 index 773150496..000000000 --- a/src/uxbox/main/ui/forms.cljs +++ /dev/null @@ -1,16 +0,0 @@ -(ns uxbox.main.ui.forms - (:require [sablono.core :refer-macros [html]] - [uxbox.util.i18n :refer (tr)] - [uxbox.util.schema :as sc])) - -(defn input-error - [errors field] - (when-let [error (get errors field)] - (html - [:ul.form-errors - [:li {:key error} (tr error)]]))) - -(defn error-class - [errors field] - (when (get errors field) - "invalid")) diff --git a/src/uxbox/main/ui/settings/password.cljs b/src/uxbox/main/ui/settings/password.cljs index 63f575c5e..99a11ee8a 100644 --- a/src/uxbox/main/ui/settings/password.cljs +++ b/src/uxbox/main/ui/settings/password.cljs @@ -6,98 +6,81 @@ ;; Copyright (c) 2016 Juan de la Cruz (ns uxbox.main.ui.settings.password - (:require [sablono.core :as html :refer-macros [html]] - [rum.core :as rum] - [lentes.core :as l] + (:require [lentes.core :as l] [cuerdas.core :as str] - [uxbox.util.schema :as sc] - [uxbox.main.state :as st] [uxbox.util.i18n :as t :refer (tr)] - [uxbox.util.router :as r] [uxbox.util.rstore :as rs] - [uxbox.main.data.users :as udu] - [uxbox.main.data.forms :as udf] - [uxbox.main.ui.icons :as i] - [uxbox.main.ui.forms :as forms] - [uxbox.main.ui.messages :as uum] + [uxbox.util.forms :as forms] + [uxbox.util.dom :as dom] [uxbox.util.mixins :as mx :include-macros true] - [uxbox.main.ui.settings.header :refer (header)] - [uxbox.util.dom :as dom])) + [uxbox.main.state :as st] + [uxbox.main.data.users :as udu] + [uxbox.main.ui.icons :as i] + [uxbox.main.ui.messages :as uum] + [uxbox.main.ui.settings.header :refer (header)])) -;; --- Password Form +(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! :profile-password)) +(def set-errors! (partial forms/set-errors! :profile-password)) -(def formdata - (-> (l/in [:forms :profile/password]) - (l/derive st/state))) +(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]]) -(def formerrors - (-> (l/in [:errors :profile/password]) - (l/derive st/state))) - -(def assign-field-value - (partial udf/assign-field-value :profile/password)) - -(defn password-form-render - [own] - (let [form (mx/react formdata) - errors (mx/react formerrors) - valid? (sc/valid? form udu/update-password-schema)] - (letfn [(on-field-change [field event] +(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+)] + (letfn [(on-change [field event] (let [value (dom/event->value event)] - (rs/emit! (assign-field-value field value)))) + (set-value! field value))) (on-submit [event] - (rs/emit! (udu/update-password form)))] - (html - [:form.password-form - [:span.user-settings-label "Change password"] - [:input.input-text - {:type "password" - :class (forms/error-class errors :old-password) - :value (:old-password form "") - :on-change (partial on-field-change :old-password) - :placeholder "Old password"}] - (forms/input-error errors :old-password) - [:input.input-text - {:type "password" - :class (forms/error-class errors :password-1) - :value (:password-1 form "") - :on-change (partial on-field-change :password-1) - :placeholder "New password"}] - (forms/input-error errors :password-1) - [:input.input-text - {:type "password" - :class (forms/error-class errors :password-2) - :value (:password-2 form "") - :on-change (partial on-field-change :password-2) - :placeholder "Confirm password"}] - (forms/input-error errors :password-2) - [:input.btn-primary - {:type "button" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :on-click on-submit - :value "Update settings"}]])))) - -(def password-form - (mx/component - {:render password-form-render - :name "password-form" - :mixins [mx/static (mx/local) mx/reactive]})) + (println "on-submit" data) + #_(rs/emit! (udu/update-password form)))] + (println "password-form" data) + [: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) + :placeholder "Old password"}] + (forms/input-error errors :old-password) + [:input.input-text + {:type "password" + :class (forms/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) + [:input.input-text + {:type "password" + :class (forms/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) + [:input.btn-primary + {:type "button" + :class (when-not valid? "btn-disabled") + :disabled (not valid?) + :on-click on-submit + :value "Update settings"}]]))) ;; --- Password Page -(defn password-page-render - [own] - (html - [:main.dashboard-main - (header) - (uum/messages) - [:section.dashboard-content.user-settings - [:section.user-settings-content - (password-form)]]])) - -(def password-page - (mx/component - {:render password-page-render - :name "password-page" - :mixins [mx/static]})) +(mx/defc password-page + {:mixins [mx/static]} + [] + [:main.dashboard-main + (header) + (uum/messages) + [:section.dashboard-content.user-settings + [:section.user-settings-content + (password-form)]]]) diff --git a/src/uxbox/main/ui/settings/profile.cljs b/src/uxbox/main/ui/settings/profile.cljs index 961d06eec..f31cf491d 100644 --- a/src/uxbox/main/ui/settings/profile.cljs +++ b/src/uxbox/main/ui/settings/profile.cljs @@ -6,100 +6,102 @@ ;; Copyright (c) 2016 Juan de la Cruz (ns uxbox.main.ui.settings.profile - (:require [sablono.core :as html :refer-macros [html]] - [rum.core :as rum] - [cuerdas.core :as str] + (:require [cuerdas.core :as str] [lentes.core :as l] - [uxbox.util.schema :as sc] + [uxbox.util.forms :as forms] [uxbox.util.router :as r] - [uxbox.main.state :as st] [uxbox.util.rstore :as rs] - [uxbox.main.ui.icons :as i] [uxbox.util.mixins :as mx :include-macros true] - [uxbox.main.ui.forms :as forms] + [uxbox.util.interop :refer (iterable->seq)] + [uxbox.util.dom :as dom] + [uxbox.main.state :as st] + [uxbox.main.ui.icons :as i] [uxbox.main.ui.settings.header :refer (header)] [uxbox.main.ui.messages :as uum] - [uxbox.main.data.users :as udu] - [uxbox.main.data.forms :as udf] - [uxbox.util.interop :refer (iterable->seq)] - [uxbox.util.dom :as dom])) + [uxbox.main.data.users :as udu])) -;; --- Constants +(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! :profile)) +(def set-error! (partial forms/set-error! :profile)) -(def formdata - (-> (l/in [:forms :profile/main]) - (l/derive st/state))) - -(def formerrors - (-> (l/in [:errors :profile/main]) - (l/derive st/state))) - -(def assign-field-value - (partial udf/assign-field-value :profile/main)) - -(def ^:private profile-ref +(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]}) + ;; --- Profile Form -(defn profile-form-render - [own] - (let [form (merge (mx/react profile-ref) - (mx/react formdata)) - errors (mx/react formerrors) - valid? (sc/valid? form udu/update-profile-schema) - theme (get-in form [:metadata :theme] "light")] - +(mx/defc profile-form + {:mixins [mx/static mx/reactive] + :will-unmount (forms/cleaner-fn :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+) + theme (:theme data)] (letfn [(on-change [field event] (let [value (dom/event->value event)] - (rs/emit! (assign-field-value field value)))) + (set-value! field value))) + (on-error [{:keys [code] :as payload}] + (case code + :uxbox.services.users/email-already-exists + (set-error! :email "Email already exists") + :uxbox.services.users/username-already-exists + (set-error! :username "Username already exists"))) + (on-success [] + (forms/clear! :profile)) (on-submit [event] - (rs/emit! (udu/update-profile form)))] - (html - [:form.profile-form - [:span.user-settings-label "Name, username and email"] - [:input.input-text - {:type "text" - :on-change (partial on-change :fullname) - :value (:fullname form "") - :placeholder "Your name"}] - (forms/input-error errors :fullname) - [:input.input-text - {:type "text" - :on-change (partial on-change :username) - :value (:username form "") - :placeholder "Your username"}] + (rs/emit! (udu/update-profile data on-success on-error)))] + [:form.profile-form + [:span.user-settings-label "Name, username and email"] + [:input.input-text + {:type "text" + :on-change (partial on-change :fullname) + :value (:fullname data "") + :placeholder "Your name"}] + [:input.input-text + {:type "text" + :on-change (partial on-change :username) + :value (:username data "") + :placeholder "Your username"}] (forms/input-error errors :username) [:input.input-text {:type "email" :on-change (partial on-change :email) - :value (:email form "") + :value (:email data "") :placeholder "Your email"}] (forms/input-error errors :email) [:span.user-settings-label "Choose a color theme"] [:div.input-radio.radio-primary [:input {:type "radio" - :checked (= theme "light") - :on-change (partial on-change [:metadata :theme]) + :checked (when (= theme "light") "checked") + :on-change (partial on-change :theme) :id "light-theme" :name "theme" :value "light"}] [:label {:for "light-theme"} "Light theme"] [:input {:type "radio" - :checked (= theme "dark") - :on-change (partial on-change [:metadata :theme]) + :checked (when (= theme "dark") "checked") + :on-change (partial on-change :theme) :id "dark-theme" :name "theme" :value "dark"}] [:label {:for "dark-theme"} "Dark theme"] [:input {:type "radio" - :checked (= theme "high-contrast") - :on-change (partial on-change [:metadata :theme]) + :checked (when (= theme "high-contrast") "checked") + :on-change (partial on-change :theme) :id "high-contrast-theme" :name "theme" :value "high-contrast"}] @@ -110,18 +112,13 @@ :class (when-not valid? "btn-disabled") :disabled (not valid?) :on-click on-submit - :value "Update settings"}]])))) - -(def profile-form - (mx/component - {:render profile-form-render - :name "profile-form" - :mixins [(mx/local) mx/reactive mx/static]})) + :value "Update settings"}]]))) ;; --- Profile Photo Form -(defn- profile-photo-form-render - [own] +(mx/defc profile-photo-form + {:mixins [mx/static mx/reactive]} + [] (letfn [(on-change [event] (let [target (dom/get-target event) file (-> (dom/get-files target) @@ -133,36 +130,22 @@ photo (if (or (str/empty? photo) (nil? photo)) "images/avatar.jpg" photo)] - (html - [:form.avatar-form - [:img {:src photo :border "0"}] - [:input {:type "file" - :value "" - :on-change on-change}]])))) - -(def profile-photo-form - (mx/component - {:render profile-photo-form-render - :name profile-photo-form - :mixins [mx/static mx/reactive]})) + [:form.avatar-form + [:img {:src photo}] + [:input {:type "file" + :value "" + :on-change on-change}]]))) ;; --- Profile Page -(defn profile-page-render - [own] - (html - [:main.dashboard-main - (header) - (uum/messages) - [:section.dashboard-content.user-settings - [:section.user-settings-content - [:span.user-settings-label "Your avatar"] - (profile-photo-form) - (profile-form) - ]]])) - -(def profile-page - (mx/component - {:render profile-page-render - :name "profile-page" - :mixins [mx/static]})) +(mx/defc profile-page + {:mixins [mx/static]} + [] + [:main.dashboard-main + (header) + (uum/messages) + [:section.dashboard-content.user-settings + [:section.user-settings-content + [:span.user-settings-label "Your avatar"] + (profile-photo-form) + (profile-form)]]]) diff --git a/src/uxbox/main/ui/workspace/base.cljs b/src/uxbox/main/ui/workspace/base.cljs index f0a1b62ee..c3d99cf3d 100644 --- a/src/uxbox/main/ui/workspace/base.cljs +++ b/src/uxbox/main/ui/workspace/base.cljs @@ -61,8 +61,8 @@ (def alignment-ref (letfn [(getter [flags] - (and (contains? flags :grid/indexed) - (contains? flags :grid/alignment) + (and (contains? flags :grid-indexed) + (contains? flags :grid-alignment) (contains? flags :grid)))] (-> (l/lens getter) (l/derive flags-ref)))) diff --git a/src/uxbox/main/ui/workspace/grid.cljs b/src/uxbox/main/ui/workspace/grid.cljs index 4ac27e178..d970a506a 100644 --- a/src/uxbox/main/ui/workspace/grid.cljs +++ b/src/uxbox/main/ui/workspace/grid.cljs @@ -21,16 +21,16 @@ (defn- grid-render [own] (let [options (:options (mx/react wb/page-ref)) - color (:grid/color options "#cccccc") + color (:grid-color options "#cccccc") width c/viewport-width height c/viewport-height x-ticks (range (- 0 c/canvas-start-x) (- width c/canvas-start-x) - (:grid/x-axis options 10)) + (:grid-x-axis options 10)) y-ticks (range (- 0 c/canvas-start-x) (- height c/canvas-start-x) - (:grid/y-axis options 10)) + (:grid-y-axis options 10)) path (as-> [] $ (reduce (partial vertical-line height) $ x-ticks) diff --git a/src/uxbox/main/ui/workspace/settings.cljs b/src/uxbox/main/ui/workspace/settings.cljs index c66518325..6cd69b620 100644 --- a/src/uxbox/main/ui/workspace/settings.cljs +++ b/src/uxbox/main/ui/workspace/settings.cljs @@ -6,119 +6,114 @@ ;; Copyright (c) 2016 Juan de la Cruz (ns uxbox.main.ui.workspace.settings - (:require [sablono.core :as html :refer-macros [html]] - [lentes.core :as l] - [rum.core :as rum] + (:require [lentes.core :as l] [uxbox.main.constants :as c] [uxbox.main.state :as st] [uxbox.util.rstore :as rs] [uxbox.main.data.pages :as udp] - [uxbox.main.data.forms :as udf] [uxbox.main.data.workspace :as udw] [uxbox.main.data.lightbox :as udl] [uxbox.main.ui.icons :as i] + [uxbox.util.forms :as forms] [uxbox.util.mixins :as mx :include-macros true] - [uxbox.main.ui.forms :as forms] [uxbox.main.ui.lightbox :as lbx] [uxbox.main.ui.colorpicker :as uucp] [uxbox.main.ui.workspace.base :as wb] [uxbox.util.dom :as dom] [uxbox.util.data :refer (parse-int)])) -;; --- Lentes - -(def formdata (udf/focus-form-data :workspace/settings)) -(def formerrors (udf/focus-form-errors :workspace/settings)) -(def assign-field-value (partial udf/assign-field-value :workspace/settings)) +(def form-data (forms/focus-data :workspace-settings st/state)) +(def form-errors (forms/focus-errors :workspace-settings st/state)) +(def set-value! (partial forms/set-value! :workspace-settings)) +(def set-errors! (partial forms/set-errors! :workspace-settings)) +(def page-ref wb/page-ref) ;; --- Form Component -(def settings-form-defaults - {:grid/x-axis c/grid-x-axis - :grid/y-axis c/grid-y-axis - :grid/color "#b5bdb9" - :grid/alignment false}) +(def +settings-defaults+ + {:grid-x-axis c/grid-x-axis + :grid-y-axis c/grid-y-axis + :grid-color "#b5bdb9" + :grid-alignment false}) -(defn- settings-form-render - [own] - (let [page (mx/react wb/page-ref) - form (merge settings-form-defaults - (:options page) - (mx/react formdata)) - errors (mx/react formerrors)] +(def +settings-form+ + {:grid-y-axis [forms/required forms/integer [forms/in-range 2 100]] + :grid-x-axis [forms/required forms/integer [forms/in-range 2 100]] + :grid-alignment [forms/boolean] + :grid-color [forms/required forms/color]}) + +(mx/defc settings-form + {:mixins [mx/reactive]} + [] + (let [{:keys [id] :as page} (mx/react page-ref) + errors (mx/react form-errors) + data (merge +settings-defaults+ + (:metadata page) + (mx/react form-data))] (letfn [(on-field-change [field event] (let [value (dom/event->value event) value (parse-int value "")] - (rs/emit! (assign-field-value field value)))) + (set-value! field value))) (on-color-change [color] - (rs/emit! (assign-field-value :grid/color color))) + (set-value! :grid-color color)) (on-align-change [event] (let [checked? (-> (dom/get-target event) (dom/checked?))] - (rs/emit! (assign-field-value :grid/alignment checked?)))) + (set-value! :grid-alignment checked?))) (on-submit [event] (dom/prevent-default event) - (rs/emit! (udw/submit-workspace-settings (:id page) form)))] - (html - [:form {:on-submit on-submit} - [:span.lightbox-label "Grid size"] - [:div.project-size - [:div.input-element.pixels - [:input#grid-x.input-text - {:placeholder "X" - :type "number" - :class (forms/error-class errors :grid/x-axis) - :value (:grid/x-axis form "") - :on-change (partial on-field-change :grid/x-axis) - :min 1 - :max 100}]] - [:div.input-element.pixels - [:input#grid-y.input-text - {:placeholder "Y" - :type "number" - :class (forms/error-class errors :grid/y-axis) - :value (:grid/y-axis form "") - :on-change (partial on-field-change :grid/y-axis) - :min 1 - :max 100}]]] - [:span.lightbox-label "Grid color"] - (uucp/colorpicker - :value (:grid/color form) - :on-change on-color-change) - [:span.lightbox-label "Grid magnet option"] - [:div.input-checkbox.check-primary - [:input - {:type "checkbox" - :on-change on-align-change - :checked (:grid/alignment form) - :id "magnet" - :value "Yes"}] - [:label {:for "magnet"} "Activate magnet"]] - [:input.btn-primary - {:type "submit" - :value "Save"}]])))) + (let [[errors data] (forms/validate data +settings-form+)] + (if errors + (set-errors! errors) + (rs/emit! (udw/update-metadata id data) + (forms/clear :workspace-settings) + (udl/hide-lightbox)))))] + [:form {:on-submit on-submit} + [:span.lightbox-label "Grid size"] + [:div.project-size + [:div.input-element.pixels + [:input#grid-x.input-text + {:placeholder "X" + :type "number" + :class (forms/error-class errors :grid-x-axis) + :value (:grid-x-axis data "") + :on-change (partial on-field-change :grid-x-axis) + :min 2 + :max 100}]] + [:div.input-element.pixels + [:input#grid-y.input-text + {:placeholder "Y" + :type "number" + :class (forms/error-class errors :grid-y-axis) + :value (:grid-y-axis data "") + :on-change (partial on-field-change :grid-y-axis) + :min 2 + :max 100}]]] + [:span.lightbox-label "Grid color"] + (uucp/colorpicker + :value (:grid-color data) + :on-change on-color-change) + [:span.lightbox-label "Grid magnet option"] + [:div.input-checkbox.check-primary + [:input + {:type "checkbox" + :on-change on-align-change + :checked (:grid-alignment data) + :id "magnet" + :value "Yes"}] + [:label {:for "magnet"} "Activate magnet"]] + [:input.btn-primary + {:type "submit" + :value "Save"}]]))) -(def settings-form - (mx/component - {:render settings-form-render - :name "settings-form" - :mixins [(mx/local) mx/reactive mx/static]})) - -(defn- settings-dialog-render +(mx/defc settings-dialog [own] - (html - [:div.lightbox-body.settings - [:h3 "Grid settings"] - (settings-form) - [:a.close {:href "#" - :on-click #(do (dom/prevent-default %) - (udl/close!))} i/close]])) - -(def settings-dialog - (mx/component - {:render settings-dialog-render - :name "settings-dialog" - :mixins []})) + [:div.lightbox-body.settings + [:h3 "Grid settings"] + (settings-form) + [:a.close {:href "#" + :on-click #(do (dom/prevent-default %) + (udl/close!))} i/close]]) (defmethod lbx/render-lightbox :settings [_] diff --git a/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs b/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs index a6e1fac1a..bcdb0d166 100644 --- a/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs +++ b/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs @@ -16,12 +16,11 @@ [uxbox.main.data.pages :as udp] [uxbox.main.data.workspace :as dw] [uxbox.main.data.lightbox :as udl] - [uxbox.main.ui.dashboard.projects :refer (+layouts+)] [uxbox.main.ui.workspace.base :as wb] + [uxbox.main.ui.workspace.sidebar.sitemap-pageform] [uxbox.main.ui.icons :as i] [uxbox.util.mixins :as mx :include-macros true] [uxbox.main.ui.lightbox :as lbx] - [uxbox.util.data :refer (deep-merge parse-int)] [uxbox.util.dom :as dom])) ;; --- Refs @@ -87,103 +86,3 @@ :let [active? (= (:id page) (:id current))]] (-> (page-item page (count pages) active?) (mx/with-key (:id page))))]]])) - -;; --- Lightbox - -(def +page-defaults+ - {:width 1920 - :height 1080 - :layout :desktop}) - -(mx/defc layout-input - [local page id] - (let [layout (get +layouts+ id) - metadata (:metadata page) - size (select-keys layout [:width :height]) - change #(swap! local update :metadata merge {:layout id} size)] - [:div - [:input {:type "radio" - :key id :id id - :name "project-layout" - :value (:id layout) - :checked (= id (:layout metadata)) - :on-change change}] - [:label {:value (:id layout) :for id} (:name layout)]])) - -(mx/defcs page-form-lightbox - {:mixins [(mx/local)]} - [own page] - (let [local (:rum/local own) - page (deep-merge page @local {:data nil}) - metadata (:metadata page) - edition? (:id page) - valid? (and (not (str/empty? (str/trim (:name page "")))) - (pos? (:width metadata)) - (pos? (:height metadata)))] - (letfn [(update-size [field e] - (let [value (dom/event->value e) - value (parse-int value)] - (swap! local assoc-in [:metadata field] value))) - (update-name [e] - (let [value (dom/event->value e)] - (swap! local assoc :name value))) - (toggle-sizes [] - (let [width (get-in page [:metadata :width]) - height (get-in page [:metadata :height])] - (swap! local update :metadata merge {:width height - :height width}))) - (cancel [e] - (dom/prevent-default e) - (udl/close!)) - (persist [e] - (dom/prevent-default e) - (udl/close!) - (if edition? - (rs/emit! (udp/update-page-metadata page)) - (rs/emit! (udp/create-page page))))] - [:div.lightbox-body - (if edition? - [:h3 "Edit page"] - [:h3 "New page"]) - [:form - [:input#project-name.input-text - {:placeholder "Page name" - :type "text" - :value (:name page "") - :auto-focus true - :on-change update-name}] - [:div.project-size - [:div.input-element.pixels - [:input#project-witdh.input-text - {:placeholder "Width" - :type "number" - :min 0 - :max 4000 - :value (:width metadata) - :on-change #(update-size :width %)}]] - [:a.toggle-layout {:on-click toggle-sizes} i/toggle] - [:div.input-element.pixels - [:input#project-height.input-text - {:placeholder "Height" - :type "number" - :min 0 - :max 4000 - :value (:height metadata) - :on-change #(update-size :height %)}]]] - - [:div.input-radio.radio-primary - (layout-input local page "mobile") - (layout-input local page "tablet") - (layout-input local page "notebook") - (layout-input local page "desktop")] - - (when valid? - [:input#project-btn.btn-primary - {:value "Go go go!" - :on-click persist - :type "button"}])] - [:a.close {:on-click cancel} i/close]]))) - -(defmethod lbx/render-lightbox :page-form - [{:keys [page]}] - (page-form-lightbox page)) diff --git a/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs b/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs new file mode 100644 index 000000000..31d83bcc0 --- /dev/null +++ b/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs @@ -0,0 +1,145 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) 2015-2016 Andrey Antukh +;; 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] + [uxbox.main.state :as st] + [uxbox.main.data.pages :as udp] + [uxbox.main.data.workspace :as dw] + [uxbox.main.data.lightbox :as udl] + [uxbox.main.ui.dashboard.projects :refer (+layouts+)] + [uxbox.main.ui.icons :as i] + [uxbox.main.ui.lightbox :as lbx] + [uxbox.util.i18n :refer (tr)] + [uxbox.util.router :as r] + [uxbox.util.rstore :as rs] + [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])) + +(def form-data (forms/focus-data :workspace-page-form st/state)) +(def set-value! (partial forms/set-value! :workspace-page-form)) + +;; --- Lightbox + +(def +page-defaults+ + {:width 1920 + :height 1080 + :layout :desktop}) + +(def +page-form+ + {:name [forms/required forms/string] + :width [forms/required forms/number] + :height [forms/required forms/number] + :layout [forms/required forms/string]}) + +(mx/defc layout-input + [data id] + (let [{:keys [id name width height]} (get +layouts+ id)] + (letfn [(on-change [event] + (set-value! :layout id) + (set-value! :width width) + (set-value! :height height))] + [:div + [:input {:type "radio" + :id id + :name "project-layout" + :value id + :checked (when (= id (:layout data)) "checked") + :on-change on-change}] + [:label {:value id :for id} name]]))) + +(mx/defc page-form + {:mixins [mx/static mx/reactive]} + [{:keys [metadata id] :as page}] + (let [data (merge +page-defaults+ + (select-keys page [:name]) + (select-keys metadata [:width :height :layout]) + (mx/react form-data)) + valid? (forms/valid? data +page-form+)] + (letfn [(update-size [field e] + (let [value (dom/event->value e) + value (parse-int value)] + (set-value! field value))) + (update-name [e] + (let [value (dom/event->value e)] + (set-value! :name value))) + (toggle-sizes [] + (let [{:keys [width height]} data] + (set-value! :width height) + (set-value! :height width))) + (on-cancel [e] + (dom/prevent-default e) + (udl/close!)) + (on-save [e] + (dom/prevent-default e) + (udl/close!) + (if (nil? id) + (rs/emit! (udp/create-page data)) + (rs/emit! (udp/update-page id data))))] + [:form + [:input#project-name.input-text + {:placeholder "Page name" + :type "text" + :value (:name data "") + :auto-focus true + :on-change update-name}] + [:div.project-size + [:div.input-element.pixels + [:input#project-witdh.input-text + {:placeholder "Width" + :type "number" + :min 0 + :max 4000 + :value (:width data) + :on-change #(update-size :width %)}]] + [:a.toggle-layout {:on-click toggle-sizes} i/toggle] + [:div.input-element.pixels + [:input#project-height.input-text + {:placeholder "Height" + :type "number" + :min 0 + :max 4000 + :value (:height data) + :on-change #(update-size :height %)}]]] + + [:div.input-radio.radio-primary + (layout-input data "mobile") + (layout-input data "tablet") + (layout-input data "notebook") + (layout-input data "desktop")] + + (when valid? + [:input#project-btn.btn-primary + {:value "Go go go!" + :on-click on-save + :type "button"}])]))) + +(mx/defc page-form-lightbox + {:mixins [mx/static] + :will-unmount (fn [own] + (forms/clear! :workspace-page-form) + own)} + [{:keys [id] :as page}] + (letfn [(on-cancel [event] + (dom/prevent-default event) + (udl/close!))] + (let [creation? (nil? id)] + [:div.lightbox-body + (if creation? + [:h3 "New page"] + [:h3 "Edit page"]) + (page-form page) + [:a.close {:on-click on-cancel} i/close]]))) + +(defmethod lbx/render-lightbox :page-form + [{:keys [page]}] + (page-form-lightbox page)) + + diff --git a/src/uxbox/util/forms.cljs b/src/uxbox/util/forms.cljs new file mode 100644 index 000000000..2c990b05d --- /dev/null +++ b/src/uxbox/util/forms.cljs @@ -0,0 +1,264 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) 2015-2016 Andrey Antukh +;; Copyright (c) 2015-2016 Juan de la Cruz + +(ns uxbox.util.forms + (:refer-clojure :exclude [keyword uuid vector boolean map set]) + (:require [struct.core :as st] + [lentes.core :as l] + [beicon.core :as rx] + [uxbox.util.rstore :as rs] + [uxbox.util.mixins :as mx :include-macros true] + [uxbox.util.i18n :refer (tr)])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Form Validation +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- Form Validators + +(def required + (assoc st/required :message "errors.form.required")) + +(def string + (assoc st/string :message "errors.form.string")) + +(def number + (assoc st/number :message "errors.form.number")) + +(def integer + (assoc st/integer :message "errors.form.integer")) + +(def boolean + (assoc st/boolean :message "errors.form.bool")) + +(def identical-to + (assoc st/identical-to :message "errors.form.identical-to")) + +(def in-range st/in-range) +;; (def uuid-like st/uuid-like) +(def uuid st/uuid) +(def keyword st/keyword) +(def integer-str st/integer-str) +(def number-str st/number-str) +;; (def boolean-like st/boolean-like) +(def email st/email) +;; (def function st/function) +(def positive st/positive) +;; (def validate st/validate) +;; (def validate! st/validate!) + +(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 + +(defn validate + ([data schema] + (validate data schema nil)) + ([data schema opts] + (st/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)))) + +(defn valid? + [data schema] + (let [[errors data] (validate data schema)] + (not errors))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Form Events +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- Set Error + +(defrecord SetError [type field error] + rs/UpdateEvent + (-apply-update [_ state] + (assoc-in state [:errors type field] error))) + +(defn set-error + ([type field] + (set-error type field nil)) + ([type field error] + {:pre [(keyword? type) + (keyword? field) + (any? error)]} + (SetError. type field error))) + +(defn set-error! + [& args] + (rs/emit! (apply set-error args))) + +;; --- Set Errors + +(defrecord SetErrors [type errors] + rs/UpdateEvent + (-apply-update [_ state] + (assoc-in state [:errors type] errors))) + +(defn set-errors + ([type] + (set-errors type nil)) + ([type errors] + {:pre [(keyword? type) + (or (map? errors) + (nil? errors))]} + (SetErrors. type errors))) + +(defn set-errors! + [& args] + (rs/emit! (apply set-errors args))) + +;; --- Set Value + +(defrecord SetValue [type field value] + rs/UpdateEvent + (-apply-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)))))) + +(defn set-value + [type field value] + {:pre [(keyword? type) + (keyword? field) + (any? value)]} + (SetValue. type field value)) + +(defn set-value! + [type field value] + (rs/emit! (set-value type field value))) + +;; --- Validate Form + +;; (defrecord ValidateForm [type form data on-success] +;; rs/WatchEvent +;; (-apply-watch [_ state stream] +;; (let [[errors data] (validate data form)] +;; (if errors +;; (rx/of (set-errors type errors)) +;; (do +;; (on-success data) +;; (rx/empty)))))) + +;; (defn validate-form +;; [& {:keys [type form data on-success]}] +;; {:pre [(keyword? type) +;; (map? form) +;; (map? data) +;; (fn? on-success)]} +;; (ValidateForm. type form data on-success)) + +;; (defn validate-form! +;; [& args] +;; (rs/emit! (apply validate-form args))) + +;; --- Clear Form + +(defrecord ClearForm [type] + rs/UpdateEvent + (-apply-update [_ state] + (assoc-in state [:forms type] nil))) + +(defn clear-form + [type] + {:pre [(keyword? type)]} + (ClearForm. type)) + +(defn clear-form! + [type] + (rs/emit! (clear-form type))) + +;; --- Clear Form + +(defrecord ClearErrors [type] + rs/UpdateEvent + (-apply-update [_ state] + (assoc-in state [:errors type] nil))) + +(defn clear-errors + [type] + {:pre [(keyword? type)]} + (ClearErrors. type)) + +(defn clear-errors! + [type] + (rs/emit! (clear-errors type))) + +;; --- Clear + +(defrecord Clear [type] + rs/WatchEvent + (-apply-watch [_ state s] + (rx/of (clear-form type) + (clear-errors type)))) + +(defn clear + [type] + (Clear. type)) + +(defn clear! + [type] + (rs/emit! (clear type))) + +;; --- Helpers + +(defn focus-data + [type state] + (-> (l/in [:forms type]) + (l/derive state))) + +(defn focus-errors + [type state] + (-> (l/in [:errors type]) + (l/derive state))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Form UI +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(mx/defc input-error + [errors field] + (when-let [error (get errors field)] + [:ul.form-errors + [:li {:key error} (tr error)]])) + +(defn error-class + [errors field] + (when (get errors field) + "invalid")) + +(defn cleaner-fn + [type] + {:pre [(keyword? type)]} + (fn [own] + (clear! type) + own)) diff --git a/src/uxbox/util/router.cljs b/src/uxbox/util/router.cljs index 307646d65..9cfc61ea0 100644 --- a/src/uxbox/util/router.cljs +++ b/src/uxbox/util/router.cljs @@ -37,10 +37,7 @@ (defrecord Navigate [id params] rs/EffectEvent (-apply-effect [_ state] - (let [loc (merge {:handler id} - (when params - {:route-params params}))] - (r/navigate! +router+ id params)))) + (r/navigate! +router+ id {}))) (defn navigate ([id] (navigate id nil)) diff --git a/src/uxbox/util/schema.cljs b/src/uxbox/util/schema.cljs deleted file mode 100644 index 1750c1935..000000000 --- a/src/uxbox/util/schema.cljs +++ /dev/null @@ -1,85 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2015-2016 Andrey Antukh -;; Copyright (c) 2015-2016 Juan de la Cruz - -(ns uxbox.util.schema - (:refer-clojure :exclude [keyword uuid vector boolean map set]) - (:require [struct.core :as st] - [uxbox.util.i18n :refer (tr)])) - -;; (def datetime -;; {:message "must be an instant" -;; :optional true -;; :validate #(instance? Instant %)}) - -(def required - (assoc st/required :message "errors.form.required")) - -(def string - (assoc st/string :message "errors.form.string")) - -(def number - (assoc st/number :message "errors.form.number")) - -(def integer - (assoc st/integer :message "errors.form.integer")) - -(def boolean - (assoc st/boolean :message "errors.form.bool")) - -(def identical-to - (assoc st/identical-to :message "errors.form.identical-to")) - -(def in-range st/in-range) -;; (def uuid-like st/uuid-like) -(def uuid st/uuid) -(def keyword st/keyword) -(def integer-str st/integer-str) -(def number-str st/number-str) -;; (def boolean-like st/boolean-like) -(def email st/email) -;; (def function st/function) -;; (def positive st/positive) -;; (def validate st/validate) -;; (def validate! st/validate!) - -(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}$" %)))}) - -(defn validate - ([data schema] - (validate data schema nil)) - ([data schema opts] - (st/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)))) - -(defn valid? - [data schema] - (let [[errors data] (validate data schema)] - (not errors))) diff --git a/src/uxbox/util/spec.cljs b/src/uxbox/util/spec.cljs new file mode 100644 index 000000000..c37dfabcc --- /dev/null +++ b/src/uxbox/util/spec.cljs @@ -0,0 +1,47 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) 2015-2016 Andrey Antukh + +(ns uxbox.util.spec + (:require [cljs.spec :as s])) + +;; --- Constants + +(def email-rx + #"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") + +(def uuid-rx + #"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") + +;; --- Predicates + +(defn email? + [v] + (and string? + (re-matches email-rx v))) + +(defn color? + [v] + (and (string? v) + (re-matches #"^#[0-9A-Fa-f]{6}$" v))) + +(defn file? + [v] + (instance? js/File v)) + +;; --- Default Specs + +(s/def ::uuid uuid?) +(s/def ::email email?) +(s/def ::color color?) + +;; --- Public Api + +(defn valid? + [spec data] + (let [valid (s/valid? spec data)] + (when-not valid + (js/console.error (str "Spec validation error:\n" (s/explain-str spec data)))) + valid)) diff --git a/src/uxbox/view/data/viewer.cljs b/src/uxbox/view/data/viewer.cljs index 332163186..15c102379 100644 --- a/src/uxbox/view/data/viewer.cljs +++ b/src/uxbox/view/data/viewer.cljs @@ -8,7 +8,7 @@ (:require [beicon.core :as rx] [uxbox.util.rstore :as rs] [uxbox.util.router :as rt] - [uxbox.util.schema :as sc] + [uxbox.util.forms :as sc] [uxbox.util.data :refer (parse-int)] [uxbox.main.repo :as rp] [uxbox.main.data.pages :as udpg] diff --git a/src/uxbox/worker/align.cljs b/src/uxbox/worker/align.cljs index 81b4f5798..b570d1003 100644 --- a/src/uxbox/worker/align.cljs +++ b/src/uxbox/worker/align.cljs @@ -13,13 +13,13 @@ (defonce tree (kd/create)) -(defmethod impl/handler :grid/init +(defmethod impl/handler :grid-init [{:keys [sender width height x-axis y-axis] :as opts}] (time (kd/setup! tree width height (or x-axis 10) (or y-axis 10))) (impl/reply! sender nil)) -(defmethod impl/handler :grid/align +(defmethod impl/handler :grid-align [{:keys [sender point] :as message}] (let [point [(:x point) (:y point)] results (kd/nearest tree point 1)