Introduce cljs.spec and refactor all forms.

This commit is contained in:
Andrey Antukh 2016-11-13 15:42:44 +01:00
parent 29e6ebdb83
commit fce36cfdd9
No known key found for this signature in database
GPG key ID: 4DFEBCB8316A8B95
34 changed files with 1187 additions and 1144 deletions

View file

@ -6,19 +6,25 @@
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.data.auth (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.main.repo :as rp]
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[uxbox.main.state :as st] [uxbox.util.spec :as us]
[uxbox.util.schema :as us]
[uxbox.util.i18n :refer (tr)] [uxbox.util.i18n :refer (tr)]
[uxbox.main.state :as st]
[uxbox.main.data.projects :as udp] [uxbox.main.data.projects :as udp]
[uxbox.main.data.users :as udu] [uxbox.main.data.users :as udu]
[uxbox.main.data.messages :as udm] [uxbox.main.data.messages :as udm]
[uxbox.main.data.forms :as udf]
[uxbox.util.storage :refer (storage)])) [uxbox.util.storage :refer (storage)]))
(s/def ::username string?)
(s/def ::password string?)
(s/def ::fullname string?)
(s/def ::email us/email?)
(s/def ::token string?)
;; --- Logged In ;; --- Logged In
(defrecord LoggedIn [data] (defrecord LoggedIn [data]
@ -58,8 +64,12 @@
(rx/map logged-in) (rx/map logged-in)
(rx/catch rp/client-error? on-error))))) (rx/catch rp/client-error? on-error)))))
(s/def ::login-event
(s/keys :req-un [::username ::password]))
(defn login (defn login
[params] [params]
{:pre [(us/valid? ::login-event params)]}
(map->Login params)) (map->Login params))
;; --- Logout ;; --- Logout
@ -80,40 +90,33 @@
;; --- Register ;; --- Register
(defrecord Register [data] ;; TODO: clean form on success
(defrecord Register [data on-error]
rs/WatchEvent rs/WatchEvent
(-apply-watch [_ state stream] (-apply-watch [_ state stream]
(letfn [(on-error [{payload :payload}] (letfn [(handle-error [{payload :payload}]
(->> (:payload payload) (on-error payload)
(udf/assign-errors :register) (rx/empty))]
(rx/of)))]
(rx/merge (rx/merge
(->> (rp/req :auth/register data) (->> (rp/req :auth/register data)
(rx/map :payload) (rx/map :payload)
(rx/map (constantly ::registered)) (rx/map (constantly ::registered))
(rx/catch rp/client-error? on-error)) (rx/catch rp/client-error? handle-error))
(->> stream (->> stream
(rx/filter #(= % ::registered)) (rx/filter #(= % ::registered))
(rx/take 1) (rx/take 1)
(rx/map #(login data))) (rx/map #(login data)))))))
(->> stream
(rx/filter logged-in?)
(rx/take 1)
(rx/map #(udf/clean :register)))))))
(def register-schema (s/def ::register-event
{:username [us/required us/string] (s/keys :req-un [::fullname ::username ::email ::password]))
:fullname [us/required us/string]
:email [us/required us/email]
:password [us/required us/string]})
(defn register (defn register
"Create a register event instance." "Create a register event instance."
[data] [data on-error]
(let [[errors data] (us/validate data register-schema)] {:pre [(us/valid? ::register-event data)
(if errors (fn? on-error)]}
(udf/assign-errors :register errors) (Register. data on-error))
(Register. data))))
;; --- Recovery Request ;; --- Recovery Request
@ -122,9 +125,7 @@
(-apply-watch [_ state stream] (-apply-watch [_ state stream]
(letfn [(on-error [{payload :payload}] (letfn [(on-error [{payload :payload}]
(println "on-error" payload) (println "on-error" payload)
(->> (:payload payload) (rx/empty))]
(udf/assign-errors :recovery-request)
(rx/of)))]
(rx/merge (rx/merge
(->> (rp/req :auth/recovery-request data) (->> (rp/req :auth/recovery-request data)
(rx/map (constantly ::recovery-requested)) (rx/map (constantly ::recovery-requested))
@ -132,11 +133,14 @@
(->> stream (->> stream
(rx/filter #(= % ::recovery-requested)) (rx/filter #(= % ::recovery-requested))
(rx/take 1) (rx/take 1)
(rx/do #(udm/info! (tr "auth.message.recovery-token-sent"))) (rx/do #(udm/info! (tr "auth.message.recovery-token-sent"))))))))
(rx/map #(udf/clean :recovery-request)))))))
(s/def ::recovery-request-event
(s/keys :req-un [::username]))
(defn recovery-request (defn recovery-request
[data] [data]
{:pre [(us/valid? ::recovery-request-event data)]}
(RecoveryRequest. data)) (RecoveryRequest. data))
;; --- Check Recovery Token ;; --- Check Recovery Token
@ -153,8 +157,9 @@
(rx/catch rp/client-error? on-error))))) (rx/catch rp/client-error? on-error)))))
(defn validate-recovery-token (defn validate-recovery-token
[data] [token]
(ValidateRecoveryToken. data)) {:pre [(string? token)]}
(ValidateRecoveryToken. token))
;; --- Recovery (Password) ;; --- Recovery (Password)
@ -171,8 +176,10 @@
(rx/mapcat on-success) (rx/mapcat on-success)
(rx/catch rp/client-error? on-error))))) (rx/catch rp/client-error? on-error)))))
(s/def ::recovery-event
(s/keys :req-un [::username ::token]))
(defn recovery (defn recovery
[{:keys [token password]}] [{:keys [token password] :as data}]
{:pre [(us/valid? ::recovery-event data)]}
(Recovery. token password)) (Recovery. token password))

View file

@ -11,7 +11,7 @@
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]
[uxbox.util.router :as r] [uxbox.util.router :as r]
[uxbox.main.state :as st] [uxbox.main.state :as st]
[uxbox.util.schema :as sc] [uxbox.util.forms :as sc]
[uxbox.main.repo :as rp] [uxbox.main.repo :as rp]
[uxbox.main.data.projects :as dp] [uxbox.main.data.projects :as dp]
[uxbox.main.data.colors :as dc] [uxbox.main.data.colors :as dc]

View file

@ -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 <niwi@niwi.nz>
(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)))

View file

@ -12,7 +12,7 @@
[uxbox.util.router :as r] [uxbox.util.router :as r]
[uxbox.main.repo :as rp] [uxbox.main.repo :as rp]
[uxbox.util.i18n :refer (tr)] [uxbox.util.i18n :refer (tr)]
[uxbox.util.schema :as sc] [uxbox.util.forms :as sc]
[uxbox.main.data.pages :as udp] [uxbox.main.data.pages :as udp]
[uxbox.main.state :as st] [uxbox.main.state :as st]
[uxbox.util.datetime :as dt] [uxbox.util.datetime :as dt]
@ -35,7 +35,7 @@
(rs/emit! (fetch-page-history id) (rs/emit! (fetch-page-history id)
(fetch-pinned-page-history id)))] (fetch-pinned-page-history id)))]
(as-> rs/stream $ (as-> rs/stream $
(rx/filter udp/page-synced? $) (rx/filter udp/page-persisted? $)
(rx/delay 500 $) (rx/delay 500 $)
(rx/map (comp :id :page) $) (rx/map (comp :id :page) $)
(rx/on-value $ on-value)))) (rx/on-value $ on-value))))
@ -137,7 +137,7 @@
rs/WatchEvent rs/WatchEvent
(-apply-watch [_ state s] (-apply-watch [_ state s]
(rx/of (udp/update-page id)))) (rx/of (udp/persist-page id))))
(defn apply-selected-history (defn apply-selected-history
[id] [id]

View file

@ -3,21 +3,43 @@
;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; ;;
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.data.pages (ns uxbox.main.data.pages
(:require [cuerdas.core :as str] (:require [cljs.spec :as s]
[cuerdas.core :as str]
[beicon.core :as rx] [beicon.core :as rx]
[lentes.core :as l] [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.rstore :as rs]
[uxbox.util.router :as r] [uxbox.util.router :as r]
[uxbox.main.repo :as rp]
[uxbox.util.i18n :refer (tr)] [uxbox.util.i18n :refer (tr)]
[uxbox.util.schema :as sc] [uxbox.util.forms :as sc]
[uxbox.main.state :as st]
[uxbox.util.datetime :as dt] [uxbox.util.datetime :as dt]
[uxbox.util.data :refer (without-keys replace-by-id)])) [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 ;; --- Protocols
@ -99,75 +121,84 @@
[projectid] [projectid]
(FetchPages. 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 ;; --- Create Page
(defrecord CreatePage [name project metadata] (defrecord CreatePage [name project width height layout]
rs/WatchEvent rs/WatchEvent
(-apply-watch [this state s] (-apply-watch [this state s]
(letfn [(on-created [{page :payload}]
(rx/of
#(assoc-page % page)
#(assoc-packed-page % page)))]
(let [params {:name name (let [params {:name name
:project project :project project
:data {} :data {}
:metadata metadata}] :metadata {:width width
:height height
:layout layout}}]
(->> (rp/req :create/page params) (->> (rp/req :create/page params)
(rx/mapcat on-created)))))) (rx/map :payload)
(rx/map page-created)))))
(def ^:private create-page-schema (s/def ::create-page-event
{:name [sc/required sc/string] (s/keys :req-un [::name ::project ::width ::height ::layout]))
:metadata [sc/required]
:project [sc/required sc/uuid]})
(defn create-page (defn create-page
[data] [data]
(-> (sc/validate! data create-page-schema) {:pre [(us/valid? ::create-page-event data)]}
(map->CreatePage))) (map->CreatePage data))
;; --- Page Synced ;; --- Page Persisted
(defrecord PageSynced [page] (defrecord PagePersisted [data]
rs/UpdateEvent rs/UpdateEvent
(-apply-update [this state] (-apply-update [_ state]
(-> state (assoc-page state data)))
(assoc-in [:pages (:id page) :version] (:version page))
(assoc-page page))))
(defn- page-synced? (defn- page-persisted?
[event] [event]
(instance? PageSynced event)) (instance? PagePersisted event))
;; --- Sync Page ;; TODO: add specs
(defrecord SyncPage [id] (defn page-persisted
rs/WatchEvent [data]
(-apply-watch [this state s] {:pre [(map? data)]}
(let [page (pack-page state id)] (PagePersisted. data))
(->> (rp/req :update/page page)
(rx/map (comp ->PageSynced :payload))))))
(defn sync-page ;; --- Persist Page
[id]
(SyncPage. id))
;; --- Update Page (defrecord PersistPage [id]
(defrecord UpdatePage [id]
rs/WatchEvent rs/WatchEvent
(-apply-watch [this state s] (-apply-watch [this state s]
(let [page (get-in state [:pages id])] (let [page (get-in state [:pages id])]
(if (:history page) (if (:history page)
(rx/empty) (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] [v]
(instance? UpdatePage v)) (instance? PersistPage v))
(defn update-page (defn persist-page
[id] [id]
(UpdatePage. id)) (PersistPage. id))
(defn watch-page-changes (defn watch-page-changes
"A function that starts watching for `IPageUpdate` "A function that starts watching for `IPageUpdate`
@ -182,59 +213,82 @@
(as-> rs/stream $ (as-> rs/stream $
(rx/filter #(satisfies? IPageUpdate %) $) (rx/filter #(satisfies? IPageUpdate %) $)
(rx/debounce 1000 $) (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 ;; that does not sends the heavyweiht `:data` attribute
;; and only serves for update other page data. ;; and only serves for update other page data.
;; TODO: sync also with the pagedata-by-id index. (defrecord PersistMetadata [id]
(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)))
rs/WatchEvent rs/WatchEvent
(-apply-watch [this state s] (-apply-watch [_ state stream]
(letfn [(on-success [{page :payload}] (let [page (get-in state [:pages id])]
#(assoc-in % [:pages id :version] (:version page)))] (->> (rp/req :update/page-metadata page)
(->> (rp/req :update/page-metadata (into {} this)) (rx/map :payload)
(rx/map on-success))))) (rx/map metadata-persisted)))))
(def ^:private update-page-schema (defn persist-metadata
{:id [sc/required] [id]
:project [sc/required] {:pre [(uuid? id)]}
:version [sc/required] (PersistMetadata. id))
: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)))
;; --- Update Page Options ;; --- 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 rs/WatchEvent
(-apply-watch [this state s] (-apply-watch [this state s]
(let [page (get-in state [:pages id]) (rx/of (persist-metadata id))))
page (assoc page :options options)]
(rx/of (map->UpdatePageMetadata page)))))
(defn update-page-options (defn update-metadata
[id options] [id metadata]
(UpdatePageOptions. id options)) {: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) ;; --- Delete Page (by id)

View file

@ -14,7 +14,7 @@
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]
[uxbox.util.router :as r] [uxbox.util.router :as r]
[uxbox.util.i18n :refer (tr)] [uxbox.util.i18n :refer (tr)]
[uxbox.util.schema :as sc] [uxbox.util.forms :as sc]
[uxbox.main.data.pages :as udp])) [uxbox.main.data.pages :as udp]))
;; --- Helpers ;; --- Helpers
@ -25,6 +25,7 @@
(let [page {:id (:page-id project) (let [page {:id (:page-id project)
:name (:page-name project) :name (:page-name project)
:version (:page-version project) :version (:page-version project)
:project (:id project)
:data (:page-data project) :data (:page-data project)
:created-at (:page-created-at project) :created-at (:page-created-at project)
:modified-at (:page-modified-at project) :modified-at (:page-modified-at project)
@ -176,7 +177,6 @@
:width [sc/required sc/integer] :width [sc/required sc/integer]
:height [sc/required sc/integer] :height [sc/required sc/integer]
:layout [sc/required sc/string]}) :layout [sc/required sc/string]})
(defn create-project (defn create-project
[params] [params]
(-> (sc/validate! params create-project-schema) (-> (sc/validate! params create-project-schema)

View file

@ -10,7 +10,7 @@
[uxbox.util.uuid :as uuid] [uxbox.util.uuid :as uuid]
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]
[uxbox.util.router :as r] [uxbox.util.router :as r]
[uxbox.util.schema :as sc] [uxbox.util.forms :as sc]
[uxbox.util.workers :as uw] [uxbox.util.workers :as uw]
[uxbox.main.constants :as c] [uxbox.main.constants :as c]
[uxbox.main.geom :as geom] [uxbox.main.geom :as geom]
@ -594,6 +594,6 @@
(defn align-point (defn align-point
[point] [point]
(let [message {:cmd :grid/align :point point}] (let [message {:cmd :grid-align :point point}]
(->> (uw/ask! worker message) (->> (uw/ask! worker message)
(rx/map :point)))) (rx/map :point))))

View file

@ -5,15 +5,20 @@
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.data.users (ns uxbox.main.data.users
(:require [beicon.core :as rx] (:require [cljs.spec :as s]
[uxbox.main.repo :as rp] [beicon.core :as rx]
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]
[uxbox.main.state :as st] [uxbox.util.spec :as us]
[uxbox.util.schema :as sc]
[uxbox.util.i18n :refer (tr)] [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])) [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 ;; --- Profile Fetched
(defrecord ProfileFetched [data] (defrecord ProfileFetched [data]
@ -55,37 +60,31 @@
;; --- Update Profile ;; --- Update Profile
(defrecord UpdateProfile [data] (defrecord UpdateProfile [data on-success on-error]
rs/WatchEvent rs/WatchEvent
(-apply-watch [_ state s] (-apply-watch [_ state s]
(letfn [(on-error [{payload :payload}] (letfn [(handle-error [{payload :payload}]
(->> (:payload payload) (on-error payload)
(udf/assign-errors :profile/main) (rx/empty))]
(rx/of)))]
(->> (rp/req :update/profile data) (->> (rp/req :update/profile data)
(rx/map :payload) (rx/map :payload)
(rx/do on-success)
(rx/map profile-updated) (rx/map profile-updated)
(rx/catch rp/client-error? on-error))))) (rx/catch rp/client-error? handle-error)))))
(def update-profile-schema (s/def ::update-profile-event
{:fullname [sc/required sc/string] (s/keys :req-un [::fullname ::email ::username ::theme]))
:email [sc/required sc/email]
:username [sc/required sc/string]})
(defn update-profile (defn update-profile
[data] [data on-success on-error]
(let [[errors data] (sc/validate data update-profile-schema)] {:pre [(us/valid? ::update-profile-event data)
(if errors (fn? on-error)
(udf/assign-errors :profile/main errors) (fn? on-success)]}
(UpdateProfile. data)))) (UpdateProfile. data on-success on-error))
;; --- Password Updated ;; --- Password Updated
(defrecord PasswordUpdated [] (defrecord PasswordUpdated []
rs/UpdateEvent
(-apply-update [_ state]
(assoc-in state [:forms :profile/password] {}))
rs/EffectEvent rs/EffectEvent
(-apply-effect [_ state] (-apply-effect [_ state]
(udm/info! (tr "settings.password-saved")))) (udm/info! (tr "settings.password-saved"))))
@ -99,28 +98,22 @@
(defrecord UpdatePassword [data] (defrecord UpdatePassword [data]
rs/WatchEvent rs/WatchEvent
(-apply-watch [_ state s] (-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) (let [params {:old-password (:old-password data)
:password (:password-1 data)}] :password (:password-1 data)}]
(->> (rp/req :update/profile-password params) (->> (rp/req :update/profile-password params)
(rx/map password-updated) (rx/map password-updated)))))
(rx/catch rp/client-error? on-error))))))
(def update-password-schema (s/def ::password-1 string?)
[[:password-1 sc/required sc/string [sc/min-len 6]] (s/def ::password-2 string?)
[:password-2 sc/required sc/string (s/def ::old-password string?)
[sc/identical-to :password-1 :message "errors.form.password-not-match"]]
[:old-password sc/required sc/string]]) (s/def ::update-password-event
(s/keys :req-un [::password-1 ::password-2 ::old-password]))
(defn update-password (defn update-password
[data] [data]
(let [[errors data] (sc/validate data update-password-schema)] {:pre [(us/valid? ::update-password-event data)]}
(if errors (UpdatePassword. data))
(udf/assign-errors :profile/password errors)
(UpdatePassword. data))))
;; --- Update Photo ;; --- Update Photo
@ -132,7 +125,7 @@
(rx/map fetch-profile)))) (rx/map fetch-profile))))
(defn update-photo (defn update-photo
([file] ([file] (update-photo file (constantly nil)))
(UpdatePhoto. file (constantly nil)))
([file done] ([file done]
{:pre [(us/file? file) (fn? done)]}
(UpdatePhoto. file done))) (UpdatePhoto. file done)))

View file

@ -5,11 +5,13 @@
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.data.workspace (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.util.uuid :as uuid]
[uxbox.main.constants :as c] [uxbox.main.constants :as c]
[uxbox.util.rstore :as rs] [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.geom.point :as gpt]
[uxbox.util.workers :as uw] [uxbox.util.workers :as uw]
[uxbox.main.state :as st] [uxbox.main.state :as st]
@ -18,7 +20,6 @@
[uxbox.main.data.pages :as udp] [uxbox.main.data.pages :as udp]
[uxbox.main.data.shapes :as uds] [uxbox.main.data.shapes :as uds]
[uxbox.main.data.shapes-impl :as shimpl] [uxbox.main.data.shapes-impl :as shimpl]
[uxbox.main.data.forms :as udf]
[uxbox.main.data.lightbox :as udl] [uxbox.main.data.lightbox :as udl]
[uxbox.main.data.history :as udh] [uxbox.main.data.history :as udh]
[uxbox.util.datetime :as dt] [uxbox.util.datetime :as dt]
@ -210,44 +211,32 @@
(-apply-watch [_ state s] (-apply-watch [_ state s]
(let [page (get-in state [:pages id]) (let [page (get-in state [:pages id])
opts (:options page) opts (:options page)
message {:cmd :grid/init message {:cmd :grid-init
:width c/viewport-width :width c/viewport-width
:height c/viewport-height :height c/viewport-height
:x-axis (:grid/x-axis opts c/grid-x-axis) :x-axis (:grid-x-axis opts c/grid-x-axis)
:y-axis (:grid/y-axis opts c/grid-y-axis)}] :y-axis (:grid-y-axis opts c/grid-y-axis)}]
(rx/merge (rx/merge
(->> (uw/send! worker message) (->> (uw/send! worker message)
(rx/map #(activate-flag :grid/indexed))) (rx/map #(activate-flag :grid-indexed)))
(when (:grid/alignment opts) (when (:grid-alignment opts)
(rx/of (activate-flag :grid/alignment))))))) (rx/of (activate-flag :grid-alignment)))))))
(defn initialize-alignment-index (defn initialize-alignment-index
[id] [id]
(InitializeAlignmentIndex. 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 rs/WatchEvent
(-apply-watch [_ state s] (-apply-watch [_ state s]
(rx/of (udp/update-page-options id options) (rx/of (udp/update-metadata id metadata)
(initialize-alignment-index id) (initialize-alignment-index id))))
(udf/clean :workspace/settings)))
rs/EffectEvent (defn update-metadata
(-apply-effect [_ state] [id metadata]
(udl/close!))) {:pre [(uuid? id) (us/valid? ::udp/metadata metadata)]}
(UpdateMetadata. id metadata))
(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))))

View file

@ -11,38 +11,23 @@
[uxbox.main.repo.impl :refer (request send!)] [uxbox.main.repo.impl :refer (request send!)]
[uxbox.util.transit :as t])) [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 (defmethod request :fetch/pages
[type data] [type data]
(let [params {:url (str url "/pages") (let [params {:url (str url "/pages")
:method :get}] :method :get}]
(->> (send! params) (send! params)))
(rx/map decode-payload))))
(defmethod request :fetch/pages-by-project (defmethod request :fetch/pages-by-project
[type {:keys [project] :as params}] [type {:keys [project] :as params}]
(let [url (str url "/projects/" project "/pages")] (let [url (str url "/projects/" project "/pages")]
(->> (send! {:method :get :url url}) (send! {:method :get :url url})))
(rx/map decode-payload))))
(defmethod request :fetch/page-history (defmethod request :fetch/page-history
[type {:keys [page] :as params}] [type {:keys [page] :as params}]
(let [url (str url "/pages/" page "/history") (let [url (str url "/pages/" page "/history")
query (select-keys params [:max :since :pinned]) query (select-keys params [:max :since :pinned])
params {:method :get :url url :query query}] params {:method :get :url url :query query}]
(->> (send! params) (send! params)))
(rx/map decode-payload))))
(defmethod request :delete/page (defmethod request :delete/page
[_ id] [_ id]
@ -51,41 +36,30 @@
:method :delete}))) :method :delete})))
(defmethod request :create/page (defmethod request :create/page
[type {:keys [data metadata] :as body}] [type body]
(let [body (assoc body (let [params {:url (str url "/pages")
:data (t/encode data)
:metadata (t/encode metadata))
params {:url (str url "/pages")
:method :post :method :post
:body body}] :body body}]
(->> (send! params) (send! params)))
(rx/map decode-payload))))
(defmethod request :update/page (defmethod request :update/page
[type {:keys [id data metadata] :as body}] [type {:keys [id] :as body}]
(let [body (assoc body (let [params {:url (str url "/pages/" id)
:data (t/encode data)
:metadata (t/encode metadata))
params {:url (str url "/pages/" id)
:method :put :method :put
:body body}] :body body}]
(->> (send! params) (send! params)))
(rx/map decode-payload))))
(defmethod request :update/page-history (defmethod request :update/page-history
[type {:keys [id page] :as data}] [type {:keys [id page] :as data}]
(let [params {:url (str url "/pages/" page "/history/" id) (let [params {:url (str url "/pages/" page "/history/" id)
:method :put :method :put
:body data}] :body data}]
(->> (send! params) (send! params)))
(rx/map decode-payload))))
(defmethod request :update/page-metadata (defmethod request :update/page-metadata
[type {:keys [id metadata] :as body}] [type {:keys [id metadata] :as body}]
(let [body (dissoc body :data) (let [body (dissoc body :data)
body (assoc body :metadata (t/encode metadata))
params {:url (str url "/pages/" id "/metadata") params {:url (str url "/pages/" id "/metadata")
:method :put :method :put
:body body}] :body body}]
(->> (send! params) (send! params)))
(rx/map decode-payload))))

View file

@ -14,27 +14,15 @@
(defmethod request :fetch/projects (defmethod request :fetch/projects
[type data] [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 ;; Obtain the list of projects and decode the embedded
;; page data in order to have it usable. ;; page data in order to have it usable.
(->> (send! {:url (str url "/projects") (send! {:url (str url "/projects")
:method :get}) :method :get}))
(rx/map decode-payload))))
(defmethod request :fetch/project-by-token (defmethod request :fetch/project-by-token
[_ token] [_ token]
(letfn [(decode-pages [response] (send! {:url (str url "/projects-by-token/" token)
(let [pages (->> (get-in response [:payload :pages]) :method :get}))
(mapv pages/decode-page))]
(assoc-in response [:payload :pages] pages)))]
(->> (send! {:url (str url "/projects-by-token/" token)
:method :get})
(rx/map decode-pages))))
(defmethod request :create/project (defmethod request :create/project
[_ data] [_ data]

View file

@ -11,27 +11,18 @@
[uxbox.main.repo.impl :refer (request send!)] [uxbox.main.repo.impl :refer (request send!)]
[uxbox.util.transit :as t])) [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 (defmethod request :fetch/profile
[type _] [type _]
(let [url (str url "/profile/me") (let [url (str url "/profile/me")
params {:method :get :url url}] params {:method :get :url url}]
(->> (send! params) (send! params)))
(rx/map decode-payload))))
(defmethod request :update/profile (defmethod request :update/profile
[type {:keys [metadata] :as body}] [type body]
(let [body (assoc body :metadata (t/encode metadata)) (let [params {:url (str url "/profile/me")
params {:url (str url "/profile/me")
:method :put :method :put
:body body}] :body body}]
(->> (send! params) (send! params)))
(rx/map decode-payload))))
(defmethod request :update/profile-password (defmethod request :update/profile-password
[type data] [type data]

View file

@ -138,6 +138,8 @@
(def routes (def routes
[["/auth/login" :auth/login] [["/auth/login" :auth/login]
["/auth/register" :auth/register]
["/auth/recovery/request" :auth/recovery-request]
["/auth/recovery/token/:token" :auth/recovery] ["/auth/recovery/token/:token" :auth/recovery]
["/settings/profile" :settings/profile] ["/settings/profile" :settings/profile]
["/settings/password" :settings/password] ["/settings/password" :settings/password]
@ -145,7 +147,6 @@
["/dashboard/projects" :dashboard/projects] ["/dashboard/projects" :dashboard/projects]
["/dashboard/elements" :dashboard/elements] ["/dashboard/elements" :dashboard/elements]
["/dashboard/icons" :dashboard/icons] ["/dashboard/icons" :dashboard/icons]
["/dashboard/icons/:type/:id" :dashboard/icons] ["/dashboard/icons/:type/:id" :dashboard/icons]
["/dashboard/icons/:type" :dashboard/icons] ["/dashboard/icons/:type" :dashboard/icons]

View file

@ -5,74 +5,22 @@
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui.auth.login (ns uxbox.main.ui.auth.login
(:require [sablono.core :as html :refer-macros [html]] (:require [lentes.core :as l]
[lentes.core :as l]
[cuerdas.core :as str] [cuerdas.core :as str]
[rum.core :as rum]
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[uxbox.main.state :as st] [uxbox.util.dom :as dom]
[uxbox.util.rstore :as rs] [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.auth :as da]
[uxbox.main.data.messages :as udm] [uxbox.main.data.messages :as udm]
[uxbox.util.dom :as dom]
[uxbox.main.ui.icons :as i] [uxbox.main.ui.icons :as i]
[uxbox.main.ui.messages :as uum] [uxbox.main.ui.messages :as uum]
[uxbox.main.ui.navigation :as nav] [uxbox.main.ui.navigation :as nav]))
[uxbox.util.mixins :as mx :include-macros true]))
(defn- login-submit (def form-data (forms/focus-data :login st/state))
[event local] (def set-value! (partial forms/set-value! :login))
(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?"]]]]]])))
(defn- login-page-will-mount (defn- login-page-will-mount
[own] [own]
@ -80,9 +28,54 @@
(rt/go :dashboard/projects)) (rt/go :dashboard/projects))
own) own)
(def login-page (def +login-form+
(mx/component {:email [forms/required forms/string]
{:render #(login-page-render % (:rum/local %)) :password [forms/required forms/string]})
:will-mount login-page-will-mount
:name "login-page" (mx/defc login-form
:mixins [(mx/local)]})) {: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)]])

View file

@ -5,65 +5,49 @@
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui.auth.recovery (ns uxbox.main.ui.auth.recovery
(:require [sablono.core :as html :refer-macros [html]] (:require [lentes.core :as l]
[lentes.core :as l]
[cuerdas.core :as str] [cuerdas.core :as str]
[rum.core :as rum]
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[uxbox.main.state :as st] [uxbox.main.state :as st]
[uxbox.util.rstore :as rs] [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.auth :as uda]
[uxbox.main.data.messages :as udm] [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.icons :as i]
[uxbox.main.ui.messages :as uum] [uxbox.main.ui.messages :as uum]
[uxbox.main.ui.navigation :as nav] [uxbox.main.ui.navigation :as nav]))
[uxbox.util.mixins :as mx :include-macros true]
[uxbox.util.dom :as dom]))
;; --- Constants ;; --- Recovery Form
(def form-data (def form-data (forms/focus-data :recovery st/state))
(-> (l/in [:forms :recovery]) (def set-value! (partial forms/set-value! :recovery))
(l/derive st/state)))
(def form-errors (def +recovery-form+
(-> (l/in [:errors :recovery]) {:password [forms/required forms/string]})
(l/derive st/state)))
(def set-value! (mx/defc recovery-form
(partial udf/assign-field-value :recovery)) {:mixins [mx/static mx/reactive]}
[token]
;; --- Recovery Request Form (let [data (merge (mx/react form-data)
{:token token})
(def schema valid? (forms/valid? data +recovery-form+)]
{: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)]
(letfn [(on-change [field event] (letfn [(on-change [field event]
(let [value (dom/event->value event)] (let [value (dom/event->value event)]
(rs/emit! (set-value! field value)))) (set-value! field value)))
(on-submit [event] (on-submit [event]
(dom/prevent-default event) (dom/prevent-default event)
(rs/emit! (uda/recovery (assoc form :token token))))] (rs/emit! (uda/recovery data)
(html (forms/clear :recovery)))]
[:form {:on-submit on-submit} [:form {:on-submit on-submit}
[:div.login-content [:div.login-content
[:input.input-text [:input.input-text
{:name "password" {:name "password"
:value (:password form "") :value (:password data "")
:on-change (partial on-change :password) :on-change (partial on-change :password)
:placeholder "Password" :placeholder "Password"
:type "password"}] :type "password"}]
(forms/input-error errors :password)
[:input.btn-primary [:input.btn-primary
{:name "login" {:name "login"
:class (when-not valid? "btn-disabled") :class (when-not valid? "btn-disabled")
@ -71,15 +55,9 @@
:value "Recover password" :value "Recover password"
:type "submit"}] :type "submit"}]
[:div.login-links [:div.login-links
[:a {:on-click #(rt/go :auth/login)} "Go back!"]]]])))) [:a {:on-click #(rt/go :auth/login)} "Go back!"]]]])))
(def form ;; --- Recovery Page
(mx/component
{:render form-render
:name "form"
:mixins [mx/static mx/reactive]}))
;; --- Recovery Request Page
(defn- recovery-page-will-mount (defn- recovery-page-will-mount
[own] [own]
@ -87,18 +65,13 @@
(rs/emit! (uda/validate-recovery-token token)) (rs/emit! (uda/validate-recovery-token token))
own)) own))
(defn- recovery-page-render (mx/defc recovery-page
[own token] {:mixins [mx/static]
(html :will-mount recovery-page-will-mount
:will-unmount (forms/cleaner-fn :recovery)}
[token]
[:div.login [:div.login
[:div.login-body [:div.login-body
(uum/messages) (uum/messages)
[:a i/logo] [:a i/logo]
(form token)]])) (recovery-form token)]])
(def recovery-page
(mx/component
{:render recovery-page-render
:will-mount recovery-page-will-mount
:name "recovery-page"
:mixins [mx/static]}))

View file

@ -5,64 +5,46 @@
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui.auth.recovery-request (ns uxbox.main.ui.auth.recovery-request
(:require [sablono.core :as html :refer-macros [html]] (:require [lentes.core :as l]
[lentes.core :as l]
[cuerdas.core :as str] [cuerdas.core :as str]
[rum.core :as rum]
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[uxbox.main.state :as st]
[uxbox.util.rstore :as rs] [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.auth :as uda]
[uxbox.main.data.messages :as udm] [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.icons :as i]
[uxbox.main.ui.messages :as uum] [uxbox.main.ui.messages :as uum]
[uxbox.main.ui.navigation :as nav] [uxbox.main.ui.navigation :as nav]))
[uxbox.util.mixins :as mx :include-macros true]
[uxbox.util.dom :as dom]))
;; --- 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 (def +recovery-request-form+
(-> (l/in [:forms :recovery-request]) {:username [forms/required forms/string]})
(l/derive st/state)))
(def form-errors (mx/defc recovery-request-form
(-> (l/in [:errors :recovery-request]) {:mixins [mx/static mx/reactive]}
(l/derive st/state))) []
(let [data (mx/react form-data)
(def set-value! valid? (forms/valid? data +recovery-request-form+)]
(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)]
(letfn [(on-change [field event] (letfn [(on-change [field event]
(let [value (dom/event->value event)] (let [value (dom/event->value event)]
(rs/emit! (set-value! field value)))) (set-value! field value)))
(on-submit [event] (on-submit [event]
(dom/prevent-default event) (dom/prevent-default event)
(rs/emit! (uda/recovery-request form)))] (rs/emit! (uda/recovery-request data)
(html (forms/clear :recovery-request)))]
[:form {:on-submit on-submit} [:form {:on-submit on-submit}
[:div.login-content [:div.login-content
[:input.input-text [:input.input-text
{:name "username" {:name "username"
:value (:username form "") :value (:username data "")
:on-change (partial on-change :username) :on-change (partial on-change :username)
:placeholder "username or email address" :placeholder "username or email address"
:type "text"}] :type "text"}]
(forms/input-error errors :username)
[:input.btn-primary [:input.btn-primary
{:name "login" {:name "login"
:class (when-not valid? "btn-disabled") :class (when-not valid? "btn-disabled")
@ -70,27 +52,16 @@
:value "Recover password" :value "Recover password"
:type "submit"}] :type "submit"}]
[:div.login-links [:div.login-links
[:a {:on-click #(rt/go :auth/login)} "Go back!"]]]])))) [: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 Request Page
(defn- recovery-request-page-render (mx/defc recovery-request-page
[own] {:mixins [mx/static]
(html :will-unmount (forms/cleaner-fn :recovery-request)}
[]
[:div.login [:div.login
[:div.login-body [:div.login-body
(uum/messages) (uum/messages)
[:a i/logo] [:a i/logo]
(form)]])) (recovery-request-form)]])
(def recovery-request-page
(mx/component
{:render recovery-request-page-render
:name "recovery-request-page"
:mixins [mx/static]}))

View file

@ -5,56 +5,56 @@
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui.auth.register (ns uxbox.main.ui.auth.register
(:require [sablono.core :as html :refer-macros [html]] (:require [lentes.core :as l]
[lentes.core :as l]
[cuerdas.core :as str] [cuerdas.core :as str]
[rum.core :as rum]
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[uxbox.main.state :as st]
[uxbox.util.rstore :as rs] [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.auth :as uda]
[uxbox.main.data.messages :as udm] [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.icons :as i]
[uxbox.main.ui.messages :as uum] [uxbox.main.ui.messages :as uum]
[uxbox.main.ui.navigation :as nav] [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))
;; --- Register Form ;; --- Register Form
(defn- register-form-render (def form-data (forms/focus-data :register st/state))
[own] (def form-errors (forms/focus-errors :register st/state))
(let [form (mx/react form-data) (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) errors (mx/react form-errors)
valid? (us/valid? form uda/register-schema)] valid? (forms/valid? data +register-form+)]
(letfn [(on-change [field event] (letfn [(on-change [field event]
(let [value (dom/event->value 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] (on-submit [event]
(dom/prevent-default event) (dom/prevent-default event)
(rs/emit! (uda/register form)))] (rs/emit! (uda/register data on-error)))]
(html
[:form {:on-submit on-submit} [:form {:on-submit on-submit}
[:div.login-content [:div.login-content
[:input.input-text [:input.input-text
{:name "fullname" {:name "fullname"
:value (:fullname form "") :value (:fullname data "")
:on-change (partial on-change :fullname) :on-change (partial on-change :fullname)
:placeholder "Full Name" :placeholder "Full Name"
:type "text"}] :type "text"}]
@ -62,7 +62,7 @@
[:input.input-text [:input.input-text
{:name "username" {:name "username"
:value (:username form "") :value (:username data "")
:on-change (partial on-change :username) :on-change (partial on-change :username)
:placeholder "Username" :placeholder "Username"
:type "text"}] :type "text"}]
@ -71,7 +71,7 @@
[:input.input-text [:input.input-text
{:name "email" {:name "email"
:ref "email" :ref "email"
:value (:email form "") :value (:email data "")
:on-change (partial on-change :email) :on-change (partial on-change :email)
:placeholder "Email" :placeholder "Email"
:type "text"}] :type "text"}]
@ -80,7 +80,7 @@
[:input.input-text [:input.input-text
{:name "password" {:name "password"
:ref "password" :ref "password"
:value (:password form "") :value (:password data "")
:on-change (partial on-change :password) :on-change (partial on-change :password)
:placeholder "Password" :placeholder "Password"
:type "password"}] :type "password"}]
@ -94,27 +94,15 @@
:type "submit"}] :type "submit"}]
[:div.login-links [:div.login-links
;; [:a {:on-click #(rt/go :auth/recover-password)} "Forgot your password?"] ;; [:a {:on-click #(rt/go :auth/recover-password)} "Forgot your password?"]
[:a {:on-click #(rt/go :auth/login)} "Already have an account?"]]]])))) [: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]}))
;; --- Register Page ;; --- Register Page
(defn- register-page-render (mx/defc register-page
{:mixins [mx/static]}
[own] [own]
(html
[:div.login [:div.login
[:div.login-body [:div.login-body
(uum/messages) (uum/messages)
[:a i/logo] [:a i/logo]
(register-form)]])) (register-form)]])
(def register-page
(mx/component
{:render register-page-render
:name "register-page"
:mixins [mx/static]}))

View file

@ -9,7 +9,7 @@
[rum.core :as rum] [rum.core :as rum]
[lentes.core :as l] [lentes.core :as l]
[goog.events :as events] [goog.events :as events]
[uxbox.util.schema :as sc] [uxbox.util.forms :as sc]
[uxbox.util.mixins :as mx :include-macros true] [uxbox.util.mixins :as mx :include-macros true]
[uxbox.util.math :as mth] [uxbox.util.math :as mth]
[uxbox.util.data :as data] [uxbox.util.data :as data]

View file

@ -15,7 +15,6 @@
[uxbox.main.ui.messages :as uum] [uxbox.main.ui.messages :as uum]
[uxbox.main.ui.colorpicker :refer (colorpicker)] [uxbox.main.ui.colorpicker :refer (colorpicker)]
[uxbox.main.ui.dashboard.header :refer (header)] [uxbox.main.ui.dashboard.header :refer (header)]
[uxbox.main.ui.forms :as form]
[uxbox.main.ui.icons :as i] [uxbox.main.ui.icons :as i]
[uxbox.main.ui.keyboard :as k] [uxbox.main.ui.keyboard :as k]
[uxbox.main.ui.lightbox :as lbx] [uxbox.main.ui.lightbox :as lbx]
@ -24,8 +23,7 @@
[uxbox.util.i18n :as t :refer (tr)] [uxbox.util.i18n :as t :refer (tr)]
[uxbox.util.lens :as ul] [uxbox.util.lens :as ul]
[uxbox.util.mixins :as mx :include-macros true] [uxbox.util.mixins :as mx :include-macros true]
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]))
[uxbox.util.schema :as sc]))
;; --- Refs ;; --- Refs

View file

@ -21,7 +21,7 @@
[uxbox.util.mixins :as mx :include-macros true] [uxbox.util.mixins :as mx :include-macros true]
[uxbox.util.datetime :as dt] [uxbox.util.datetime :as dt]
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]
[uxbox.util.schema :as sc] [uxbox.util.forms :as sc]
[uxbox.util.lens :as ul] [uxbox.util.lens :as ul]
[uxbox.util.i18n :refer (tr)] [uxbox.util.i18n :refer (tr)]
[uxbox.util.dom :as dom])) [uxbox.util.dom :as dom]))

View file

@ -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"))

View file

@ -6,69 +6,64 @@
;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.settings.password (ns uxbox.main.ui.settings.password
(:require [sablono.core :as html :refer-macros [html]] (:require [lentes.core :as l]
[rum.core :as rum]
[lentes.core :as l]
[cuerdas.core :as str] [cuerdas.core :as str]
[uxbox.util.schema :as sc]
[uxbox.main.state :as st]
[uxbox.util.i18n :as t :refer (tr)] [uxbox.util.i18n :as t :refer (tr)]
[uxbox.util.router :as r]
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]
[uxbox.main.data.users :as udu] [uxbox.util.forms :as forms]
[uxbox.main.data.forms :as udf] [uxbox.util.dom :as dom]
[uxbox.main.ui.icons :as i]
[uxbox.main.ui.forms :as forms]
[uxbox.main.ui.messages :as uum]
[uxbox.util.mixins :as mx :include-macros true] [uxbox.util.mixins :as mx :include-macros true]
[uxbox.main.ui.settings.header :refer (header)] [uxbox.main.state :as st]
[uxbox.util.dom :as dom])) [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 (def +password-form+
(-> (l/in [:forms :profile/password]) [[:password-1 forms/required forms/string [forms/min-len 6]]
(l/derive st/state))) [: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 (mx/defc password-form
(-> (l/in [:errors :profile/password]) {:mixins [mx/reactive mx/static]}
(l/derive st/state))) []
(let [data (mx/react form-data)
(def assign-field-value errors (mx/react form-errors)
(partial udf/assign-field-value :profile/password)) valid? (forms/valid? data +password-form+)]
(letfn [(on-change [field event]
(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]
(let [value (dom/event->value event)] (let [value (dom/event->value event)]
(rs/emit! (assign-field-value field value)))) (set-value! field value)))
(on-submit [event] (on-submit [event]
(rs/emit! (udu/update-password form)))] (println "on-submit" data)
(html #_(rs/emit! (udu/update-password form)))]
(println "password-form" data)
[:form.password-form [:form.password-form
[:span.user-settings-label "Change password"] [:span.user-settings-label "Change password"]
[:input.input-text [:input.input-text
{:type "password" {:type "password"
:class (forms/error-class errors :old-password) :class (forms/error-class errors :old-password)
:value (:old-password form "") :value (:old-password data "")
:on-change (partial on-field-change :old-password) :on-change (partial on-change :old-password)
:placeholder "Old password"}] :placeholder "Old password"}]
(forms/input-error errors :old-password) (forms/input-error errors :old-password)
[:input.input-text [:input.input-text
{:type "password" {:type "password"
:class (forms/error-class errors :password-1) :class (forms/error-class errors :password-1)
:value (:password-1 form "") :value (:password-1 data "")
:on-change (partial on-field-change :password-1) :on-change (partial on-change :password-1)
:placeholder "New password"}] :placeholder "New password"}]
(forms/input-error errors :password-1) (forms/input-error errors :password-1)
[:input.input-text [:input.input-text
{:type "password" {:type "password"
:class (forms/error-class errors :password-2) :class (forms/error-class errors :password-2)
:value (:password-2 form "") :value (:password-2 data "")
:on-change (partial on-field-change :password-2) :on-change (partial on-change :password-2)
:placeholder "Confirm password"}] :placeholder "Confirm password"}]
(forms/input-error errors :password-2) (forms/input-error errors :password-2)
[:input.btn-primary [:input.btn-primary
@ -76,28 +71,16 @@
:class (when-not valid? "btn-disabled") :class (when-not valid? "btn-disabled")
:disabled (not valid?) :disabled (not valid?)
:on-click on-submit :on-click on-submit
:value "Update settings"}]])))) :value "Update settings"}]])))
(def password-form
(mx/component
{:render password-form-render
:name "password-form"
:mixins [mx/static (mx/local) mx/reactive]}))
;; --- Password Page ;; --- Password Page
(defn password-page-render (mx/defc password-page
[own] {:mixins [mx/static]}
(html []
[:main.dashboard-main [:main.dashboard-main
(header) (header)
(uum/messages) (uum/messages)
[:section.dashboard-content.user-settings [:section.dashboard-content.user-settings
[:section.user-settings-content [:section.user-settings-content
(password-form)]]])) (password-form)]]])
(def password-page
(mx/component
{:render password-page-render
:name "password-page"
:mixins [mx/static]}))

View file

@ -6,100 +6,102 @@
;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.settings.profile (ns uxbox.main.ui.settings.profile
(:require [sablono.core :as html :refer-macros [html]] (:require [cuerdas.core :as str]
[rum.core :as rum]
[cuerdas.core :as str]
[lentes.core :as l] [lentes.core :as l]
[uxbox.util.schema :as sc] [uxbox.util.forms :as forms]
[uxbox.util.router :as r] [uxbox.util.router :as r]
[uxbox.main.state :as st]
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]
[uxbox.main.ui.icons :as i]
[uxbox.util.mixins :as mx :include-macros true] [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.settings.header :refer (header)]
[uxbox.main.ui.messages :as uum] [uxbox.main.ui.messages :as uum]
[uxbox.main.data.users :as udu] [uxbox.main.data.users :as udu]))
[uxbox.main.data.forms :as udf]
[uxbox.util.interop :refer (iterable->seq)]
[uxbox.util.dom :as dom]))
;; --- 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 (def profile-ref
(-> (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
(-> (l/key :profile) (-> (l/key :profile)
(l/derive st/state))) (l/derive st/state)))
(def +profile-form+
{:fullname [forms/required forms/string]
:email [forms/required forms/email]
:username [forms/required forms/string]})
;; --- Profile Form ;; --- Profile Form
(defn profile-form-render (mx/defc profile-form
[own] {:mixins [mx/static mx/reactive]
(let [form (merge (mx/react profile-ref) :will-unmount (forms/cleaner-fn :profile)}
(mx/react formdata)) []
errors (mx/react formerrors) ;; TODO: properly persist theme
valid? (sc/valid? form udu/update-profile-schema) (let [data (merge {:theme "light"}
theme (get-in form [:metadata :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] (letfn [(on-change [field event]
(let [value (dom/event->value 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] (on-submit [event]
(rs/emit! (udu/update-profile form)))] (rs/emit! (udu/update-profile data on-success on-error)))]
(html
[:form.profile-form [:form.profile-form
[:span.user-settings-label "Name, username and email"] [:span.user-settings-label "Name, username and email"]
[:input.input-text [:input.input-text
{:type "text" {:type "text"
:on-change (partial on-change :fullname) :on-change (partial on-change :fullname)
:value (:fullname form "") :value (:fullname data "")
:placeholder "Your name"}] :placeholder "Your name"}]
(forms/input-error errors :fullname)
[:input.input-text [:input.input-text
{:type "text" {:type "text"
:on-change (partial on-change :username) :on-change (partial on-change :username)
:value (:username form "") :value (:username data "")
:placeholder "Your username"}] :placeholder "Your username"}]
(forms/input-error errors :username) (forms/input-error errors :username)
[:input.input-text [:input.input-text
{:type "email" {:type "email"
:on-change (partial on-change :email) :on-change (partial on-change :email)
:value (:email form "") :value (:email data "")
:placeholder "Your email"}] :placeholder "Your email"}]
(forms/input-error errors :email) (forms/input-error errors :email)
[:span.user-settings-label "Choose a color theme"] [:span.user-settings-label "Choose a color theme"]
[:div.input-radio.radio-primary [:div.input-radio.radio-primary
[:input {:type "radio" [:input {:type "radio"
:checked (= theme "light") :checked (when (= theme "light") "checked")
:on-change (partial on-change [:metadata :theme]) :on-change (partial on-change :theme)
:id "light-theme" :id "light-theme"
:name "theme" :name "theme"
:value "light"}] :value "light"}]
[:label {:for "light-theme"} "Light theme"] [:label {:for "light-theme"} "Light theme"]
[:input {:type "radio" [:input {:type "radio"
:checked (= theme "dark") :checked (when (= theme "dark") "checked")
:on-change (partial on-change [:metadata :theme]) :on-change (partial on-change :theme)
:id "dark-theme" :id "dark-theme"
:name "theme" :name "theme"
:value "dark"}] :value "dark"}]
[:label {:for "dark-theme"} "Dark theme"] [:label {:for "dark-theme"} "Dark theme"]
[:input {:type "radio" [:input {:type "radio"
:checked (= theme "high-contrast") :checked (when (= theme "high-contrast") "checked")
:on-change (partial on-change [:metadata :theme]) :on-change (partial on-change :theme)
:id "high-contrast-theme" :id "high-contrast-theme"
:name "theme" :name "theme"
:value "high-contrast"}] :value "high-contrast"}]
@ -110,18 +112,13 @@
:class (when-not valid? "btn-disabled") :class (when-not valid? "btn-disabled")
:disabled (not valid?) :disabled (not valid?)
:on-click on-submit :on-click on-submit
:value "Update settings"}]])))) :value "Update settings"}]])))
(def profile-form
(mx/component
{:render profile-form-render
:name "profile-form"
:mixins [(mx/local) mx/reactive mx/static]}))
;; --- Profile Photo Form ;; --- Profile Photo Form
(defn- profile-photo-form-render (mx/defc profile-photo-form
[own] {:mixins [mx/static mx/reactive]}
[]
(letfn [(on-change [event] (letfn [(on-change [event]
(let [target (dom/get-target event) (let [target (dom/get-target event)
file (-> (dom/get-files target) file (-> (dom/get-files target)
@ -133,24 +130,17 @@
photo (if (or (str/empty? photo) (nil? photo)) photo (if (or (str/empty? photo) (nil? photo))
"images/avatar.jpg" "images/avatar.jpg"
photo)] photo)]
(html
[:form.avatar-form [:form.avatar-form
[:img {:src photo :border "0"}] [:img {:src photo}]
[:input {:type "file" [:input {:type "file"
:value "" :value ""
:on-change on-change}]])))) :on-change on-change}]])))
(def profile-photo-form
(mx/component
{:render profile-photo-form-render
:name profile-photo-form
:mixins [mx/static mx/reactive]}))
;; --- Profile Page ;; --- Profile Page
(defn profile-page-render (mx/defc profile-page
[own] {:mixins [mx/static]}
(html []
[:main.dashboard-main [:main.dashboard-main
(header) (header)
(uum/messages) (uum/messages)
@ -158,11 +148,4 @@
[:section.user-settings-content [:section.user-settings-content
[:span.user-settings-label "Your avatar"] [:span.user-settings-label "Your avatar"]
(profile-photo-form) (profile-photo-form)
(profile-form) (profile-form)]]])
]]]))
(def profile-page
(mx/component
{:render profile-page-render
:name "profile-page"
:mixins [mx/static]}))

View file

@ -61,8 +61,8 @@
(def alignment-ref (def alignment-ref
(letfn [(getter [flags] (letfn [(getter [flags]
(and (contains? flags :grid/indexed) (and (contains? flags :grid-indexed)
(contains? flags :grid/alignment) (contains? flags :grid-alignment)
(contains? flags :grid)))] (contains? flags :grid)))]
(-> (l/lens getter) (-> (l/lens getter)
(l/derive flags-ref)))) (l/derive flags-ref))))

View file

@ -21,16 +21,16 @@
(defn- grid-render (defn- grid-render
[own] [own]
(let [options (:options (mx/react wb/page-ref)) (let [options (:options (mx/react wb/page-ref))
color (:grid/color options "#cccccc") color (:grid-color options "#cccccc")
width c/viewport-width width c/viewport-width
height c/viewport-height height c/viewport-height
x-ticks (range (- 0 c/canvas-start-x) x-ticks (range (- 0 c/canvas-start-x)
(- width 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) y-ticks (range (- 0 c/canvas-start-x)
(- height c/canvas-start-x) (- height c/canvas-start-x)
(:grid/y-axis options 10)) (:grid-y-axis options 10))
path (as-> [] $ path (as-> [] $
(reduce (partial vertical-line height) $ x-ticks) (reduce (partial vertical-line height) $ x-ticks)

View file

@ -6,60 +6,68 @@
;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.workspace.settings (ns uxbox.main.ui.workspace.settings
(:require [sablono.core :as html :refer-macros [html]] (:require [lentes.core :as l]
[lentes.core :as l]
[rum.core :as rum]
[uxbox.main.constants :as c] [uxbox.main.constants :as c]
[uxbox.main.state :as st] [uxbox.main.state :as st]
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]
[uxbox.main.data.pages :as udp] [uxbox.main.data.pages :as udp]
[uxbox.main.data.forms :as udf]
[uxbox.main.data.workspace :as udw] [uxbox.main.data.workspace :as udw]
[uxbox.main.data.lightbox :as udl] [uxbox.main.data.lightbox :as udl]
[uxbox.main.ui.icons :as i] [uxbox.main.ui.icons :as i]
[uxbox.util.forms :as forms]
[uxbox.util.mixins :as mx :include-macros true] [uxbox.util.mixins :as mx :include-macros true]
[uxbox.main.ui.forms :as forms]
[uxbox.main.ui.lightbox :as lbx] [uxbox.main.ui.lightbox :as lbx]
[uxbox.main.ui.colorpicker :as uucp] [uxbox.main.ui.colorpicker :as uucp]
[uxbox.main.ui.workspace.base :as wb] [uxbox.main.ui.workspace.base :as wb]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.data :refer (parse-int)])) [uxbox.util.data :refer (parse-int)]))
;; --- Lentes (def form-data (forms/focus-data :workspace-settings st/state))
(def form-errors (forms/focus-errors :workspace-settings st/state))
(def formdata (udf/focus-form-data :workspace/settings)) (def set-value! (partial forms/set-value! :workspace-settings))
(def formerrors (udf/focus-form-errors :workspace/settings)) (def set-errors! (partial forms/set-errors! :workspace-settings))
(def assign-field-value (partial udf/assign-field-value :workspace/settings)) (def page-ref wb/page-ref)
;; --- Form Component ;; --- Form Component
(def settings-form-defaults (def +settings-defaults+
{:grid/x-axis c/grid-x-axis {:grid-x-axis c/grid-x-axis
:grid/y-axis c/grid-y-axis :grid-y-axis c/grid-y-axis
:grid/color "#b5bdb9" :grid-color "#b5bdb9"
:grid/alignment false}) :grid-alignment false})
(defn- settings-form-render (def +settings-form+
[own] {:grid-y-axis [forms/required forms/integer [forms/in-range 2 100]]
(let [page (mx/react wb/page-ref) :grid-x-axis [forms/required forms/integer [forms/in-range 2 100]]
form (merge settings-form-defaults :grid-alignment [forms/boolean]
(:options page) :grid-color [forms/required forms/color]})
(mx/react formdata))
errors (mx/react formerrors)] (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] (letfn [(on-field-change [field event]
(let [value (dom/event->value event) (let [value (dom/event->value event)
value (parse-int value "")] value (parse-int value "")]
(rs/emit! (assign-field-value field value)))) (set-value! field value)))
(on-color-change [color] (on-color-change [color]
(rs/emit! (assign-field-value :grid/color color))) (set-value! :grid-color color))
(on-align-change [event] (on-align-change [event]
(let [checked? (-> (dom/get-target event) (let [checked? (-> (dom/get-target event)
(dom/checked?))] (dom/checked?))]
(rs/emit! (assign-field-value :grid/alignment checked?)))) (set-value! :grid-alignment checked?)))
(on-submit [event] (on-submit [event]
(dom/prevent-default event) (dom/prevent-default event)
(rs/emit! (udw/submit-workspace-settings (:id page) form)))] (let [[errors data] (forms/validate data +settings-form+)]
(html (if errors
(set-errors! errors)
(rs/emit! (udw/update-metadata id data)
(forms/clear :workspace-settings)
(udl/hide-lightbox)))))]
[:form {:on-submit on-submit} [:form {:on-submit on-submit}
[:span.lightbox-label "Grid size"] [:span.lightbox-label "Grid size"]
[:div.project-size [:div.project-size
@ -67,58 +75,45 @@
[:input#grid-x.input-text [:input#grid-x.input-text
{:placeholder "X" {:placeholder "X"
:type "number" :type "number"
:class (forms/error-class errors :grid/x-axis) :class (forms/error-class errors :grid-x-axis)
:value (:grid/x-axis form "") :value (:grid-x-axis data "")
:on-change (partial on-field-change :grid/x-axis) :on-change (partial on-field-change :grid-x-axis)
:min 1 :min 2
:max 100}]] :max 100}]]
[:div.input-element.pixels [:div.input-element.pixels
[:input#grid-y.input-text [:input#grid-y.input-text
{:placeholder "Y" {:placeholder "Y"
:type "number" :type "number"
:class (forms/error-class errors :grid/y-axis) :class (forms/error-class errors :grid-y-axis)
:value (:grid/y-axis form "") :value (:grid-y-axis data "")
:on-change (partial on-field-change :grid/y-axis) :on-change (partial on-field-change :grid-y-axis)
:min 1 :min 2
:max 100}]]] :max 100}]]]
[:span.lightbox-label "Grid color"] [:span.lightbox-label "Grid color"]
(uucp/colorpicker (uucp/colorpicker
:value (:grid/color form) :value (:grid-color data)
:on-change on-color-change) :on-change on-color-change)
[:span.lightbox-label "Grid magnet option"] [:span.lightbox-label "Grid magnet option"]
[:div.input-checkbox.check-primary [:div.input-checkbox.check-primary
[:input [:input
{:type "checkbox" {:type "checkbox"
:on-change on-align-change :on-change on-align-change
:checked (:grid/alignment form) :checked (:grid-alignment data)
:id "magnet" :id "magnet"
:value "Yes"}] :value "Yes"}]
[:label {:for "magnet"} "Activate magnet"]] [:label {:for "magnet"} "Activate magnet"]]
[:input.btn-primary [:input.btn-primary
{:type "submit" {:type "submit"
:value "Save"}]])))) :value "Save"}]])))
(def settings-form (mx/defc settings-dialog
(mx/component
{:render settings-form-render
:name "settings-form"
:mixins [(mx/local) mx/reactive mx/static]}))
(defn- settings-dialog-render
[own] [own]
(html
[:div.lightbox-body.settings [:div.lightbox-body.settings
[:h3 "Grid settings"] [:h3 "Grid settings"]
(settings-form) (settings-form)
[:a.close {:href "#" [:a.close {:href "#"
:on-click #(do (dom/prevent-default %) :on-click #(do (dom/prevent-default %)
(udl/close!))} i/close]])) (udl/close!))} i/close]])
(def settings-dialog
(mx/component
{:render settings-dialog-render
:name "settings-dialog"
:mixins []}))
(defmethod lbx/render-lightbox :settings (defmethod lbx/render-lightbox :settings
[_] [_]

View file

@ -16,12 +16,11 @@
[uxbox.main.data.pages :as udp] [uxbox.main.data.pages :as udp]
[uxbox.main.data.workspace :as dw] [uxbox.main.data.workspace :as dw]
[uxbox.main.data.lightbox :as udl] [uxbox.main.data.lightbox :as udl]
[uxbox.main.ui.dashboard.projects :refer (+layouts+)]
[uxbox.main.ui.workspace.base :as wb] [uxbox.main.ui.workspace.base :as wb]
[uxbox.main.ui.workspace.sidebar.sitemap-pageform]
[uxbox.main.ui.icons :as i] [uxbox.main.ui.icons :as i]
[uxbox.util.mixins :as mx :include-macros true] [uxbox.util.mixins :as mx :include-macros true]
[uxbox.main.ui.lightbox :as lbx] [uxbox.main.ui.lightbox :as lbx]
[uxbox.util.data :refer (deep-merge parse-int)]
[uxbox.util.dom :as dom])) [uxbox.util.dom :as dom]))
;; --- Refs ;; --- Refs
@ -87,103 +86,3 @@
:let [active? (= (:id page) (:id current))]] :let [active? (= (:id page) (:id current))]]
(-> (page-item page (count pages) active?) (-> (page-item page (count pages) active?)
(mx/with-key (:id page))))]]])) (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))

View file

@ -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 <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(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))

264
src/uxbox/util/forms.cljs Normal file
View file

@ -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 <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(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))

View file

@ -37,10 +37,7 @@
(defrecord Navigate [id params] (defrecord Navigate [id params]
rs/EffectEvent rs/EffectEvent
(-apply-effect [_ state] (-apply-effect [_ state]
(let [loc (merge {:handler id} (r/navigate! +router+ id {})))
(when params
{:route-params params}))]
(r/navigate! +router+ id params))))
(defn navigate (defn navigate
([id] (navigate id nil)) ([id] (navigate id nil))

View file

@ -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 <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(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)))

47
src/uxbox/util/spec.cljs Normal file
View file

@ -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 <niwi@niwi.nz>
(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))

View file

@ -8,7 +8,7 @@
(:require [beicon.core :as rx] (:require [beicon.core :as rx]
[uxbox.util.rstore :as rs] [uxbox.util.rstore :as rs]
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[uxbox.util.schema :as sc] [uxbox.util.forms :as sc]
[uxbox.util.data :refer (parse-int)] [uxbox.util.data :refer (parse-int)]
[uxbox.main.repo :as rp] [uxbox.main.repo :as rp]
[uxbox.main.data.pages :as udpg] [uxbox.main.data.pages :as udpg]

View file

@ -13,13 +13,13 @@
(defonce tree (kd/create)) (defonce tree (kd/create))
(defmethod impl/handler :grid/init (defmethod impl/handler :grid-init
[{:keys [sender width height x-axis y-axis] :as opts}] [{:keys [sender width height x-axis y-axis] :as opts}]
(time (time
(kd/setup! tree width height (or x-axis 10) (or y-axis 10))) (kd/setup! tree width height (or x-axis 10) (or y-axis 10)))
(impl/reply! sender nil)) (impl/reply! sender nil))
(defmethod impl/handler :grid/align (defmethod impl/handler :grid-align
[{:keys [sender point] :as message}] [{:keys [sender point] :as message}]
(let [point [(:x point) (:y point)] (let [point [(:x point) (:y point)]
results (kd/nearest tree point 1) results (kd/nearest tree point 1)