0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 02:28:18 -05:00

🚧 Use cljs.spec everywhere.

This commit is contained in:
Andrey Antukh 2019-09-09 23:21:55 +02:00
parent a009961a58
commit faf7877d00
15 changed files with 93 additions and 391 deletions

View file

@ -7,7 +7,6 @@
environ/environ {:mvn/version "1.1.0"} environ/environ {:mvn/version "1.1.0"}
metosin/reitit-core {:mvn/version "0.3.9"} metosin/reitit-core {:mvn/version "0.3.9"}
funcool/struct {:mvn/version "2.0.0-SNAPSHOT"}
funcool/beicon {:mvn/version "5.1.0"} funcool/beicon {:mvn/version "5.1.0"}
funcool/cuerdas {:mvn/version "2.2.0"} funcool/cuerdas {:mvn/version "2.2.0"}
funcool/lentes {:mvn/version "1.3.0-SNAPSHOT"} funcool/lentes {:mvn/version "1.3.0-SNAPSHOT"}

View file

@ -10,7 +10,6 @@
[potok.core :as ptk] [potok.core :as ptk]
[uxbox.util.router :as r] [uxbox.util.router :as r]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[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

@ -21,7 +21,6 @@
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.workers :as uwrk] [uxbox.main.workers :as uwrk]
[uxbox.util.data :refer [dissoc-in index-of]] [uxbox.util.data :refer [dissoc-in index-of]]
[uxbox.util.forms :as sc]
[uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt] [uxbox.util.geom.point :as gpt]
[uxbox.util.math :as mth] [uxbox.util.math :as mth]

View file

@ -15,7 +15,7 @@
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages-widget]] [uxbox.main.ui.messages :refer [messages-widget]]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.forms2 :as fm2] [uxbox.util.forms :as fm2]
[uxbox.util.i18n :refer [tr]] [uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt])) [uxbox.util.router :as rt]))

View file

@ -7,7 +7,7 @@
(ns uxbox.main.ui.auth.recovery (ns uxbox.main.ui.auth.recovery
(:require (:require
[cljs.spec.alpha :as s :include-macros true] [cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[lentes.core :as l] [lentes.core :as l]
[rumext.core :as mx :include-macros true] [rumext.core :as mx :include-macros true]

View file

@ -7,10 +7,10 @@
(ns uxbox.main.ui.auth.register (ns uxbox.main.ui.auth.register
(:require (:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[lentes.core :as l] [lentes.core :as l]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[struct.alpha :as s]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.auth :as uda] [uxbox.main.data.auth :as uda]
[uxbox.main.store :as st] [uxbox.main.store :as st]
@ -21,11 +21,16 @@
[uxbox.util.i18n :refer [tr]] [uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt])) [uxbox.util.router :as rt]))
(s/defs ::register-form (s/def ::username ::fm/not-empty-string)
(s/dict :username (s/&& ::s/string ::fm/not-empty-string) (s/def ::fullname ::fm/not-empty-string)
:fullname (s/&& ::s/string ::fm/not-empty-string) (s/def ::password ::fm/not-empty-string)
:password (s/&& ::s/string ::fm/not-empty-string) (s/def ::email ::fm/email)
:email ::s/email))
(s/def ::register-form
(s/keys :req-un [::username
::password
::fullname
::email]))
(defn- on-error (defn- on-error
[error form] [error form]
@ -112,7 +117,6 @@
:type #{::api} :type #{::api}
:field :email}] :field :email}]
[:input.btn-primary [:input.btn-primary
{:type "submit" {:type "submit"
:tab-index "5" :tab-index "5"

View file

@ -7,7 +7,7 @@
(ns uxbox.main.ui.dashboard.projects-forms (ns uxbox.main.ui.dashboard.projects-forms
(:require (:require
[struct.alpha :as s] [cljs.spec.alpha :as s]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.projects :as udp] [uxbox.main.data.projects :as udp]
@ -17,10 +17,12 @@
[uxbox.util.forms :as fm] [uxbox.util.forms :as fm]
[uxbox.util.i18n :as t :refer [tr]])) [uxbox.util.i18n :as t :refer [tr]]))
(s/defs ::project-form (s/def ::name ::fm/not-empty-string)
(s/dict :name (s/&& ::s/string ::fm/not-empty-string) (s/def ::width ::fm/number-str)
:width ::s/number-str (s/def ::height ::fm/number-str)
:height ::s/number-str))
(s/def ::project-form
(s/keys :req-un [::name ::width ::height]))
(def defaults (def defaults
{:name "" {:name ""

View file

@ -13,7 +13,7 @@
[uxbox.main.data.users :as udu] [uxbox.main.data.users :as udu]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.forms2 :as fm] [uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]] [uxbox.util.i18n :refer [tr]]
[uxbox.util.messages :as um])) [uxbox.util.messages :as um]))

View file

@ -16,7 +16,7 @@
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.util.data :refer [read-string]] [uxbox.util.data :refer [read-string]]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.forms2 :as fm] [uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.interop :refer [iterable->seq]] [uxbox.util.interop :refer [iterable->seq]]
[uxbox.util.messages :as um])) [uxbox.util.messages :as um]))

View file

@ -8,22 +8,29 @@
(ns uxbox.main.ui.workspace.sidebar.sitemap-forms (ns uxbox.main.ui.workspace.sidebar.sitemap-forms
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[struct.alpha :as s] [cljs.spec.alpha :as s]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.constants :as c] [uxbox.main.constants :as c]
[uxbox.main.data.pages :as udp] [uxbox.main.data.pages :as udp]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.ui.modal :as modal] [uxbox.main.ui.modal :as modal]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.spec :as us]
[uxbox.util.forms :as fm] [uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]])) [uxbox.util.i18n :refer [tr]]))
(s/defs ::page-form (s/def ::id ::us/uuid)
(s/dict :id (s/opt ::s/uuid) (s/def ::project ::us/uuid)
:project ::s/uuid (s/def ::name ::us/not-empty-string)
:name (s/&& ::s/string ::fm/not-empty-string) (s/def ::width ::us/number-str)
:width ::s/number-str (s/def ::height ::us/number-str)
:height ::s/number-str))
(s/def ::page-form
(s/keys :req-un [::id
::project
::name
::width
::height]))
(def defaults (def defaults
{:name "" {:name ""

View file

@ -13,9 +13,8 @@
[lentes.core :as l] [lentes.core :as l]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[rumext.core :as mx]
[struct.alpha :as st]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.spec :as us]
[uxbox.util.i18n :refer [tr]])) [uxbox.util.i18n :refer [tr]]))
;; --- Handlers Helpers ;; --- Handlers Helpers
@ -36,43 +35,41 @@
(defn- translate-error-type (defn- translate-error-type
[name] [name]
(case name "errors.undefined-error")
::st/string "errors.form.string"
::st/number "errors.form.number"
::st/number-str "errors.form.number"
::st/integer "errors.form.integer"
::st/integer-str "errors.form.integer"
::st/required "errors.form.required"
::st/email "errors.form.email"
;; ::st/identical-to "errors.form.does-not-match"
"errors.undefined-error"))
(defn- process-errors (defn- interpret-problem
[errors] [acc {:keys [path pred val via in] :as problem}]
(reduce (fn [acc {:keys [path name] :as error}] ;; (prn "interpret-problem" problem)
(let [message (translate-error-type name)] (cond
(assoc-in acc path (and (empty? path)
(-> (assoc error :message message) (list? pred)
(dissoc :path))))) (= (first (last pred)) 'cljs.core/contains?))
{} errors)) (let [path (conj path (last (last pred)))]
(assoc-in acc path {:name ::missing :type :builtin}))
(and (not (empty? path))
(not (empty? via)))
(assoc-in acc path {:name (last via) :type :builtin})
:else acc))
(defn use-form (defn use-form
[spec initial] [spec initial]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
:errors {} :errors {}
:touched {}}) :touched {}})
cdata (st/conform spec (:data state)) clean-data (s/conform spec (:data state))
errors' (when (= ::st/invalid cdata) problems (when (= ::s/invalid clean-data)
(st/explain spec (:data state))) (::s/problems (s/explain-data spec (:data state))))
errors (merge (process-errors errors')
errors (merge (reduce interpret-problem {} problems)
(:errors state))] (:errors state))]
(-> (assoc state (-> (assoc state
:errors errors :errors errors
:clean-data (when (not= cdata ::st/invalid) cdata) :clean-data (when (not= clean-data ::s/invalid) clean-data)
:valid (and (empty? errors) :valid (and (empty? errors)
(not= cdata ::st/invalid))) (not= clean-data ::s/invalid)))
(impl-mutator update-state)))) (impl-mutator update-state))))
(defn on-input-change (defn on-input-change
@ -103,9 +100,10 @@
(when (and touched? error (when (and touched? error
(cond (cond
(nil? type) true (nil? type) true
(ifn? type) (type (:type error))
(keyword? type) (= (:type error) type) (keyword? type) (= (:type error) type)
(ifn? type) (type (:type error))
:else false)) :else false))
(prn "field-error" error)
[:ul.form-errors [:ul.form-errors
[:li {:key code} (tr message)]]))) [:li {:key code} (tr message)]])))
@ -115,191 +113,10 @@
(get-in form [:touched field])) (get-in form [:touched field]))
"invalid")) "invalid"))
;; --- Additional Validators
(st/defs ::not-empty-string #(not (empty? %)))
;; (def string (assoc st/string :message "errors.should-be-string"))
;; (def number (assoc st/number :message "errors.should-be-number"))
;; (def number-str (assoc st/number-str :message "errors.should-be-number"))
;; (def integer (assoc st/integer :message "errors.should-be-integer"))
;; (def integer-str (assoc st/integer-str :message "errors.should-be-integer"))
;; (def required (assoc st/required :message "errors.required"))
;; (def email (assoc st/email :message "errors.should-be-valid-email"))
;; (def uuid (assoc st/uuid :message "errors.should-be-uuid"))
;; (def uuid-str (assoc st/uuid-str :message "errors.should-be-valid-uuid"))
;; DEPRECATED
;; --- Form Validation Api
;; --- Form Specs and Conformers ;; --- Form Specs and Conformers
(def ^:private email-re ;; TODO: migrate to uxbox.util.spec
#"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") (s/def ::email ::us/email)
(s/def ::not-empty-string ::us/not-empty-string)
(def ^:private number-re (s/def ::color ::us/color)
#"^[-+]?[0-9]*\.?[0-9]+$") (s/def ::number-str ::us/number-str)
(def ^:private color-re
#"^#[0-9A-Fa-f]{6}$")
(s/def ::email
(s/and string? #(boolean (re-matches email-re %))))
(s/def ::non-empty-string
(s/and string? #(not (str/empty? %))))
(s/def ::not-empty #(not (str/empty? %)))
(defn- parse-number
[v]
(cond
(re-matches number-re v) (js/parseFloat v)
(number? v) v
:else ::s/invalid))
(s/def ::string-number
(s/conformer parse-number str))
(s/def ::color
(s/and string? #(boolean (re-matches color-re %))))
;; --- Form State Events
;; --- Assoc Error
(defrecord AssocError [type field error]
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:errors type field] error)))
(defn assoc-error
([type field]
(assoc-error type field nil))
([type field error]
{:pre [(keyword? type)
(keyword? field)
(any? error)]}
(AssocError. type field error)))
;; --- Assoc Errors
(defrecord AssocErrors [type errors]
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:errors type] errors)))
(defn assoc-errors
([type]
(assoc-errors type nil))
([type errors]
{:pre [(keyword? type)
(or (map? errors)
(nil? errors))]}
(AssocErrors. type errors)))
;; --- Assoc Value
(declare clear-error)
(defrecord AssocValue [type field value]
ptk/UpdateEvent
(update [_ state]
(let [form-path (into [:forms type] (if (coll? field) field [field]))]
(assoc-in state form-path value)))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (clear-error type field))))
(defn assoc-value
[type field value]
{:pre [(keyword? type)
(keyword? field)
(any? value)]}
(AssocValue. type field value))
;; --- Clear Values
(defrecord ClearValues [type]
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:forms type] nil)))
(defn clear-values
[type]
{:pre [(keyword? type)]}
(ClearValues. type))
;; --- Clear Error
(deftype ClearError [type field]
ptk/UpdateEvent
(update [_ state]
(let [errors (get-in state [:errors type])]
(if (map? errors)
(assoc-in state [:errors type] (dissoc errors field))
(update state :errors dissoc type)))))
(defn clear-error
[type field]
{:pre [(keyword? type)
(keyword? field)]}
(ClearError. type field))
;; --- Clear Errors
(defrecord ClearErrors [type]
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:errors type] nil)))
(defn clear-errors
[type]
{:pre [(keyword? type)]}
(ClearErrors. type))
;; --- Clear Form
(deftype ClearForm [type]
ptk/WatchEvent
(watch [_ state stream]
(rx/of (clear-values type)
(clear-errors type))))
(defn clear-form
[type]
{:pre [(keyword? type)]}
(ClearForm. 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 clear-mixin
[store type]
{:will-unmount (fn [own]
(ptk/emit! store (clear-form type))
own)})

View file

@ -1,145 +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-2017 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.util.forms2
(:refer-clojure :exclude [uuid])
(:require
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[potok.core :as ptk]
[rumext.alpha :as mf]
[uxbox.util.dom :as dom]
[uxbox.util.i18n :refer [tr]]))
;; --- Handlers Helpers
(defn- impl-mutator
[v update-fn]
(specify v
IReset
(-reset! [_ new-value]
(update-fn new-value))
ISwap
(-swap!
([self f] (update-fn f))
([self f x] (update-fn #(f % x)))
([self f x y] (update-fn #(f % x y)))
([self f x y more] (update-fn #(apply f % x y more))))))
(defn- translate-error-type
[name]
"errors.undefined-error")
(defn- interpret-problem
[acc {:keys [path pred val via in] :as problem}]
;; (prn "interpret-problem" problem)
(cond
(and (empty? path)
(list? pred)
(= (first (last pred)) 'cljs.core/contains?))
(let [path (conj path (last (last pred)))]
(assoc-in acc path {:name ::missing :type :builtin}))
(and (not (empty? path))
(not (empty? via)))
(assoc-in acc path {:name (last via) :type :builtin})
:else acc))
(defn use-form
[spec initial]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}})
clean-data (s/conform spec (:data state))
problems (when (= ::s/invalid clean-data)
(::s/problems (s/explain-data spec (:data state))))
errors (merge (reduce interpret-problem {} problems)
(:errors state))]
(-> (assoc state
:errors errors
:clean-data (when (not= clean-data ::s/invalid) clean-data)
:valid (and (empty? errors)
(not= clean-data ::s/invalid)))
(impl-mutator update-state))))
(defn on-input-change
[{:keys [data] :as form} field]
(fn [event]
(let [target (dom/get-target event)
value (dom/get-value target)]
(swap! form (fn [state]
(-> state
(assoc-in [:data field] value)
(update :errors dissoc field)))))))
(defn on-input-blur
[{:keys [touched] :as form} field]
(fn [event]
(let [target (dom/get-target event)]
(when-not (get touched field)
(swap! form assoc-in [:touched field] true)))))
;; --- Helper Components
(mf/defc field-error
[{:keys [form field type]
:or {only (constantly true)}
:as props}]
(let [touched? (get-in form [:touched field])
{:keys [message code] :as error} (get-in form [:errors field])]
(when (and touched? error
(cond
(nil? type) true
(keyword? type) (= (:type error) type)
(ifn? type) (type (:type error))
:else false))
(prn "field-error" error)
[:ul.form-errors
[:li {:key code} (tr message)]])))
(defn error-class
[form field]
(when (and (get-in form [:errors field])
(get-in form [:touched field]))
"invalid"))
;; --- Form Validation Api
;; --- Form Specs and Conformers
(def ^:private email-re
#"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
(def ^:private number-re
#"^[-+]?[0-9]*\.?[0-9]+$")
(def ^:private color-re
#"^#[0-9A-Fa-f]{6}$")
(s/def ::email
(s/and string? #(boolean (re-matches email-re %))))
(s/def ::not-empty-string
(s/and string? #(not (str/empty? %))))
(defn- parse-number
[v]
(cond
(re-matches number-re v) (js/parseFloat v)
(number? v) v
:else ::s/invalid))
(s/def ::string-number
(s/conformer parse-number str))
(s/def ::color
(s/and string? #(boolean (re-matches color-re %))))

View file

@ -140,7 +140,6 @@
(mf/defc messages-widget (mf/defc messages-widget
[{:keys [message] :as props}] [{:keys [message] :as props}]
(prn "messages-widget" props)
(case (:type message) (case (:type message)
:error (mf/element notification-box props) :error (mf/element notification-box props)
:info (mf/element notification-box props) :info (mf/element notification-box props)

View file

@ -5,7 +5,9 @@
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.util.spec (ns uxbox.util.spec
(:require [cljs.spec.alpha :as s])) (:require [cljs.spec.alpha :as s]
[cuerdas.core :as str]))
;; --- Constants ;; --- Constants
@ -15,11 +17,17 @@
(def uuid-rx (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}$") #"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
(def number-rx
#"^[+-]?([0-9]*\.?[0-9]+|[0-9]+\.?[0-9]*)([eE][+-]?[0-9]+)?$")
(def ^:private color-re
#"^#[0-9A-Fa-f]{6}$")
;; --- Predicates ;; --- Predicates
(defn email? (defn email?
[v] [v]
(and string? (and (string? v)
(re-matches email-rx v))) (re-matches email-rx v)))
(defn color? (defn color?
@ -31,8 +39,6 @@
[v] [v]
(instance? js/File v)) (instance? js/File v))
;; TODO: properly implement
(defn url-str? (defn url-str?
[v] [v]
(string? v)) (string? v))
@ -49,6 +55,22 @@
(s/def ::keyword keyword?) (s/def ::keyword keyword?)
(s/def ::fn fn?) (s/def ::fn fn?)
(s/def ::not-empty-string
(s/and string? #(not (str/empty? %))))
(defn- conform-number-str
[v]
(cond
(re-matches number-rx v) (js/parseFloat v)
(number? v) v
:else ::s/invalid))
(s/def ::number-str
(s/conformer conform-number-str str))
(s/def ::color color?)
;; --- Public Api ;; --- Public Api
(defn valid? (defn valid?

View file

@ -8,7 +8,6 @@
(:require [beicon.core :as rx] (:require [beicon.core :as rx]
[potok.core :as ptk] [potok.core :as ptk]
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[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]