🔧 Refactor token validation schemas

This commit is contained in:
Andrey Antukh
2025-11-14 11:04:16 +01:00
committed by Andrés Moya
parent 8a8f360c7f
commit e8ff89836e
38 changed files with 3503 additions and 562 deletions

View File

@@ -27,6 +27,7 @@
[app.common.types.path :as path] [app.common.types.path :as path]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst] [app.common.types.shape-tree :as ctst]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.common.types.typographies-list :as ctyl] [app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as ctt] [app.common.types.typography :as ctt]
@@ -378,7 +379,7 @@
[:type [:= :set-token]] [:type [:= :set-token]]
[:set-id ::sm/uuid] [:set-id ::sm/uuid]
[:token-id ::sm/uuid] [:token-id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-attrs]]]] [:attrs [:maybe cto/schema:token-attrs]]]]
[:set-token-set [:set-token-set
[:map {:title "SetTokenSetChange"} [:map {:title "SetTokenSetChange"}

View File

@@ -8,9 +8,112 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.i18n :refer [tr]]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[clojure.set :as set] [clojure.set :as set]
[cuerdas.core :as str])) [cuerdas.core :as str]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HIGH LEVEL SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Token
(defn make-token-name-schema
"Dynamically generates a schema to check a token name, adding translated error messages
and two additional validations:
- Min and max length.
- Checks if other token with a path derived from the name already exists at `tokens-tree`.
e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists."
[tokens-tree]
[:and
(-> cto/schema:token-name
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]])
(def schema:token-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-schema
[tokens-tree]
(sm/merge
cto/schema:token-attrs
[:map
[:name (make-token-name-schema tokens-tree)]
[:description {:optional true} schema:token-description]]))
;; Token set
(defn make-token-set-name-schema
"Generates a dynamic schema to check a token set name:
- Validate name length.
- Checks if other token set with a path derived from the name already exists in the tokens lib."
[tokens-lib set-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-set-already-exists" (:value %))}
(fn [name]
(let [set (ctob/get-set-by-name tokens-lib name)]
(or (nil? set) (= (ctob/get-id set) set-id))))]])
(def schema:token-set-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-set-schema
[tokens-lib set-id]
(sm/merge
ctob/schema:token-set-attrs
[:map
[:name [:and (make-token-set-name-schema tokens-lib set-id)
[:fn #(ctob/normalized-set-name? %)]]]
[:description {:optional true} schema:token-set-description]]))
;; Token theme
(defn make-token-theme-group-schema
"Generates a dynamic schema to check a token theme group:
- Validate group length.
- Checks if other token theme with the same name already exists in the new group in the tokens lib."
[tokens-lib name theme-id]
[:and
[:string {:min 0 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (:value %))}
(fn [group]
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme) (= (:id theme) theme-id))))]])
(defn make-token-theme-name-schema
"Generates a dynamic schema to check a token theme name:
- Validate name length.
- Checks if other token theme with the same name already exists in the same group in the tokens lib."
[tokens-lib group theme-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (str group "/" (:value %)))}
(fn [name]
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme) (= (:id theme) theme-id))))]])
(def schema:token-theme-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-theme-schema
[tokens-lib group name theme-id]
(sm/merge
ctob/schema:token-theme-attrs
[:map
[:group (make-token-theme-group-schema tokens-lib name theme-id)] ;; TODO how to keep error-fn from here?
[:name (make-token-theme-name-schema tokens-lib group theme-id)]
[:description {:optional true} schema:token-theme-description]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def parseable-token-value-regexp (def parseable-token-value-regexp
"Regexp that can be used to parse a number value out of resolved token value. "Regexp that can be used to parse a number value out of resolved token value.
This regexp also trims whitespace around the value." This regexp also trims whitespace around the value."
@@ -80,56 +183,6 @@
(defn shapes-applied-all? [ids-by-attributes shape-ids attributes] (defn shapes-applied-all? [ids-by-attributes shape-ids attributes]
(every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes)) (every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes))
(defn token-name->path
"Splits token-name into a path vector split by `.` characters.
Will concatenate multiple `.` characters into one."
[token-name]
(str/split token-name #"\.+"))
(defn token-name->path-selector
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
`:selector` is the last item of the names path
`:path` is everything leading up the the `:selector`."
[token-name]
(let [path-segments (token-name->path token-name)
last-idx (dec (count path-segments))
[path [selector]] (split-at last-idx path-segments)]
{:path (seq path)
:selector selector}))
(defn token-name-path-exists?
"Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists.
It's not allowed to create a token inside a token. E.g.:
Creating a token with
{:name \"foo.bar\"}
in the tokens tree:
{\"foo\" {:name \"other\"}}"
[token-name token-names-tree]
(let [{:keys [path selector]} (token-name->path-selector token-name)
path-target (reduce
(fn [acc cur]
(let [target (get acc cur)]
(cond
;; Path segment doesn't exist yet
(nil? target) (reduced false)
;; A token exists at this path
(:name target) (reduced true)
;; Continue traversing the true
:else target)))
token-names-tree path)]
(cond
(boolean? path-target) path-target
(get path-target :name) true
:else (-> (get path-target selector)
(seq)
(boolean)))))
(defn color-token? [token] (defn color-token? [token]
(= (:type token) :color)) (= (:type token) :color))

View File

@@ -0,0 +1,15 @@
;; 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) KALEIDOS INC
(ns app.common.i18n
"Dummy i18n functions, to be used by code in common that needs translations.")
(defn tr
"This function will be monkeypatched at runtime with the real function in frontend i18n.
Here it just returns the key passed as argument. This way the result can be used in
unit tests or backend code for logs or error messages."
[key & _args]
key)

View File

@@ -58,7 +58,7 @@
(cto/shape-attr->token-attrs attr changed-sub-attr))] (cto/shape-attr->token-attrs attr changed-sub-attr))]
(if (some #(contains? tokens %) token-attrs) (if (some #(contains? tokens %) token-attrs)
(pcb/update-shapes changes [shape-id] #(cto/unapply-token-id % token-attrs)) (pcb/update-shapes changes [shape-id] #(cto/unapply-tokens-from-shape % token-attrs))
changes))) changes)))
check-shape check-shape

View File

@@ -92,6 +92,16 @@
[& items] [& items]
(apply mu/merge (map schema items))) (apply mu/merge (map schema items)))
(defn assoc-key
"Add a key & value to a schema"
[s k v]
(mu/assoc s k v))
(defn dissoc-key
"Remove a key from a schema"
[s k]
(mu/dissoc s k))
(defn ref? (defn ref?
[s] [s]
(m/-ref-schema? s)) (m/-ref-schema? s))
@@ -270,6 +280,13 @@
(let [explain (fn [] (me/with-error-messages explain))] (let [explain (fn [] (me/with-error-messages explain))]
((mdp/prettifier variant message explain default-options))))) ((mdp/prettifier variant message explain default-options)))))
(defn validation-errors
"Checks a value against a schema. If valid, returns nil. If not, returns a list
of english error messages."
[value schema]
(let [explainer (explainer schema)]
(-> value explainer simplify not-empty)))
(defmacro ignoring (defmacro ignoring
[expr] [expr]
(if (:ns &env) (if (:ns &env)

View File

@@ -9,13 +9,13 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.schema.generators :as sg] [app.common.schema.generators :as sg]
[clojure.data :as data] [app.common.time :as ct]
[clojure.set :as set] [clojure.set :as set]
[cuerdas.core :as str] [cuerdas.core :as str]
[malli.util :as mu])) [malli.util :as mu]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS ;; GENERAL HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- schema-keys (defn- schema-keys
@@ -45,32 +45,33 @@
[token-name token-value] [token-name token-value]
(let [token-references (find-token-value-references token-value) (let [token-references (find-token-value-references token-value)
self-reference? (get token-references token-name)] self-reference? (get token-references token-name)]
self-reference?)) (boolean self-reference?)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn composite-token-reference?
;; SCHEMA "Predicate if a composite token is a reference value - a string pointing to another token."
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; [token-value]
(string? token-value))
(def token-type->dtcg-token-type (def token-type->dtcg-token-type
{:boolean "boolean" {:boolean "boolean"
:border-radius "borderRadius" :border-radius "borderRadius"
:shadow "shadow"
:color "color" :color "color"
:dimensions "dimension" :dimensions "dimension"
:font-family "fontFamilies" :font-family "fontFamilies"
:font-size "fontSizes" :font-size "fontSizes"
:font-weight "fontWeights"
:letter-spacing "letterSpacing" :letter-spacing "letterSpacing"
:number "number" :number "number"
:opacity "opacity" :opacity "opacity"
:other "other" :other "other"
:rotation "rotation" :rotation "rotation"
:shadow "shadow"
:sizing "sizing" :sizing "sizing"
:spacing "spacing" :spacing "spacing"
:string "string" :string "string"
:stroke-width "borderWidth" :stroke-width "borderWidth"
:text-case "textCase" :text-case "textCase"
:text-decoration "textDecoration" :text-decoration "textDecoration"
:font-weight "fontWeights"
:typography "typography"}) :typography "typography"})
(def dtcg-token-type->token-type (def dtcg-token-type->token-type
@@ -82,14 +83,13 @@
"boxShadow" :shadow))) "boxShadow" :shadow)))
(def composite-token-type->dtcg-token-type (def composite-token-type->dtcg-token-type
"Custom set of conversion keys for composite typography token with `:line-height` available. "When converting the type of one element inside a composite token, an additional type
(Penpot doesn't support `:line-height` token)" :line-height is available, that is not allowed for a standalone token."
(assoc token-type->dtcg-token-type (assoc token-type->dtcg-token-type
:line-height "lineHeights")) :line-height "lineHeights"))
(def composite-dtcg-token-type->token-type (def composite-dtcg-token-type->token-type
"Custom set of conversion keys for composite typography token with `:line-height` available. "Same as above, in the opposite direction."
(Penpot doesn't support `:line-height` token)"
(assoc dtcg-token-type->token-type (assoc dtcg-token-type->token-type
"lineHeights" :line-height "lineHeights" :line-height
"lineHeight" :line-height)) "lineHeight" :line-height))
@@ -97,93 +97,95 @@
(def token-types (def token-types
(into #{} (keys token-type->dtcg-token-type))) (into #{} (keys token-type->dtcg-token-type)))
(def token-name-ref ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[:re {:title "TokenNameRef" :gen/gen sg/text} ;; SCHEMA: Token
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def schema:token-name
"A token name can contains letters, numbers, underscores the character $ and dots, but
not start with $ or end with a dot. The $ character does not have any special meaning,
but dots separate token groups (e.g. color.primary.background)."
[:re {:title "TokenName"
:gen/gen sg/text}
#"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"]) #"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"])
(def ^:private schema:color (def schema:token-type
[:map [::sm/one-of {:decode/json (fn [type]
[:fill {:optional true} token-name-ref] (if (string? type)
[:stroke-color {:optional true} token-name-ref]]) (dtcg-token-type->token-type type)
type))}
(def color-keys (schema-keys schema:color)) token-types])
(def schema:token-attrs
[:map {:title "Token"}
[:id ::sm/uuid]
[:name schema:token-name]
[:type schema:token-type]
[:value ::sm/any]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA: Token application to shape
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; All the following schemas define the `:applied-tokens` attribute of a shape.
;; This attribute is a map <token-attribute> -> <token-name>.
;; Token attributes approximately match shape attributes, but not always.
;; For each schema there is a `*keys` set including all the possible token attributes
;; to which a token of the corresponding type can be applied.
;; Some token types can be applied to some attributes only if the shape has a
;; particular condition (i.e. has a layout itself or is a layout item).
(def ^:private schema:border-radius (def ^:private schema:border-radius
[:map {:title "BorderRadiusTokenAttrs"} [:map {:title "BorderRadiusTokenAttrs"}
[:r1 {:optional true} token-name-ref] [:r1 {:optional true} schema:token-name]
[:r2 {:optional true} token-name-ref] [:r2 {:optional true} schema:token-name]
[:r3 {:optional true} token-name-ref] [:r3 {:optional true} schema:token-name]
[:r4 {:optional true} token-name-ref]]) [:r4 {:optional true} schema:token-name]])
(def border-radius-keys (schema-keys schema:border-radius)) (def border-radius-keys (schema-keys schema:border-radius))
(def ^:private schema:shadow (def ^:private schema:color
[:map {:title "ShadowTokenAttrs"}
[:shadow {:optional true} token-name-ref]])
(def shadow-keys (schema-keys schema:shadow))
(def ^:private schema:stroke-width
[:map [:map
[:stroke-width {:optional true} token-name-ref]]) [:fill {:optional true} schema:token-name]
[:stroke-color {:optional true} schema:token-name]])
(def stroke-width-keys (schema-keys schema:stroke-width)) (def color-keys (schema-keys schema:color))
(def ^:private schema:sizing-base (def ^:private schema:sizing-base
[:map {:title "SizingBaseTokenAttrs"} [:map {:title "SizingBaseTokenAttrs"}
[:width {:optional true} token-name-ref] [:width {:optional true} schema:token-name]
[:height {:optional true} token-name-ref]]) [:height {:optional true} schema:token-name]])
(def ^:private schema:sizing-layout-item (def ^:private schema:sizing-layout-item
[:map {:title "SizingLayoutItemTokenAttrs"} [:map {:title "SizingLayoutItemTokenAttrs"}
[:layout-item-min-w {:optional true} token-name-ref] [:layout-item-min-w {:optional true} schema:token-name]
[:layout-item-max-w {:optional true} token-name-ref] [:layout-item-max-w {:optional true} schema:token-name]
[:layout-item-min-h {:optional true} token-name-ref] [:layout-item-min-h {:optional true} schema:token-name]
[:layout-item-max-h {:optional true} token-name-ref]]) [:layout-item-max-h {:optional true} schema:token-name]])
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
(def ^:private schema:sizing (def ^:private schema:sizing
(-> (reduce mu/union [schema:sizing-base (-> (reduce mu/union [schema:sizing-base
schema:sizing-layout-item]) schema:sizing-layout-item])
(mu/update-properties assoc :title "SizingTokenAttrs"))) (mu/update-properties assoc :title "SizingTokenAttrs")))
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
(def sizing-keys (schema-keys schema:sizing)) (def sizing-keys (schema-keys schema:sizing))
(def ^:private schema:opacity
[:map {:title "OpacityTokenAttrs"}
[:opacity {:optional true} token-name-ref]])
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:spacing-gap (def ^:private schema:spacing-gap
[:map {:title "SpacingGapTokenAttrs"} [:map {:title "SpacingGapTokenAttrs"}
[:row-gap {:optional true} token-name-ref] [:row-gap {:optional true} schema:token-name]
[:column-gap {:optional true} token-name-ref]]) [:column-gap {:optional true} schema:token-name]])
(def ^:private schema:spacing-padding (def ^:private schema:spacing-padding
[:map {:title "SpacingPaddingTokenAttrs"} [:map {:title "SpacingPaddingTokenAttrs"}
[:p1 {:optional true} token-name-ref] [:p1 {:optional true} schema:token-name]
[:p2 {:optional true} token-name-ref] [:p2 {:optional true} schema:token-name]
[:p3 {:optional true} token-name-ref] [:p3 {:optional true} schema:token-name]
[:p4 {:optional true} token-name-ref]]) [:p4 {:optional true} schema:token-name]])
(def ^:private schema:spacing-margin
[:map {:title "SpacingMarginTokenAttrs"}
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]])
(def ^:private schema:spacing
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin])
(mu/update-properties assoc :title "SpacingTokenAttrs")))
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:spacing-gap-padding (def ^:private schema:spacing-gap-padding
(-> (reduce mu/union [schema:spacing-gap (-> (reduce mu/union [schema:spacing-gap
@@ -192,6 +194,29 @@
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding)) (def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
(def ^:private schema:spacing-margin
[:map {:title "SpacingMarginTokenAttrs"}
[:m1 {:optional true} schema:token-name]
[:m2 {:optional true} schema:token-name]
[:m3 {:optional true} schema:token-name]
[:m4 {:optional true} schema:token-name]])
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def ^:private schema:spacing
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin])
(mu/update-properties assoc :title "SpacingTokenAttrs")))
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:stroke-width
[:map
[:stroke-width {:optional true} schema:token-name]])
(def stroke-width-keys (schema-keys schema:stroke-width))
(def ^:private schema:dimensions (def ^:private schema:dimensions
(-> (reduce mu/union [schema:sizing (-> (reduce mu/union [schema:sizing
schema:spacing schema:spacing
@@ -201,91 +226,109 @@
(def dimensions-keys (schema-keys schema:dimensions)) (def dimensions-keys (schema-keys schema:dimensions))
(def ^:private schema:axis
[:map
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
(def axis-keys (schema-keys schema:axis))
(def ^:private schema:rotation
[:map {:title "RotationTokenAttrs"}
[:rotation {:optional true} token-name-ref]])
(def rotation-keys (schema-keys schema:rotation))
(def ^:private schema:font-size
[:map {:title "FontSizeTokenAttrs"}
[:font-size {:optional true} token-name-ref]])
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:letter-spacing
[:map {:title "LetterSpacingTokenAttrs"}
[:letter-spacing {:optional true} token-name-ref]])
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(def ^:private schema:font-family (def ^:private schema:font-family
[:map [:map
[:font-family {:optional true} token-name-ref]]) [:font-family {:optional true} schema:token-name]])
(def font-family-keys (schema-keys schema:font-family)) (def font-family-keys (schema-keys schema:font-family))
(def ^:private schema:text-case (def ^:private schema:font-size
[:map [:map {:title "FontSizeTokenAttrs"}
[:text-case {:optional true} token-name-ref]]) [:font-size {:optional true} schema:token-name]])
(def text-case-keys (schema-keys schema:text-case)) (def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:font-weight (def ^:private schema:font-weight
[:map [:map
[:font-weight {:optional true} token-name-ref]]) [:font-weight {:optional true} schema:token-name]])
(def font-weight-keys (schema-keys schema:font-weight)) (def font-weight-keys (schema-keys schema:font-weight))
(def ^:private schema:typography (def ^:private schema:letter-spacing
[:map [:map {:title "LetterSpacingTokenAttrs"}
[:typography {:optional true} token-name-ref]]) [:letter-spacing {:optional true} schema:token-name]])
(def typography-token-keys (schema-keys schema:typography)) (def letter-spacing-keys (schema-keys schema:letter-spacing))
(def ^:private schema:text-decoration (def ^:private schema:line-height ;; This is not available for standalone tokens, only typography
[:map [:map {:title "LineHeightTokenAttrs"}
[:text-decoration {:optional true} token-name-ref]]) [:line-height {:optional true} schema:token-name]])
(def text-decoration-keys (schema-keys schema:text-decoration)) (def line-height-keys (schema-keys schema:line-height))
(def typography-keys (set/union font-size-keys (def ^:private schema:rotation
letter-spacing-keys [:map {:title "RotationTokenAttrs"}
font-family-keys [:rotation {:optional true} schema:token-name]])
font-weight-keys
text-case-keys (def rotation-keys (schema-keys schema:rotation))
text-decoration-keys
font-weight-keys
typography-token-keys
#{:line-height}))
(def ^:private schema:number (def ^:private schema:number
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]] (-> (reduce mu/union [schema:line-height
schema:rotation]) schema:rotation])
(mu/update-properties assoc :title "NumberTokenAttrs"))) (mu/update-properties assoc :title "NumberTokenAttrs")))
(def number-keys (schema-keys schema:number)) (def number-keys (schema-keys schema:number))
(def all-keys (set/union color-keys (def ^:private schema:opacity
[:map {:title "OpacityTokenAttrs"}
[:opacity {:optional true} schema:token-name]])
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:shadow
[:map {:title "ShadowTokenAttrs"}
[:shadow {:optional true} schema:token-name]])
(def shadow-keys (schema-keys schema:shadow))
(def ^:private schema:text-case
[:map
[:text-case {:optional true} schema:token-name]])
(def text-case-keys (schema-keys schema:text-case))
(def ^:private schema:text-decoration
[:map
[:text-decoration {:optional true} schema:token-name]])
(def text-decoration-keys (schema-keys schema:text-decoration))
(def ^:private schema:typography
[:map
[:typography {:optional true} schema:token-name]])
(def typography-token-keys (schema-keys schema:typography))
(def typography-keys (set/union font-family-keys
font-size-keys
font-weight-keys
font-weight-keys
letter-spacing-keys
line-height-keys
text-case-keys
text-decoration-keys
typography-token-keys))
(def ^:private schema:axis
[:map
[:x {:optional true} schema:token-name]
[:y {:optional true} schema:token-name]])
(def axis-keys (schema-keys schema:axis))
(def all-keys (set/union axis-keys
border-radius-keys border-radius-keys
shadow-keys color-keys
stroke-width-keys
sizing-keys
opacity-keys
spacing-keys
dimensions-keys dimensions-keys
axis-keys number-keys
opacity-keys
rotation-keys rotation-keys
shadow-keys
sizing-keys
spacing-keys
stroke-width-keys
typography-keys typography-keys
typography-token-keys typography-token-keys))
number-keys))
(def ^:private schema:tokens (def ^:private schema:tokens
[:map {:title "GenericTokenAttrs"}]) [:map {:title "GenericTokenAttrs"}])
@@ -306,11 +349,28 @@
schema:text-decoration schema:text-decoration
schema:dimensions]) schema:dimensions])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for token attributes by token type
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn token-attr? (defn token-attr?
[attr] [attr]
(contains? all-keys attr)) (contains? all-keys attr))
(defn token-attr->shape-attr
"Returns the actual shape attribute affected when a token have been applied
to a given `token-attr`."
[token-attr]
(case token-attr
:fill :fills
:stroke-color :strokes
:stroke-width :strokes
token-attr))
(defn shape-attr->token-attrs (defn shape-attr->token-attrs
"Returns the token-attr affected when a given attribute in a shape is changed.
The sub-attr is for attributes that may have multiple values, like strokes
(may be width or color) and layout padding & margin (may have 4 edges)."
([shape-attr] (shape-attr->token-attrs shape-attr nil)) ([shape-attr] (shape-attr->token-attrs shape-attr nil))
([shape-attr changed-sub-attr] ([shape-attr changed-sub-attr]
(cond (cond
@@ -352,21 +412,13 @@
(number-keys shape-attr) #{shape-attr} (number-keys shape-attr) #{shape-attr}
(axis-keys shape-attr) #{shape-attr}))) (axis-keys shape-attr) #{shape-attr})))
(defn token-attr->shape-attr
[token-attr]
(case token-attr
:fill :fills
:stroke-color :strokes
:stroke-width :strokes
token-attr))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKEN SHAPE ATTRIBUTES ;; HELPERS for token attributes by shape type
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def position-attributes #{:x :y}) (def ^:private position-attributes #{:x :y})
(def generic-attributes (def ^:private generic-attributes
(set/union color-keys (set/union color-keys
stroke-width-keys stroke-width-keys
rotation-keys rotation-keys
@@ -375,20 +427,22 @@
shadow-keys shadow-keys
position-attributes)) position-attributes))
(def rect-attributes (def ^:private rect-attributes
(set/union generic-attributes (set/union generic-attributes
border-radius-keys)) border-radius-keys))
(def frame-with-layout-attributes (def ^:private frame-with-layout-attributes
(set/union rect-attributes (set/union rect-attributes
spacing-gap-padding-keys)) spacing-gap-padding-keys))
(def text-attributes (def ^:private text-attributes
(set/union generic-attributes (set/union generic-attributes
typography-keys typography-keys
number-keys)) number-keys))
(defn shape-type->attributes (defn shape-type->attributes
"Returns what token attributes may be applied to a shape depending on its type
and if it is a frame with a layout."
[type is-layout] [type is-layout]
(case type (case type
:bool generic-attributes :bool generic-attributes
@@ -404,12 +458,14 @@
nil)) nil))
(defn appliable-attrs-for-shape (defn appliable-attrs-for-shape
"Returns intersection of shape `attributes` for `shape-type`." "Returns which ones of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
[attributes shape-type is-layout] [attributes shape-type is-layout]
(set/intersection attributes (shape-type->attributes shape-type is-layout))) (set/intersection attributes (shape-type->attributes shape-type is-layout)))
(defn any-appliable-attr-for-shape? (defn any-appliable-attr-for-shape?
"Checks if `token-type` supports given shape `attributes`." "Returns if any of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
[attributes token-type is-layout] [attributes token-type is-layout]
(d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout))) (d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout)))
@@ -420,42 +476,6 @@
typography-keys typography-keys
#{:fill})) #{:fill}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS IN SHAPES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- toggle-or-apply-token
"Remove any shape attributes from token if they exists.
Othewise apply token attributes."
[shape token]
(let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)]
(merge {} shape-leftover token-leftover)))
(defn- token-from-attributes [token attributes]
(->> (map (fn [attr] [attr (:name token)]) attributes)
(into {})))
(defn- apply-token-to-attributes [{:keys [shape token attributes]}]
(let [token (token-from-attributes token attributes)]
(toggle-or-apply-token shape token)))
(defn apply-token-to-shape
[{:keys [shape token attributes] :as _props}]
(let [applied-tokens (apply-token-to-attributes {:shape shape
:token token
:attributes attributes})]
(update shape :applied-tokens #(merge % applied-tokens))))
(defn unapply-token-id [shape attributes]
(update shape :applied-tokens d/without-keys attributes))
(defn unapply-layout-item-tokens
"Unapplies all layout item related tokens from shape."
[shape]
(let [layout-item-attrs (set/union sizing-layout-item-keys
spacing-margin-keys)]
(unapply-token-id shape layout-item-attrs)))
(def tokens-by-input (def tokens-by-input
"A map from input name to applicable token for that input." "A map from input name to applicable token for that input."
{:width #{:sizing :dimensions} {:width #{:sizing :dimensions}
@@ -481,7 +501,48 @@
:stroke-color #{:color}}) :stroke-color #{:color}})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TYPOGRAPHY ;; HELPERS for tokens application
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TODO it seems that this function is redundant, maybe?
;; (defn- toggle-or-apply-token
;; "Remove any shape attributes from token if they exists.
;; Othewise apply token attributes."
;; [shape token]
;; (let [[only-in-shape only-in-token _matching] (data/diff (:applied-tokens shape) token)]
;; (merge {} only-in-shape only-in-token)))
(defn- generate-attr-map [token attributes]
(->> (map (fn [attr] [attr (:name token)]) attributes)
(into {})))
(defn apply-token-to-shape
"Applies the token to the given attributes in the shape."
[{:keys [shape token attributes] :as _props}]
(let [map-to-apply (generate-attr-map token attributes)]
(update shape :applied-tokens #(merge % map-to-apply))))
;; (defn apply-token-to-shape
;; [{:keys [shape token attributes] :as _props}]
;; (let [map-to-apply (generate-attr-map token attributes)
;; applied-tokens (toggle-or-apply-token shape map-to-apply)]
;; (update shape :applied-tokens #(merge % applied-tokens))))
(defn unapply-tokens-from-shape
"Removes any token applied to the given attributes in the shape."
[shape attributes]
(update shape :applied-tokens d/without-keys attributes))
(defn unapply-layout-item-tokens
"Unapplies all layout item related tokens from shape."
[shape]
(let [layout-item-attrs (set/union sizing-layout-item-keys
spacing-margin-keys)]
(unapply-tokens-from-shape shape layout-item-attrs)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for typography tokens
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-font-family (defn split-font-family
@@ -544,17 +605,3 @@
(when (font-weight-values weight) (when (font-weight-values weight)
(cond-> {:weight weight} (cond-> {:weight weight}
italic? (assoc :style "italic"))))) italic? (assoc :style "italic")))))
(defn typography-composite-token-reference?
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHADOW
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn shadow-composite-token-reference?
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))

View File

@@ -114,25 +114,19 @@
[o] [o]
(instance? Token o)) (instance? Token o))
(def schema:token-attrs
[:map {:title "Token"}
[:id ::sm/uuid]
[:name cto/token-name-ref]
[:type [::sm/one-of cto/token-types]]
[:value ::sm/any]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]])
(declare make-token) (declare make-token)
(def schema:token (def schema:token
[:and {:gen/gen (->> (sg/generator schema:token-attrs) [:and {:gen/gen (->> (sg/generator cto/schema:token-attrs)
(sg/fmap #(make-token %)))} (sg/fmap #(make-token %)))}
(sm/required-keys schema:token-attrs) (sm/required-keys cto/schema:token-attrs)
[:fn token?]]) [:fn token?]])
(def ^:private check-token-attrs (def ^:private check-token-attrs
(sm/check-fn schema:token-attrs :hint "expected valid params for token")) (sm/check-fn cto/schema:token-attrs :hint "expected valid params for token"))
(def decode-token-attrs
(sm/lazy-decoder cto/schema:token-attrs sm/json-transformer))
(def check-token (def check-token
(sm/check-fn schema:token :hint "expected valid token")) (sm/check-fn schema:token :hint "expected valid token"))
@@ -317,10 +311,13 @@
[o] [o]
(instance? TokenSetLegacy o)) (instance? TokenSetLegacy o))
(declare make-token-set)
(declare normalized-set-name?)
(def schema:token-set-attrs (def schema:token-set-attrs
[:map {:title "TokenSet"} [:map {:title "TokenSet"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:name :string] [:name [:and :string [:fn #(normalized-set-name? %)]]]
[:description {:optional true} :string] [:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst] [:modified-at {:optional true} ::ct/inst]
[:tokens {:optional true [:tokens {:optional true
@@ -342,8 +339,6 @@
:string schema:token] :string schema:token]
[:fn d/ordered-map?]]]]) [:fn d/ordered-map?]]]])
(declare make-token-set)
(def schema:token-set (def schema:token-set
[:schema {:gen/gen (->> (sg/generator schema:token-set-attrs) [:schema {:gen/gen (->> (sg/generator schema:token-set-attrs)
(sg/fmap #(make-token-set %)))} (sg/fmap #(make-token-set %)))}
@@ -404,12 +399,25 @@
(split-set-name name)) (split-set-name name))
(cpn/join-path :separator set-separator :with-spaces? false)))) (cpn/join-path :separator set-separator :with-spaces? false))))
(defn normalized-set-name?
"Check if a set name is normalized (no extra spaces)."
[name]
(= name (normalize-set-name name)))
(defn replace-last-path-name (defn replace-last-path-name
"Replaces the last element in a `path` vector with `name`." "Replaces the last element in a `path` vector with `name`."
[path name] [path name]
(-> (into [] (drop-last path)) (-> (into [] (drop-last path))
(conj name))) (conj name)))
(defn make-child-name
"Generate the name of a set child of `parent-set` adding the name `name`."
[parent-set name]
(if-let [parent-path (get-set-path parent-set)]
(->> (concat parent-path (split-set-name name))
(join-set-path))
(normalize-set-name name)))
;; The following functions will be removed after refactoring the internal structure of TokensLib, ;; The following functions will be removed after refactoring the internal structure of TokensLib,
;; since we'll no longer need group prefixes to differentiate between sets and set-groups. ;; since we'll no longer need group prefixes to differentiate between sets and set-groups.
@@ -1365,10 +1373,13 @@ Will return a value that matches this schema:
(def ^:private check-tokens-lib-map (def ^:private check-tokens-lib-map
(sm/check-fn schema:tokens-lib-map :hint "invalid tokens-lib internal data structure")) (sm/check-fn schema:tokens-lib-map :hint "invalid tokens-lib internal data structure"))
(defn tokens-lib?
[o]
(instance? TokensLib o))
(defn valid-tokens-lib? (defn valid-tokens-lib?
[o] [o]
(and (instance? TokensLib o) (and (tokens-lib? o) (valid? o)))
(valid? o)))
(defn- ensure-hidden-theme (defn- ensure-hidden-theme
"A helper that is responsible to ensure that the hidden theme always "A helper that is responsible to ensure that the hidden theme always
@@ -1430,6 +1441,50 @@ Will return a value that matches this schema:
(rename copy-name) (rename copy-name)
(reid (uuid/next)))))) (reid (uuid/next))))))
(defn- token-name->path-selector
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
`:selector` is the last item of the names path
`:path` is everything leading up the the `:selector`."
[token-name]
(let [path-segments (get-token-path {:name token-name})
last-idx (dec (count path-segments))
[path [selector]] (split-at last-idx path-segments)]
{:path (seq path)
:selector selector}))
(defn token-name-path-exists?
"Traverses the path from `token-name` down a `tokens-tree` and checks if a token at that path exists.
It's not allowed to create a token inside a token. E.g.:
Creating a token with
{:name \"foo.bar\"}
in the tokens tree:
{\"foo\" {:name \"other\"}}"
[token-name tokens-tree]
(let [{:keys [path selector]} (token-name->path-selector token-name)
path-target (reduce
(fn [acc cur]
(let [target (get acc cur)]
(cond
;; Path segment doesn't exist yet
(nil? target) (reduced false)
;; A token exists at this path
(:name target) (reduced true)
;; Continue traversing the true
:else target)))
tokens-tree
path)]
(cond
(boolean? path-target) path-target
(get path-target :name) true
:else (-> (get path-target selector)
(seq)
(boolean)))))
;; === Import / Export from JSON format ;; === Import / Export from JSON format
;; Supported formats: ;; Supported formats:

View File

@@ -6,34 +6,34 @@
(ns common-tests.files.tokens-test (ns common-tests.files.tokens-test
(:require (:require
[app.common.files.tokens :as cft] [app.common.files.tokens :as cfo]
[clojure.test :as t])) [clojure.test :as t]))
(t/deftest test-parse-token-value (t/deftest test-parse-token-value
(t/testing "parses double from a token value" (t/testing "parses double from a token value"
(t/is (= {:value 100.1 :unit nil} (cft/parse-token-value "100.1"))) (t/is (= {:value 100.1 :unit nil} (cfo/parse-token-value "100.1")))
(t/is (= {:value -9.0 :unit nil} (cft/parse-token-value "-9")))) (t/is (= {:value -9.0 :unit nil} (cfo/parse-token-value "-9"))))
(t/testing "trims white-space" (t/testing "trims white-space"
(t/is (= {:value -1.3 :unit nil} (cft/parse-token-value " -1.3 ")))) (t/is (= {:value -1.3 :unit nil} (cfo/parse-token-value " -1.3 "))))
(t/testing "parses unit: px" (t/testing "parses unit: px"
(t/is (= {:value 70.3 :unit "px"} (cft/parse-token-value " 70.3px ")))) (t/is (= {:value 70.3 :unit "px"} (cfo/parse-token-value " 70.3px "))))
(t/testing "parses unit: %" (t/testing "parses unit: %"
(t/is (= {:value -10.0 :unit "%"} (cft/parse-token-value "-10%")))) (t/is (= {:value -10.0 :unit "%"} (cfo/parse-token-value "-10%"))))
(t/testing "parses unit: px") (t/testing "parses unit: px")
(t/testing "returns nil for any invalid characters" (t/testing "returns nil for any invalid characters"
(t/is (nil? (cft/parse-token-value " -1.3a ")))) (t/is (nil? (cfo/parse-token-value " -1.3a "))))
(t/testing "doesnt accept invalid double" (t/testing "doesnt accept invalid double"
(t/is (nil? (cft/parse-token-value ".3"))))) (t/is (nil? (cfo/parse-token-value ".3")))))
(t/deftest token-applied-test (t/deftest token-applied-test
(t/testing "matches passed token with `:token-attributes`" (t/testing "matches passed token with `:token-attributes`"
(t/is (true? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x})))) (t/is (true? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match empty token" (t/testing "doesn't match empty token"
(t/is (nil? (cft/token-applied? {} {:applied-tokens {:x "a"}} #{:x})))) (t/is (nil? (cfo/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "does't match passed token `:id`" (t/testing "does't match passed token `:id`"
(t/is (nil? (cft/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x})))) (t/is (nil? (cfo/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match passed `:token-attributes`" (t/testing "doesn't match passed `:token-attributes`"
(t/is (nil? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y}))))) (t/is (nil? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
(t/deftest shapes-ids-by-applied-attributes (t/deftest shapes-ids-by-applied-attributes
(t/testing "Returns set of matched attributes that fit the applied token" (t/testing "Returns set of matched attributes that fit the applied token"
@@ -54,7 +54,7 @@
shape-applied-x-y shape-applied-x-y
shape-applied-all shape-applied-all
shape-applied-none] shape-applied-none]
expected (cft/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)] expected (cfo/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
(t/is (= (:x expected) (shape-ids shape-applied-x (t/is (= (:x expected) (shape-ids shape-applied-x
shape-applied-x-y shape-applied-x-y
shape-applied-all))) shape-applied-all)))
@@ -62,34 +62,21 @@
shape-applied-x-y shape-applied-x-y
shape-applied-all))) shape-applied-all)))
(t/is (= (:z expected) (shape-ids shape-applied-all))) (t/is (= (:z expected) (shape-ids shape-applied-all)))
(t/is (true? (cft/shapes-applied-all? expected (shape-ids shape-applied-all) attributes))) (t/is (true? (cfo/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
(t/is (false? (cft/shapes-applied-all? expected (apply shape-ids shapes) attributes))) (t/is (false? (cfo/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
(shape-ids shape-applied-x (shape-ids shape-applied-x
shape-applied-x-y shape-applied-x-y
shape-applied-all)))) shape-applied-all))))
(t/deftest tokens-applied-test (t/deftest tokens-applied-test
(t/testing "is true when single shape matches the token and attributes" (t/testing "is true when single shape matches the token and attributes"
(t/is (true? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}} (t/is (true? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "b"}}] {:applied-tokens {:x "b"}}]
#{:x})))) #{:x}))))
(t/testing "is false when no shape matches the token or attributes" (t/testing "is false when no shape matches the token or attributes"
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}} (t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
{:applied-tokens {:x "b"}}] {:applied-tokens {:x "b"}}]
#{:x}))) #{:x})))
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}} (t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "a"}}] {:applied-tokens {:x "a"}}]
#{:y}))))) #{:y})))))
(t/deftest name->path-test
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo.bar.baz")))
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz")))
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz...."))))
(t/deftest token-name-path-exists?-test
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
(t/is (true? (cft/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
(t/is (true? (cft/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
(t/is (false? (cft/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
(t/is (false? (cft/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))

View File

@@ -255,28 +255,28 @@
(cls/generate-update-shapes [(:id frame1)] (cls/generate-update-shapes [(:id frame1)]
(fn [shape] (fn [shape]
(-> shape (-> shape
(cto/unapply-token-id [:r1 :r2 :r3 :r4]) (cto/unapply-tokens-from-shape [:r1 :r2 :r3 :r4])
(cto/unapply-token-id [:rotation]) (cto/unapply-tokens-from-shape [:rotation])
(cto/unapply-token-id [:opacity]) (cto/unapply-tokens-from-shape [:opacity])
(cto/unapply-token-id [:stroke-width]) (cto/unapply-tokens-from-shape [:stroke-width])
(cto/unapply-token-id [:stroke-color]) (cto/unapply-tokens-from-shape [:stroke-color])
(cto/unapply-token-id [:fill]) (cto/unapply-tokens-from-shape [:fill])
(cto/unapply-token-id [:width :height]))) (cto/unapply-tokens-from-shape [:width :height])))
(:objects page) (:objects page)
{}) {})
(cls/generate-update-shapes [(:id text1)] (cls/generate-update-shapes [(:id text1)]
(fn [shape] (fn [shape]
(-> shape (-> shape
(cto/unapply-token-id [:font-size]) (cto/unapply-tokens-from-shape [:font-size])
(cto/unapply-token-id [:letter-spacing]) (cto/unapply-tokens-from-shape [:letter-spacing])
(cto/unapply-token-id [:font-family]))) (cto/unapply-tokens-from-shape [:font-family])))
(:objects page) (:objects page)
{}) {})
(cls/generate-update-shapes [(:id circle1)] (cls/generate-update-shapes [(:id circle1)]
(fn [shape] (fn [shape]
(-> shape (-> shape
(cto/unapply-token-id [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w]) (cto/unapply-tokens-from-shape [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
(cto/unapply-token-id [:m1 :m2 :m3 :m4]))) (cto/unapply-tokens-from-shape [:m1 :m2 :m3 :m4])))
(:objects page) (:objects page)
{})) {}))

View File

@@ -8,20 +8,19 @@
(:require (:require
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.uuid :as uuid]
[clojure.test :as t])) [clojure.test :as t]))
(t/deftest test-valid-token-name-schema (t/deftest test-valid-token-name-schema
;; Allow regular namespace token names ;; Allow regular namespace token names
(t/is (true? (sm/validate cto/token-name-ref "Foo"))) (t/is (true? (sm/validate cto/schema:token-name "Foo")))
(t/is (true? (sm/validate cto/token-name-ref "foo"))) (t/is (true? (sm/validate cto/schema:token-name "foo")))
(t/is (true? (sm/validate cto/token-name-ref "FOO"))) (t/is (true? (sm/validate cto/schema:token-name "FOO")))
(t/is (true? (sm/validate cto/token-name-ref "Foo.Bar.Baz"))) (t/is (true? (sm/validate cto/schema:token-name "Foo.Bar.Baz")))
;; Disallow trailing tokens ;; Disallow trailing tokens
(t/is (false? (sm/validate cto/token-name-ref "Foo.Bar.Baz...."))) (t/is (false? (sm/validate cto/schema:token-name "Foo.Bar.Baz....")))
;; Disallow multiple separator dots ;; Disallow multiple separator dots
(t/is (false? (sm/validate cto/token-name-ref "Foo..Bar.Baz"))) (t/is (false? (sm/validate cto/schema:token-name "Foo..Bar.Baz")))
;; Disallow any special characters ;; Disallow any special characters
(t/is (false? (sm/validate cto/token-name-ref "Hey Foo.Bar"))) (t/is (false? (sm/validate cto/schema:token-name "Hey Foo.Bar")))
(t/is (false? (sm/validate cto/token-name-ref "Hey😈Foo.Bar"))) (t/is (false? (sm/validate cto/schema:token-name "Hey😈Foo.Bar")))
(t/is (false? (sm/validate cto/token-name-ref "Hey%Foo.Bar")))) (t/is (false? (sm/validate cto/schema:token-name "Hey%Foo.Bar"))))

View File

@@ -678,35 +678,35 @@
(t/deftest list-active-themes-tokens-bug-taiga-10617 (t/deftest list-active-themes-tokens-bug-taiga-10617
(let [tokens-lib (-> (ctob/make-tokens-lib) (let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "Mode / Dark" (ctob/add-set (ctob/make-token-set :name "Mode/Dark"
:tokens {"red" :tokens {"red"
(ctob/make-token :name "red" (ctob/make-token :name "red"
:type :color :type :color
:value "#700000")})) :value "#700000")}))
(ctob/add-set (ctob/make-token-set :name "Mode / Light" (ctob/add-set (ctob/make-token-set :name "Mode/Light"
:tokens {"red" :tokens {"red"
(ctob/make-token :name "red" (ctob/make-token :name "red"
:type :color :type :color
:value "#ff0000")})) :value "#ff0000")}))
(ctob/add-set (ctob/make-token-set :name "Device / Desktop" (ctob/add-set (ctob/make-token-set :name "Device/Desktop"
:tokens {"border1" :tokens {"border1"
(ctob/make-token :name "border1" (ctob/make-token :name "border1"
:type :border-radius :type :border-radius
:value 30)})) :value 30)}))
(ctob/add-set (ctob/make-token-set :name "Device / Mobile" (ctob/add-set (ctob/make-token-set :name "Device/Mobile"
:tokens {"border1" :tokens {"border1"
(ctob/make-token :name "border1" (ctob/make-token :name "border1"
:type :border-radius :type :border-radius
:value 50)})) :value 50)}))
(ctob/add-theme (ctob/make-token-theme :group "App" (ctob/add-theme (ctob/make-token-theme :group "App"
:name "Mobile" :name "Mobile"
:sets #{"Mode / Dark" "Device / Mobile"})) :sets #{"Mode/Dark" "Device/Mobile"}))
(ctob/add-theme (ctob/make-token-theme :group "App" (ctob/add-theme (ctob/make-token-theme :group "App"
:name "Web" :name "Web"
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop"})) :sets #{"Mode/Dark" "Mode/Light" "Device/Desktop"}))
(ctob/add-theme (ctob/make-token-theme :group "Brand" (ctob/add-theme (ctob/make-token-theme :group "Brand"
:name "Brand A" :name "Brand A"
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop" "Device / Mobile"})) :sets #{"Mode/Dark" "Mode/Light" "Device/Desktop" "Device/Mobile"}))
(ctob/add-theme (ctob/make-token-theme :group "Brand" (ctob/add-theme (ctob/make-token-theme :group "Brand"
:name "Brand B" :name "Brand B"
:sets #{})) :sets #{}))
@@ -2013,3 +2013,11 @@
(t/is (some? imported-ref)) (t/is (some? imported-ref))
(t/is (= (:type original-ref) (:type imported-ref))) (t/is (= (:type original-ref) (:type imported-ref)))
(t/is (= (:value imported-ref) (:value original-ref)))))))) (t/is (= (:value imported-ref) (:value original-ref))))))))
(t/deftest token-name-path-exists?-test
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
(t/is (false? (ctob/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
(t/is (false? (ctob/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))

View File

@@ -8,7 +8,7 @@
(:require (:require
["@tokens-studio/sd-transforms" :as sd-transforms] ["@tokens-studio/sd-transforms" :as sd-transforms]
["style-dictionary$default" :as sd] ["style-dictionary$default" :as sd]
[app.common.files.tokens :as cft] [app.common.files.tokens :as cfo]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
@@ -83,7 +83,7 @@
[value] [value]
(let [number? (or (number? value) (let [number? (or (number? value)
(numeric-string? value)) (numeric-string? value))
parsed-value (cft/parse-token-value value) parsed-value (cfo/parse-token-value value)
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int) out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
(<= (:value parsed-value) sm/min-safe-int))] (<= (:value parsed-value) sm/min-safe-int))]
@@ -109,7 +109,7 @@
"Parses `value` of a number `sd-token` into a map like `{:value 1 :unit \"px\"}`. "Parses `value` of a number `sd-token` into a map like `{:value 1 :unit \"px\"}`.
If the `value` is not parseable and/or has missing references returns a map with `:errors`." If the `value` is not parseable and/or has missing references returns a map with `:errors`."
[value] [value]
(let [parsed-value (cft/parse-token-value value) (let [parsed-value (cfo/parse-token-value value)
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int) out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
(<= (:value parsed-value) sm/min-safe-int))] (<= (:value parsed-value) sm/min-safe-int))]
(if (and parsed-value (not out-of-bounds)) (if (and parsed-value (not out-of-bounds))
@@ -127,7 +127,7 @@
If the `value` is parseable but is out of range returns a map with `warnings`." If the `value` is parseable but is out of range returns a map with `warnings`."
[value] [value]
(let [missing-references? (seq (seq (cto/find-token-value-references value))) (let [missing-references? (seq (seq (cto/find-token-value-references value)))
parsed-value (cft/parse-token-value value) parsed-value (cfo/parse-token-value value)
out-of-scope (not (<= 0 (:value parsed-value) 1)) out-of-scope (not (<= 0 (:value parsed-value) 1))
references (seq (cto/find-token-value-references value))] references (seq (cto/find-token-value-references value))]
(cond (and parsed-value (not out-of-scope)) (cond (and parsed-value (not out-of-scope))
@@ -151,7 +151,7 @@
If the `value` is parseable but is out of range returns a map with `warnings`." If the `value` is parseable but is out of range returns a map with `warnings`."
[value] [value]
(let [missing-references? (seq (cto/find-token-value-references value)) (let [missing-references? (seq (cto/find-token-value-references value))
parsed-value (cft/parse-token-value value) parsed-value (cfo/parse-token-value value)
out-of-scope (< (:value parsed-value) 0) out-of-scope (< (:value parsed-value) 0)
references (seq (cto/find-token-value-references value))] references (seq (cto/find-token-value-references value))]
(cond (cond
@@ -250,7 +250,7 @@
:font-size-value font-size-value})] :font-size-value font-size-value})]
(or error (or error
(try (try
(when-let [{:keys [unit value]} (cft/parse-token-value line-height-value)] (when-let [{:keys [unit value]} (cfo/parse-token-value line-height-value)]
(case unit (case unit
"%" (/ value 100) "%" (/ value 100)
"px" (/ value font-size-value) "px" (/ value font-size-value)

View File

@@ -7,7 +7,7 @@
(ns app.main.data.workspace.tokens.application (ns app.main.data.workspace.tokens.application
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.files.tokens :as cft] [app.common.files.tokens :as cfo]
[app.common.types.component :as ctk] [app.common.types.component :as ctk]
[app.common.types.shape.layout :as ctsl] [app.common.types.shape.layout :as ctsl]
[app.common.types.shape.radius :as ctsr] [app.common.types.shape.radius :as ctsr]
@@ -525,8 +525,8 @@
shape-ids (d/nilv (keys shapes) []) shape-ids (d/nilv (keys shapes) [])
any-variant? (->> shapes vals (some ctk/is-variant?) boolean) any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value]) resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
tokenized-attributes (cft/attributes-map attributes token) tokenized-attributes (cfo/attributes-map attributes token)
type (:type token)] type (:type token)]
(rx/concat (rx/concat
(rx/of (rx/of
@@ -585,7 +585,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (rx/of
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))] (let [remove-token #(when % (cfo/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes (dwsh/update-shapes
shape-ids shape-ids
(fn [shape] (fn [shape]
@@ -613,7 +613,7 @@
(get token-properties (:type token)) (get token-properties (:type token))
unapply-tokens? unapply-tokens?
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes)) (cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
shape-ids (map :id shapes)] shape-ids (map :id shapes)]

View File

@@ -6,7 +6,7 @@
(ns app.main.data.workspace.tokens.color (ns app.main.data.workspace.tokens.color
(:require (:require
[app.common.files.tokens :as cft] [app.common.files.tokens :as cfo]
[app.main.data.tinycolor :as tinycolor])) [app.main.data.tinycolor :as tinycolor]))
(defn color-bullet-color [token-color-value] (defn color-bullet-color [token-color-value]
@@ -17,5 +17,5 @@
(tinycolor/->hex-string tc)))) (tinycolor/->hex-string tc))))
(defn resolved-token-bullet-color [{:keys [resolved-value] :as token}] (defn resolved-token-bullet-color [{:keys [resolved-value] :as token}]
(when (and resolved-value (cft/color-token? token)) (when (and resolved-value (cfo/color-token? token))
(color-bullet-color resolved-value))) (color-bullet-color resolved-value)))

View File

@@ -147,27 +147,30 @@
(defn create-token-set (defn create-token-set
[token-set] [token-set]
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema?
(ptk/reify ::create-token-set (ptk/reify ::create-token-set
ptk/UpdateEvent
(update [_ state]
;; Clear possible local state
(update state :workspace-tokens dissoc :token-set-new-path))
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [data (dsh/lookup-file-data state) (let [data (dsh/lookup-file-data state)
tokens-lib (get data :tokens-lib) changes (-> (pcb/empty-changes it)
token-set (ctob/rename token-set (ctob/normalize-set-name (ctob/get-name token-set)))]
(if (and tokens-lib (ctob/get-set-by-name tokens-lib (ctob/get-name token-set)))
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000}))
(let [changes (-> (pcb/empty-changes it)
(pcb/with-library-data data) (pcb/with-library-data data)
(pcb/set-token-set (ctob/get-id token-set) token-set))] (pcb/set-token-set (ctob/get-id token-set) token-set))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set)) (rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes)))))))) (dch/commit-changes changes))))))
(defn rename-token-set
[token-set new-name]
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema after renaming?
(assert (string? new-name) "a new name is required") ;; TODO should assert normalized-set-name?
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/rename-token-set (ctob/get-id token-set) new-name))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))
(defn rename-token-set-group (defn rename-token-set-group
[set-group-path set-group-fname] [set-group-path set-group-fname]
@@ -179,26 +182,6 @@
(rx/of (rx/of
(dch/commit-changes changes)))))) (dch/commit-changes changes))))))
(defn update-token-set
[token-set name]
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
name (ctob/normalize-set-name name (ctob/get-name token-set))
tokens-lib (get data :tokens-lib)]
(if (ctob/get-set-by-name tokens-lib name)
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000}))
(let [changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/rename-token-set (ctob/get-id token-set) name))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))))
(defn duplicate-token-set (defn duplicate-token-set
[id] [id]
(ptk/reify ::duplicate-token-set (ptk/reify ::duplicate-token-set
@@ -448,7 +431,7 @@
(ctob/get-id token-set) (ctob/get-id token-set)
token-id)] token-id)]
(let [tokens (vals (ctob/get-tokens tokens-lib (ctob/get-id token-set))) (let [tokens (vals (ctob/get-tokens tokens-lib (ctob/get-id token-set)))
unames (map :name tokens) unames (map :name tokens) ;; TODO: add function duplicate-token in tokens-lib
suffix (tr "workspace.tokens.duplicate-suffix") suffix (tr "workspace.tokens.duplicate-suffix")
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix) copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)
new-token (-> token new-token (-> token

View File

@@ -9,7 +9,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.tokens :as cft] [app.common.files.tokens :as cfo]
[app.common.types.shape.layout :as ctsl] [app.common.types.shape.layout :as ctsl]
[app.common.types.token :as ctt] [app.common.types.token :as ctt]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@@ -47,9 +47,9 @@
;; Actions --------------------------------------------------------------------- ;; Actions ---------------------------------------------------------------------
(defn attribute-actions [token selected-shapes attributes] (defn attribute-actions [token selected-shapes attributes]
(let [ids-by-attributes (cft/shapes-ids-by-applied-attributes token selected-shapes attributes) (let [ids-by-attributes (cfo/shapes-ids-by-applied-attributes token selected-shapes attributes)
shape-ids (into #{} (map :id selected-shapes))] shape-ids (into #{} (map :id selected-shapes))]
{:all-selected? (cft/shapes-applied-all? ids-by-attributes shape-ids attributes) {:all-selected? (cfo/shapes-applied-all? ids-by-attributes shape-ids attributes)
:shape-ids shape-ids :shape-ids shape-ids
:selected-pred #(seq (% ids-by-attributes))})) :selected-pred #(seq (% ids-by-attributes))}))

View File

@@ -0,0 +1,223 @@
;; 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) KALEIDOS INC
(ns app.main.ui.workspace.tokens.management.create.color
(:require-macros [app.main.style :as stl])
(:require
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.modal :as modal]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.tokens.management.create.form-color-input-token :refer [form-color-input-token*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(defn- make-schema
[tokens-tree]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(not (cto/token-value-self-reference? name value))))]]))
(mf/defc form*
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
(let [token
(mf/with-memo [token]
(or token {:type :color}))
token-type
(get token :type)
token-properties
(dwta/get-token-properties token)
token-title (str/lower (:title token-properties))
tokens
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens
(mf/with-memo [tokens token]
;; Ensure that the resolved value uses the currently editing token
;; even if the name has been overriden by a token with the same name
;; in another set below.
(cond-> tokens
(and (:name token) (:value token))
(assoc (:name token) token)))
schema
(mf/with-memo [tokens-tree-in-selected-set]
(make-schema tokens-tree-in-selected-set))
initial
(mf/with-memo [token]
{:name (:name token "")
:value (:value token "")
:description (:description token "")})
form
(fm/use-form :schema schema
:initial initial)
warning-name-change?
(not= (get-in @form [:data :name])
(:name initial))
on-cancel
(mf/use-fn
(fn [e]
(dom/prevent-default e)
(modal/hide!)))
on-delete-token
(mf/use-fn
(mf/deps selected-token-set-id token)
(fn [e]
(dom/prevent-default e)
(modal/hide!)
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
handle-key-down-delete
(mf/use-fn
(mf/deps on-delete-token)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(on-delete-token e))))
handle-key-down-cancel
(mf/use-fn
(mf/deps on-cancel)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(on-cancel e))))
on-submit
(mf/use-fn
(mf/deps validate-token token tokens token-type)
(fn [form _event]
(let [name (get-in @form [:clean-data :name])
description (get-in @form [:clean-data :description])
value (get-in @form [:clean-data :value])]
(->> (validate-token {:token-value value
:token-name name
:token-description description
:prev-token token
:tokens tokens})
(rx/subs!
(fn [valid-token]
(st/emit!
(if is-create
(dwtl/create-token (ctob/make-token {:name name
:type token-type
:value (:value valid-token)
:description description}))
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description}))
(dwtp/propagate-workspace-tokens)
(modal/hide))))))))]
[:> fc/form* {:class (stl/css :form-wrapper)
:form form
:on-submit on-submit}
[:div {:class (stl/css :token-rows)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
(tr "workspace.tokens.create-token" token-type)]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-name"
:name :name
:label (tr "workspace.tokens.token-name")
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
:max-length max-input-length
:variant "comfortable"
:auto-focus true}]
(when (and warning-name-change? (= action "edit"))
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
[:> context-notification*
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
[:div {:class (stl/css :input-row)}
[:> form-color-input-token*
{:placeholder (tr "workspace.tokens.token-value-enter")
:label (tr "workspace.tokens.token-value")
:name :value
:token token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-description"
:name :description
:label (tr "workspace.tokens.token-description")
:placeholder (tr "workspace.tokens.token-description")
:max-length max-input-length
:variant "comfortable"
:is-optional true}]]
[:div {:class (stl/css-case :button-row true
:with-delete (= action "edit"))}
(when (= action "edit")
[:> button* {:on-click on-delete-token
:on-key-down handle-key-down-delete
:class (stl/css :delete-btn)
:type "button"
:icon i/delete
:variant "secondary"}
(tr "labels.delete")])
[:> button* {:on-click on-cancel
:on-key-down handle-key-down-cancel
:type "button"
:id "token-modal-cancel"
:variant "secondary"}
(tr "labels.cancel")]
[:> fc/form-submit* {:variant "primary"
:on-submit on-submit}
(tr "labels.save")]]]]))

View File

@@ -0,0 +1,222 @@
;; 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) KALEIDOS INC
(ns app.main.ui.workspace.tokens.management.create.dimensions
(:require-macros [app.main.style :as stl])
(:require
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.modal :as modal]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.tokens.management.create.form-input-token :refer [form-input-token*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(defn- make-schema
[tokens-tree]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(not (cto/token-value-self-reference? name value))))]]))
(mf/defc form*
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
(let [token
(mf/with-memo [token]
(or token {:type :dimensions}))
token-type
(get token :type)
token-properties
(dwta/get-token-properties token)
token-title (str/lower (:title token-properties))
tokens
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens
(mf/with-memo [tokens token]
;; Ensure that the resolved value uses the currently editing token
;; even if the name has been overriden by a token with the same name
;; in another set below.
(cond-> tokens
(and (:name token) (:value token))
(assoc (:name token) token)))
schema
(mf/with-memo [tokens-tree-in-selected-set]
(make-schema tokens-tree-in-selected-set))
initial
(mf/with-memo [token]
{:name (:name token "")
:value (:value token "")
:description (:description token "")})
form
(fm/use-form :schema schema
:initial initial)
warning-name-change?
(not= (get-in @form [:data :name])
(:name initial))
on-cancel
(mf/use-fn
(fn [e]
(dom/prevent-default e)
(modal/hide!)))
on-delete-token
(mf/use-fn
(mf/deps selected-token-set-id token)
(fn [e]
(dom/prevent-default e)
(modal/hide!)
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
handle-key-down-delete
(mf/use-fn
(mf/deps on-delete-token)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(on-delete-token e))))
handle-key-down-cancel
(mf/use-fn
(mf/deps on-cancel)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(on-cancel e))))
on-submit
(mf/use-fn
(mf/deps validate-token token tokens token-type)
(fn [form _event]
(let [name (get-in @form [:clean-data :name])
description (get-in @form [:clean-data :description])
value (get-in @form [:clean-data :value])]
(->> (validate-token {:token-value value
:token-name name
:token-description description
:prev-token token
:tokens tokens})
(rx/subs!
(fn [valid-token]
(st/emit!
(if is-create
(dwtl/create-token (ctob/make-token {:name name
:type token-type
:value (:value valid-token)
:description description}))
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description}))
(dwtp/propagate-workspace-tokens)
(modal/hide))))))))]
[:> fc/form* {:class (stl/css :form-wrapper)
:form form
:on-submit on-submit}
[:div {:class (stl/css :token-rows)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
(tr "workspace.tokens.create-token" token-type)]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-name"
:name :name
:label (tr "workspace.tokens.token-name")
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
:max-length max-input-length
:variant "comfortable"
:auto-focus true}]
(when (and warning-name-change? (= action "edit"))
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
[:> context-notification*
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
[:div {:class (stl/css :input-row)}
[:> form-input-token*
{:placeholder (tr "workspace.tokens.token-value-enter")
:label (tr "workspace.tokens.token-value")
:name :value
:token token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-description"
:name :description
:label (tr "workspace.tokens.token-description")
:placeholder (tr "workspace.tokens.token-description")
:max-length max-input-length
:variant "comfortable"
:is-optional true}]]
[:div {:class (stl/css-case :button-row true
:with-delete (= action "edit"))}
(when (= action "edit")
[:> button* {:on-click on-delete-token
:on-key-down handle-key-down-delete
:class (stl/css :delete-btn)
:type "button"
:icon i/delete
:variant "secondary"}
(tr "labels.delete")])
[:> button* {:on-click on-cancel
:on-key-down handle-key-down-cancel
:type "button"
:id "token-modal-cancel"
:variant "secondary"}
(tr "labels.cancel")]
[:> fc/form-submit* {:variant "primary"
:on-submit on-submit}
(tr "labels.save")]]]]))

View File

@@ -0,0 +1,220 @@
;; 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) KALEIDOS INC
(ns app.main.ui.workspace.tokens.management.create.font-family
(:require-macros [app.main.style :as stl])
(:require
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.modal :as modal]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.tokens.management.create.combobox-token-fonts :refer [font-picker-combobox*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- make-schema
[tokens-tree]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value ::sm/text]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(not (cto/token-value-self-reference? name value))))]]))
(mf/defc form*
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
(let [token
(mf/with-memo [token]
(if token
(update token :value cto/join-font-family)
{:type :font-family}))
token-type
(get token :type)
token-properties
(dwta/get-token-properties token)
token-title (str/lower (:title token-properties))
tokens
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens
(mf/with-memo [tokens token]
;; Ensure that the resolved value uses the currently editing token
;; even if the name has been overriden by a token with the same name
;; in another set below.
(cond-> tokens
(and (:name token) (:value token))
(assoc (:name token) token)))
schema
(mf/with-memo [tokens-tree-in-selected-set]
(make-schema tokens-tree-in-selected-set))
initial
(mf/with-memo [token]
{:name (:name token "")
:value (:value token "")
:description (:description token "")})
form
(fm/use-form :schema schema
:initial initial)
warning-name-change?
(not= (get-in @form [:data :name])
(:name initial))
on-cancel
(mf/use-fn
(fn [e]
(dom/prevent-default e)
(modal/hide!)))
on-delete-token
(mf/use-fn
(mf/deps selected-token-set-id token)
(fn [e]
(dom/prevent-default e)
(modal/hide!)
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
handle-key-down-delete
(mf/use-fn
(mf/deps on-delete-token)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(on-delete-token e))))
handle-key-down-cancel
(mf/use-fn
(mf/deps on-cancel)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(on-cancel e))))
on-submit
(mf/use-fn
(mf/deps validate-token token tokens token-type)
(fn [form _event]
(let [name (get-in @form [:clean-data :name])
description (get-in @form [:clean-data :description])
value (get-in @form [:clean-data :value])]
(->> (validate-token {:token-value value
:token-name name
:token-description description
:prev-token token
:tokens tokens})
(rx/subs!
(fn [valid-token]
(st/emit!
(if is-create
(dwtl/create-token (ctob/make-token {:name name
:type token-type
:value (:value valid-token)
:description description}))
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description}))
(dwtp/propagate-workspace-tokens)
(modal/hide))))))))]
[:> fc/form* {:class (stl/css :form-wrapper)
:form form
:on-submit on-submit}
[:div {:class (stl/css :token-rows)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
(tr "workspace.tokens.create-token" token-type)]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-name"
:name :name
:label (tr "workspace.tokens.token-name")
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
:max-length max-input-length
:variant "comfortable"
:auto-focus true}]
(when (and warning-name-change? (= action "edit"))
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
[:> context-notification*
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
[:div {:class (stl/css :input-row)}
[:> font-picker-combobox*
{:placeholder (tr "workspace.tokens.token-value-enter")
:label (tr "workspace.tokens.token-value")
:name :value
:token token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-description"
:name :description
:label (tr "workspace.tokens.token-description")
:placeholder (tr "workspace.tokens.token-description")
:max-length max-input-length
:variant "comfortable"
:is-optional true}]]
[:div {:class (stl/css-case :button-row true
:with-delete (= action "edit"))}
(when (= action "edit")
[:> button* {:on-click on-delete-token
:on-key-down handle-key-down-delete
:class (stl/css :delete-btn)
:type "button"
:icon i/delete
:variant "secondary"}
(tr "labels.delete")])
[:> button* {:on-click on-cancel
:on-key-down handle-key-down-cancel
:type "button"
:id "token-modal-cancel"
:variant "secondary"}
(tr "labels.cancel")]
[:> fc/form-submit* {:variant "primary"
:on-submit on-submit}
(tr "labels.save")]]]]))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,223 @@
;; 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) KALEIDOS INC
(ns app.main.ui.workspace.tokens.management.create.text-case
(:require-macros [app.main.style :as stl])
(:require
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.modal :as modal]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.tokens.management.create.form-input-token :refer [form-input-token*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(defn- make-schema
[tokens-tree]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(not (cto/token-value-self-reference? name value))))]]))
(mf/defc form*
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
(let [token
(mf/with-memo [token]
(or token {:type :text-case}))
token-type
(get token :type)
token-properties
(dwta/get-token-properties token)
token-title (str/lower (:title token-properties))
tokens
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens
(mf/with-memo [tokens token]
;; Ensure that the resolved value uses the currently editing token
;; even if the name has been overriden by a token with the same name
;; in another set below.
(cond-> tokens
(and (:name token) (:value token))
(assoc (:name token) token)))
schema
(mf/with-memo [tokens-tree-in-selected-set]
(make-schema tokens-tree-in-selected-set))
initial
(mf/with-memo [token]
{:name (:name token "")
:value (:value token "")
:description (:description token "")})
form
(fm/use-form :schema schema
:initial initial)
warning-name-change?
(not= (get-in @form [:data :name])
(:name initial))
on-cancel
(mf/use-fn
(fn [e]
(dom/prevent-default e)
(modal/hide!)))
on-delete-token
(mf/use-fn
(mf/deps selected-token-set-id token)
(fn [e]
(dom/prevent-default e)
(modal/hide!)
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
handle-key-down-delete
(mf/use-fn
(mf/deps on-delete-token)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(on-delete-token e))))
handle-key-down-cancel
(mf/use-fn
(mf/deps on-cancel)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(on-cancel e))))
on-submit
(mf/use-fn
(mf/deps validate-token token tokens token-type)
(fn [form _event]
(let [name (get-in @form [:clean-data :name])
description (get-in @form [:clean-data :description])
value (get-in @form [:clean-data :value])]
(->> (validate-token {:token-value value
:token-name name
:token-description description
:prev-token token
:tokens tokens})
(rx/subs!
(fn [valid-token]
(st/emit!
(if is-create
(dwtl/create-token (ctob/make-token {:name name
:type token-type
:value (:value valid-token)
:description description}))
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description}))
(dwtp/propagate-workspace-tokens)
(modal/hide))))))))]
[:> fc/form* {:class (stl/css :form-wrapper)
:form form
:on-submit on-submit}
[:div {:class (stl/css :token-rows)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
(tr "workspace.tokens.create-token" token-type)]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-name"
:name :name
:label (tr "workspace.tokens.token-name")
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
:max-length max-input-length
:variant "comfortable"
:auto-focus true}]
(when (and warning-name-change? (= action "edit"))
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
[:> context-notification*
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
[:div {:class (stl/css :input-row)}
[:> form-input-token*
{:placeholder (tr "workspace.tokens.text-case-value-enter")
:label (tr "workspace.tokens.token-value")
:name :value
:token token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-description"
:name :description
:label (tr "workspace.tokens.token-description")
:placeholder (tr "workspace.tokens.token-description")
:max-length max-input-length
:variant "comfortable"
:is-optional true}]]
[:div {:class (stl/css-case :button-row true
:with-delete (= action "edit"))}
(when (= action "edit")
[:> button* {:on-click on-delete-token
:on-key-down handle-key-down-delete
:class (stl/css :delete-btn)
:type "button"
:icon i/delete
:variant "secondary"}
(tr "labels.delete")])
[:> button* {:on-click on-cancel
:on-key-down handle-key-down-cancel
:type "button"
:id "token-modal-cancel"
:variant "secondary"}
(tr "labels.cancel")]
[:> fc/form-submit* {:variant "primary"
:on-submit on-submit}
(tr "labels.save")]]]]))

View File

@@ -0,0 +1,427 @@
;; 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) KALEIDOS INC
(ns app.main.ui.workspace.tokens.management.create.typography
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.modal :as modal]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.forms :as forms]
[app.main.ui.workspace.tokens.management.create.combobox-token-fonts :refer [font-picker-composite-combobox*]]
[app.main.ui.workspace.tokens.management.create.form-input-token :refer [token-composite-value-input*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(mf/defc composite-form*
[{:keys [token tokens] :as props}]
(let [letter-spacing-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :letter-spacing
:value (cto/join-font-family (get value :letter-spacing))}
{:type :letter-spacing}))
font-family-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :font-family
:value (get value :font-family)}
{:type :font-family}))
font-size-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :font-size
:value (get value :font-size)}
{:type :font-size}))
font-weight-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :font-weight
:value (get value :font-weight)}
{:type :font-weight}))
;; TODO: Review this type
line-height-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :number
:value (get value :line-height)}
{:type :number}))
text-case-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :text-case
:value (get value :text-case)}
{:type :text-case}))
text-decoration-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :text-decoration
:value (get value :text-decoration)}
{:type :text-decoration}))]
[:*
[:div {:class (stl/css :input-row)}
[:> font-picker-composite-combobox*
{:icon i/text-font-family
:placeholder (tr "workspace.tokens.token-font-family-value-enter")
:aria-label (tr "workspace.tokens.token-font-family-value")
:name :font-family
:token font-family-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> token-composite-value-input*
{:aria-label "Font Size"
:icon i/text-font-size
:placeholder (tr "workspace.tokens.font-size-value-enter")
:name :font-size
:token font-size-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> token-composite-value-input*
{:aria-label "Font Weight"
:icon i/text-font-weight
:placeholder (tr "workspace.tokens.font-weight-value-enter")
:name :font-weight
:token font-weight-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> token-composite-value-input*
{:aria-label "Line Height"
:icon i/text-lineheight
:placeholder (tr "workspace.tokens.line-height-value-enter")
:name :line-height
:token line-height-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> token-composite-value-input*
{:aria-label "Letter Spacing"
:icon i/text-letterspacing
:placeholder (tr "workspace.tokens.letter-spacing-value-enter-composite")
:name :letter-spacing
:token letter-spacing-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> token-composite-value-input*
{:aria-label "Text Case"
:icon i/text-mixed
:placeholder (tr "workspace.tokens.text-case-value-enter")
:name :text-case
:token text-case-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> token-composite-value-input*
{:aria-label "Text Decoration"
:icon i/text-underlined
:placeholder (tr "workspace.tokens.text-decoration-value-enter")
:name :text-decoration
:token text-decoration-sub-token
:tokens tokens}]]]))
(mf/defc reference-form*
[{:keys [token tokens] :as props}]
[:div {:class (stl/css :input-row)}
[:> token-composite-value-input*
{:placeholder (tr "workspace.tokens.reference-composite")
:aria-label (tr "labels.reference")
:icon i/text-typography
:name :reference
:token token
:tokens tokens}]])
(defn- make-schema
[tokens-tree active-tab]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/schema:token-name assoc
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value
[:map
[:font-family {:optional true} [:maybe :string]]
[:font-size {:optional true} [:maybe :string]]
[:font-weight {:optional true} [:maybe :string]]
[:line-height {:optional true} [:maybe :string]]
[:letter-spacing {:optional true} [:maybe :string]]
[:text-case {:optional true} [:maybe :string]]
[:text-decoration {:optional true} [:maybe :string]]
(if (= active-tab :reference)
[:reference {:optional false} ::sm/text]
[:reference {:optional true} [:maybe :string]])]]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field [:value :reference]
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(let [reference (get value :reference)]
(if (and reference name)
(not (cto/token-value-self-reference? name reference))
true)))]
[:fn {:error/field [:value :line-height]
:error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size")}
(fn [{:keys [value]}]
(let [line-heigh (get value :line-height)
font-size (get value :font-size)]
(if (and line-heigh (not font-size))
false
true)))]
;; This error does not shown on interface, it's just to avoid saving empty composite tokens
;; We don't need to translate it.
[:fn {:error/fn (fn [_] "At least one composite field must be set")
:error/field :value}
(fn [attrs]
(let [result (reduce-kv (fn [_ _ v]
(if (str/empty? v)
false
(reduced true)))
false
(get attrs :value))]
result))]]))
(mf/defc form*
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
(let [token
(mf/with-memo [token]
(or token {:type :typography}))
active-tab* (mf/use-state #(if (cfo/is-reference? token) :reference :composite))
active-tab (deref active-tab*)
token-type
(get token :type)
token-properties
(dwta/get-token-properties token)
token-title (str/lower (:title token-properties))
tokens
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens
(mf/with-memo [tokens token]
;; Ensure that the resolved value uses the currently editing token
;; even if the name has been overriden by a token with the same name
;; in another set below.
(cond-> tokens
(and (:name token) (:value token))
(assoc (:name token) token)))
schema
(mf/with-memo [tokens-tree-in-selected-set active-tab]
(make-schema tokens-tree-in-selected-set active-tab))
initial
(mf/with-memo [token]
(let [value (:value token)
processed-value
(cond
(string? value)
{:reference value}
(map? value)
(let [value (cond-> value
(:font-family value)
(update :font-family cto/join-font-family))]
(select-keys value
[:font-family
:font-size
:font-weight
:line-height
:letter-spacing
:text-case
:text-decoration]))
:else
{})]
{:name (:name token "")
:value processed-value
:description (:description token "")}))
form
(fm/use-form :schema schema
:initial initial)
warning-name-change?
(not= (get-in @form [:data :name])
(:name initial))
on-toggle-tab
(mf/use-fn
(mf/deps)
(fn [new-tab]
(let [new-tab (keyword new-tab)]
(reset! active-tab* new-tab))))
on-cancel
(mf/use-fn
(fn [e]
(dom/prevent-default e)
(modal/hide!)))
on-delete-token
(mf/use-fn
(mf/deps selected-token-set-id token)
(fn [e]
(dom/prevent-default e)
(modal/hide!)
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
handle-key-down-delete
(mf/use-fn
(mf/deps on-delete-token)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(on-delete-token e))))
handle-key-down-cancel
(mf/use-fn
(mf/deps on-cancel)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(on-cancel e))))
on-submit
(mf/use-fn
(mf/deps validate-token token tokens token-type)
(fn [form _event]
(let [name (get-in @form [:clean-data :name])
description (get-in @form [:clean-data :description])
value (get-in @form [:clean-data :value])]
(->> (validate-token {:token-value (if (contains? value :reference)
(get value :reference)
value)
:token-name name
:token-description description
:prev-token token
:tokens tokens})
(rx/subs!
(fn [valid-token]
(st/emit!
(if is-create
(dwtl/create-token (ctob/make-token {:name name
:type token-type
:value (:value valid-token)
:description description}))
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description}))
(dwtp/propagate-workspace-tokens)
(modal/hide))))))))]
[:> forms/form* {:class (stl/css :form-wrapper)
:form form
:on-submit on-submit}
[:div {:class (stl/css :token-rows)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
(tr "workspace.tokens.create-token" token-type)]
[:div {:class (stl/css :input-row)}
[:> forms/form-input* {:id "token-name"
:name :name
:label (tr "workspace.tokens.token-name")
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
:max-length max-input-length
:variant "comfortable"
:auto-focus true}]
(when (and warning-name-change? (= action "edit"))
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
[:> context-notification*
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
[:div {:class (stl/css :title-bar)}
[:div {:class (stl/css :title)} (tr "labels.typography")]
[:& radio-buttons {:class (stl/css :listing-options)
:selected (d/name active-tab)
:on-change on-toggle-tab
:name "reference-composite-tab"}
[:& radio-button {:icon i/layers
:value "composite"
:title (tr "workspace.tokens.individual-tokens")
:id "composite-opt"}]
[:& radio-button {:icon i/tokens
:value "reference"
:title (tr "workspace.tokens.use-reference")
:id "reference-opt"}]]]
[:div {:class (stl/css :inputs-wrapper)}
(if (= active-tab :composite)
[:> composite-form* {:token token
:tokens tokens}]
[:> reference-form* {:token token
:tokens tokens}])]
[:div {:class (stl/css :input-row)}
[:> forms/form-input* {:id "token-description"
:name :description
:label (tr "workspace.tokens.token-description")
:placeholder (tr "workspace.tokens.token-description")
:max-length max-input-length
:variant "comfortable"
:is-optional true}]]
[:div {:class (stl/css-case :button-row true
:with-delete (= action "edit"))}
(when (= action "edit")
[:> button* {:on-click on-delete-token
:on-key-down handle-key-down-delete
:class (stl/css :delete-btn)
:type "button"
:icon i/delete
:variant "secondary"}
(tr "labels.delete")])
[:> button* {:on-click on-cancel
:on-key-down handle-key-down-cancel
:type "button"
:id "token-modal-cancel"
:variant "secondary"}
(tr "labels.cancel")]
[:> forms/form-submit* {:variant "primary"
:on-submit on-submit}
(tr "labels.save")]]]]))

View File

@@ -6,9 +6,9 @@
(ns app.main.ui.workspace.tokens.management.forms.color (ns app.main.ui.workspace.tokens.management.forms.color
(:require (:require
[app.common.files.tokens :as cft]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls] [app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic] [app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
@@ -29,9 +29,9 @@
[:name [:name
[:and [:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) (sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]] #(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]] [:value [::sm/text {:error/fn token-value-error-fn}]]

View File

@@ -28,7 +28,7 @@
token-path token-path
(mf/with-memo [token] (mf/with-memo [token]
(cft/token-name->path (:name token))) (ctob/get-token-path token))
tokens-tree-in-selected-set tokens-tree-in-selected-set
(mf/with-memo [token-path tokens-in-selected-set] (mf/with-memo [token-path tokens-in-selected-set]

View File

@@ -7,7 +7,7 @@
(ns app.main.ui.workspace.tokens.management.forms.generic-form (ns app.main.ui.workspace.tokens.management.forms.generic-form
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.files.tokens :as cft] [app.common.files.tokens :as cfo]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
@@ -39,7 +39,6 @@
(str/blank? value)) (str/blank? value))
(tr "workspace.tokens.empty-input"))) (tr "workspace.tokens.empty-input")))
(defn get-value-for-validator (defn get-value-for-validator
[active-tab value value-subfield form-type] [active-tab value value-subfield form-type]
@@ -64,9 +63,9 @@
[:name [:name
[:and [:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) (sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]] #(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]] [:value [::sm/text {:error/fn token-value-error-fn}]]
@@ -77,7 +76,7 @@
:error/fn #(tr "workspace.tokens.self-reference")} :error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}] (fn [{:keys [name value]}]
(when (and name value) (when (and name value)
(nil? (cto/token-value-self-reference? name value))))]])) (not (cto/token-value-self-reference? name value))))]]))
(mf/defc form* (mf/defc form*
[{:keys [token [{:keys [token
@@ -98,7 +97,7 @@
input-component (or input-component token.controls/input*) input-component (or input-component token.controls/input*)
validate-token (or validator default-validate-token) validate-token (or validator default-validate-token)
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite)) active-tab* (mf/use-state #(if (cfo/is-reference? token) :reference :composite))
active-tab (deref active-tab*) active-tab (deref active-tab*)
on-toggle-tab on-toggle-tab

View File

@@ -8,9 +8,9 @@
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.errors :as wte] [app.main.data.workspace.tokens.errors :as wte]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
@@ -50,7 +50,7 @@
;; Entering form without a value - show no error just resolve nil ;; Entering form without a value - show no error just resolve nil
(nil? token-value) (rx/of nil) (nil? token-value) (rx/of nil)
;; Validate refrence string ;; Validate refrence string
(cto/shadow-composite-token-reference? token-value) (default-validate-token params) (cto/composite-token-reference? token-value) (default-validate-token params)
;; Validate composite token ;; Validate composite token
:else :else
(let [params (-> params (let [params (-> params
@@ -261,10 +261,10 @@
[:and [:and
[:string {:min 1 :max 255 [:string {:min 1 :max 255
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc (sm/update-properties cto/schema:token-name assoc
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]] #(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [:value
[:map [:map

View File

@@ -8,9 +8,9 @@
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.errors :as wte] [app.main.data.workspace.tokens.errors :as wte]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.foundations.assets.icon :as i]
@@ -46,7 +46,7 @@
;; Entering form without a value - show no error just resolve nil ;; Entering form without a value - show no error just resolve nil
(nil? token-value) (rx/of nil) (nil? token-value) (rx/of nil)
;; Validate refrence string ;; Validate refrence string
(cto/typography-composite-token-reference? token-value) (default-validate-token props) (cto/composite-token-reference? token-value) (default-validate-token props)
;; Validate composite token ;; Validate composite token
:else :else
(-> props (-> props
@@ -216,10 +216,10 @@
[:and [:and
[:string {:min 1 :max 255 [:string {:min 1 :max 255
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc (sm/update-properties cto/schema:token-name assoc
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]] #(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [:value
[:map [:map

View File

@@ -1,7 +1,6 @@
(ns app.main.ui.workspace.tokens.management.forms.validators (ns app.main.ui.workspace.tokens.management.forms.validators
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
@@ -29,7 +28,7 @@
;; When creating a new token we dont have a name yet or invalid name, ;; When creating a new token we dont have a name yet or invalid name,
;; but we still want to resolve the value to show in the form. ;; but we still want to resolve the value to show in the form.
;; So we use a temporary token name that hopefully doesn't clash with any of the users token names ;; So we use a temporary token name that hopefully doesn't clash with any of the users token names
(not (sm/valid? cto/token-name-ref (:name token))) (assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__")) (not (sm/valid? cto/schema:token-name (:name token))) (assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"))
tokens' (cond-> tokens tokens' (cond-> tokens
;; Remove previous token when renaming a token ;; Remove previous token when renaming a token
(not= (:name token) (:name prev-token)) (not= (:name token) (:name prev-token))
@@ -89,23 +88,3 @@
[token-name token-vals] [token-name token-vals]
(when (some #(cto/token-value-self-reference? token-name %) token-vals) (when (some #(cto/token-value-self-reference? token-name %) token-vals)
(wte/get-error-code :error.token/direct-self-reference))) (wte/get-error-code :error.token/direct-self-reference)))
;; This is used in plugins
(defn- make-token-name-schema
"Generate a dynamic schema validation to check if a token path derived
from the name already exists at `tokens-tree`."
[tokens-tree]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]])
(defn validate-token-name
[tokens-tree name]
(let [schema (make-token-name-schema tokens-tree)
explainer (sm/explainer schema)]
(-> name explainer sm/simplify not-empty)))

View File

@@ -10,7 +10,7 @@
[app.main.style :as stl]) [app.main.style :as stl])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.files.tokens :as cft] [app.common.files.tokens :as cfo]
[app.common.path-names :as cpn] [app.common.path-names :as cpn]
[app.common.types.token :as ctt] [app.common.types.token :as ctt]
[app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.application :as dwta]
@@ -156,9 +156,9 @@
(defn- applied-all-attributes? (defn- applied-all-attributes?
[token selected-shapes attributes] [token selected-shapes attributes]
(let [ids-by-attributes (cft/shapes-ids-by-applied-attributes token selected-shapes attributes) (let [ids-by-attributes (cfo/shapes-ids-by-applied-attributes token selected-shapes attributes)
shape-ids (into #{} xf:map-id selected-shapes)] shape-ids (into #{} xf:map-id selected-shapes)]
(cft/shapes-applied-all? ids-by-attributes shape-ids attributes))) (cfo/shapes-applied-all? ids-by-attributes shape-ids attributes)))
(defn attributes-match-selection? (defn attributes-match-selection?
[selected-shapes attrs & {:keys [selected-inside-layout?]}] [selected-shapes attrs & {:keys [selected-inside-layout?]}]
@@ -178,7 +178,7 @@
(let [{:keys [name value errors type]} token (let [{:keys [name value errors type]} token
has-selected? (pos? (count selected-shapes)) has-selected? (pos? (count selected-shapes))
is-reference? (cft/is-reference? token) is-reference? (cfo/is-reference? token)
contains-path? (str/includes? name ".") contains-path? (str/includes? name ".")
attributes (as-> (get dwta/token-properties type) $ attributes (as-> (get dwta/token-properties type) $
@@ -191,7 +191,7 @@
applied? applied?
(if has-selected? (if has-selected?
(cft/shapes-token-applied? token selected-shapes attributes) (cfo/shapes-token-applied? token selected-shapes attributes)
false) false)
half-applied? half-applied?
@@ -219,7 +219,7 @@
no-valid-value) no-valid-value)
color color
(when (cft/color-token? token) (when (cfo/color-token? token)
(let [theme-token (get active-theme-tokens name)] (let [theme-token (get active-theme-tokens name)]
(or (dwtc/resolved-token-bullet-color theme-token) (or (dwtc/resolved-token-bullet-color theme-token)
(dwtc/resolved-token-bullet-color token)))) (dwtc/resolved-token-bullet-color token))))

View File

@@ -62,7 +62,8 @@
(st/emit! (dwtl/start-token-set-edition id)))))] (st/emit! (dwtl/start-token-set-edition id)))))]
[:> controlled-sets-list* [:> controlled-sets-list*
{:token-sets token-sets {:tokens-lib tokens-lib
:token-sets token-sets
:is-token-set-active token-set-active? :is-token-set-active token-set-active?
:is-token-set-group-active token-set-group-active? :is-token-set-group-active token-set-group-active?
@@ -79,6 +80,6 @@
:on-toggle-token-set on-toggle-token-set-click :on-toggle-token-set on-toggle-token-set-click
:on-toggle-token-set-group on-toggle-token-set-group-click :on-toggle-token-set-group on-toggle-token-set-group-click
:on-update-token-set sets-helpers/on-update-token-set :on-update-token-set (partial sets-helpers/on-update-token-set tokens-lib)
:on-update-token-set-group sets-helpers/on-update-token-set-group :on-update-token-set-group sets-helpers/on-update-token-set-group
:on-create-token-set sets-helpers/on-create-token-set}])) :on-create-token-set sets-helpers/on-create-token-set}]))

View File

@@ -1,9 +1,13 @@
(ns app.main.ui.workspace.tokens.sets.helpers (ns app.main.ui.workspace.tokens.sets.helpers
(:require (:require
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.notifications :as ntf]
[app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st] [app.main.store :as st]
[app.util.i18n :refer [tr]]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -11,9 +15,18 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn on-update-token-set (defn on-update-token-set
[token-set name] [tokens-lib token-set name]
(st/emit! (dwtl/clear-token-set-edition) (let [name (ctob/normalize-set-name name)
(dwtl/update-token-set token-set name))) errors (sm/validation-errors name (cfo/make-token-set-name-schema
tokens-lib
(ctob/get-id token-set)))]
(st/emit! (dwtl/clear-token-set-edition))
(if (empty? errors)
(st/emit! (dwtl/rename-token-set token-set name))
(st/emit! (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000})))))
(defn on-update-token-set-group (defn on-update-token-set-group
[path name] [path name]
@@ -21,15 +34,15 @@
(dwtl/rename-token-set-group path name))) (dwtl/rename-token-set-group path name)))
(defn on-create-token-set (defn on-create-token-set
[parent-set name] [tokens-lib parent-set name]
(let [;; FIXME: this code should be reusable under helper under (let [name (ctob/make-child-name parent-set name)
;; common types namespace errors (sm/validation-errors name (cfo/make-token-set-name-schema tokens-lib nil))]
name
(if-let [parent-path (ctob/get-set-path parent-set)]
(->> (concat parent-path (ctob/split-set-name name))
(ctob/join-set-path))
(ctob/normalize-set-name name))
token-set (ctob/make-token-set :name name)]
(st/emit! (ptk/data-event ::ev/event {::ev/name "create-token-set" :name name}) (st/emit! (ptk/data-event ::ev/event {::ev/name "create-token-set" :name name})
(dwtl/create-token-set token-set)))) (dwtl/clear-token-set-creation))
(if (empty? errors)
(let [token-set (ctob/make-token-set :name name)]
(st/emit! (dwtl/create-token-set token-set)))
(st/emit! (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000})))))

View File

@@ -321,6 +321,7 @@
on-select on-select
on-toggle-set on-toggle-set
on-toggle-set-group on-toggle-set-group
tokens-lib
token-sets token-sets
new-path new-path
edition-id]}] edition-id]}]
@@ -408,7 +409,7 @@
:on-drop on-drop :on-drop on-drop
:on-reset-edition on-reset-edition :on-reset-edition on-reset-edition
:on-edit-submit sets-helpers/on-create-token-set}] :on-edit-submit (partial sets-helpers/on-create-token-set tokens-lib)}]
:else :else
[:> sets-tree-set* [:> sets-tree-set*
@@ -434,7 +435,8 @@
:on-edit-submit on-edit-submit-set}]))))) :on-edit-submit on-edit-submit-set}])))))
(mf/defc controlled-sets-list* (mf/defc controlled-sets-list*
[{:keys [token-sets [{:keys [tokens-lib
token-sets
selected selected
on-update-token-set on-update-token-set
on-update-token-set-group on-update-token-set-group
@@ -486,6 +488,7 @@
{:is-draggable draggable? {:is-draggable draggable?
:new-path new-path :new-path new-path
:edition-id edition-id :edition-id edition-id
:tokens-lib tokens-lib
:token-sets token-sets :token-sets token-sets
:selected selected :selected selected
:on-select on-select :on-select on-select

View File

@@ -7,8 +7,11 @@
(ns app.main.ui.workspace.tokens.themes.create-modal (ns app.main.ui.workspace.tokens.themes.create-modal
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.tokens :as cfo]
[app.common.logic.tokens :as clt] [app.common.logic.tokens :as clt]
[app.common.schema :as sm]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]] [app.main.constants :refer [max-input-length]]
[app.main.data.event :as ev] [app.main.data.event :as ev]
@@ -30,32 +33,9 @@
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.keyboard :as k] [app.util.keyboard :as k]
[cuerdas.core :as str] [cuerdas.core :as str]
[malli.core :as m]
[malli.error :as me]
[potok.v2.core :as ptk] [potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
;; Schemas ---------------------------------------------------------------------
(defn- theme-name-schema
"Generate a dynamic schema validation to check if a theme path derived from the name already exists at `tokens-tree`."
[{:keys [group theme-id tokens-lib]}]
(m/-simple-schema
{:type :token/name-exists
:pred (fn [name]
(if tokens-lib
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme)
(= (ctob/get-id theme) theme-id)))
true)) ;; if still no library exists, cannot be duplicate
:type-properties {:error/fn #(tr "workspace.tokens.theme-name-already-exists")}}))
(defn validate-theme-name
[tokens-lib group theme-id name]
(let [schema (theme-name-schema {:tokens-lib tokens-lib :theme-id theme-id :group group})
validation (m/explain schema (str/trim name))]
(me/humanize validation)))
;; Form Component -------------------------------------------------------------- ;; Form Component --------------------------------------------------------------
(mf/defc empty-themes (mf/defc empty-themes
@@ -199,26 +179,43 @@
theme-groups) theme-groups)
current-group* (mf/use-state (:group theme)) current-group* (mf/use-state (:group theme))
current-group (deref current-group*) current-group (deref current-group*)
current-name* (mf/use-state (:name theme))
current-name (deref current-name*)
group-errors* (mf/use-state nil)
group-errors (deref group-errors*)
name-errors* (mf/use-state nil) name-errors* (mf/use-state nil)
name-errors (deref name-errors*) name-errors (deref name-errors*)
on-update-group on-update-group
(mf/use-fn (mf/use-fn
(mf/deps on-change-field) (mf/deps on-change-field tokens-lib current-name)
(fn [value] (fn [value]
(let [errors (sm/validation-errors value (cfo/make-token-theme-group-schema
tokens-lib
current-name
(ctob/get-id theme)))]
(reset! group-errors* errors)
(if (empty? errors)
(do
(reset! current-group* value) (reset! current-group* value)
(on-change-field :group value))) (on-change-field :group value))
(on-change-field :group "")))))
on-update-name on-update-name
(mf/use-fn (mf/use-fn
(mf/deps on-change-field tokens-lib current-group) (mf/deps on-change-field tokens-lib current-group)
(fn [event] (fn [event]
(let [value (-> event dom/get-target dom/get-value) (let [value (-> event dom/get-target dom/get-value)
errors (validate-theme-name tokens-lib current-group (ctob/get-id theme) value)] errors (sm/validation-errors value (cfo/make-token-theme-name-schema
tokens-lib
current-group
(ctob/get-id theme)))]
(reset! name-errors* errors) (reset! name-errors* errors)
(mf/set-ref-val! theme-name-ref value) (mf/set-ref-val! theme-name-ref value)
(if (empty? errors) (if (empty? errors)
(on-change-field :name value) (do
(reset! current-name* value)
(on-change-field :name value))
(on-change-field :name "")))))] (on-change-field :name "")))))]
[:div {:class (stl/css :edit-theme-inputs-wrapper)} [:div {:class (stl/css :edit-theme-inputs-wrapper)}
@@ -228,6 +225,7 @@
:placeholder (tr "workspace.tokens.label.group-placeholder") :placeholder (tr "workspace.tokens.label.group-placeholder")
:default-selected (:group theme) :default-selected (:group theme)
:options (clj->js options) :options (clj->js options)
:has-error (d/not-empty? group-errors)
:on-change on-update-group}]] :on-change on-update-group}]]
[:div {:class (stl/css :group-input-wrapper)} [:div {:class (stl/css :group-input-wrapper)}
@@ -280,6 +278,7 @@
(mf/defc edit-create-theme* (mf/defc edit-create-theme*
[{:keys [change-view theme on-save is-editing has-prev-view]}] [{:keys [change-view theme on-save is-editing has-prev-view]}]
(let [ordered-token-sets (mf/deref refs/workspace-ordered-token-sets) (let [ordered-token-sets (mf/deref refs/workspace-ordered-token-sets)
tokens-lib (mf/deref refs/tokens-lib)
token-sets (mf/deref refs/workspace-token-sets-tree) token-sets (mf/deref refs/workspace-token-sets-tree)
current-theme* (mf/use-state theme) current-theme* (mf/use-state theme)
@@ -381,7 +380,8 @@
[:div {:class (stl/css :sets-list-wrapper)} [:div {:class (stl/css :sets-list-wrapper)}
[:> wts/controlled-sets-list* [:> wts/controlled-sets-list*
{:token-sets token-sets {:tokens-lib tokens-lib
:token-sets token-sets
:is-token-set-active token-set-active? :is-token-set-active token-set-active?
:is-token-set-group-active token-set-group-active? :is-token-set-group-active token-set-group-active?
:on-select on-click-token-set :on-select on-click-token-set

View File

@@ -7,18 +7,20 @@
(ns app.plugins.tokens (ns app.plugins.tokens
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.workspace.tokens.management.forms.validators :as form-validator]
[app.main.ui.workspace.tokens.themes.create-modal :as theme-form]
[app.plugins.utils :as u] [app.plugins.utils :as u]
[app.util.object :as obj] [app.util.object :as obj]
[clojure.datafy :refer [datafy]])) [clojure.datafy :refer [datafy]]))
;; === Token
(defn- apply-token-to-shapes (defn- apply-token-to-shapes
[file-id set-id id shape-ids attrs] [file-id set-id id shape-ids attrs]
(let [token (u/locate-token file-id set-id id) (let [token (u/locate-token file-id set-id id)
@@ -50,15 +52,13 @@
(ctob/get-name token))) (ctob/get-name token)))
:set :set
(fn [_ value] (fn [_ value]
(let [tokens-lib (u/locate-tokens-lib file-id) (let [name (u/coerce-1 value
errors (form-validator/validate-token-name (cfo/make-token-name-schema
(ctob/get-tokens tokens-lib set-id) (-> (u/locate-tokens-lib file-id)
value)] (ctob/get-tokens set-id)))
(cond :name
(some? errors) "Invalid token name")]
(u/display-not-valid :name (first errors)) (when name
:else
(st/emit! (dwtl/update-token set-id id {:name value})))))} (st/emit! (dwtl/update-token set-id id {:name value})))))}
:type :type
@@ -84,6 +84,11 @@
:duplicate :duplicate
(fn [] (fn []
;; TODO:
;; - add function duplicate-token in tokens-lib, that allows to specify the new id
;; - use this function in dwtl/duplicate-token
;; - return the new token proxy using the locally forced id
;; - do the same with sets and themes
(let [token (u/locate-token file-id set-id id) (let [token (u/locate-token file-id set-id id)
token' (ctob/make-token (-> (datafy token) token' (ctob/make-token (-> (datafy token)
(dissoc :id (dissoc :id
@@ -104,6 +109,9 @@
(let [selected (get-in @st/state [:workspace-local :selected])] (let [selected (get-in @st/state [:workspace-local :selected])]
(apply-token-to-shapes file-id set-id id selected attrs))))) (apply-token-to-shapes file-id set-id id selected attrs)))))
;; === Token Set
(defn token-set-proxy (defn token-set-proxy
[plugin-id file-id id] [plugin-id file-id id]
(obj/reify {:name "TokenSetProxy"} (obj/reify {:name "TokenSetProxy"}
@@ -122,13 +130,15 @@
(ctob/get-name set))) (ctob/get-name set)))
:set :set
(fn [_ value] (fn [_ value]
(let [set (u/locate-token-set file-id id)] (let [set (u/locate-token-set file-id id)
(cond name (u/coerce-1 value
(not (string? value)) (cfo/make-token-set-name-schema
(u/display-not-valid :name value) (u/locate-tokens-lib file-id)
id)
:else :setTokenSet
(st/emit! (dwtl/update-token-set set value)))))} "Invalid token set name")]
(when name
(st/emit! (dwtl/rename-token-set set name)))))}
:active :active
{:this true {:this true
@@ -140,8 +150,13 @@
(ctob/token-set-active? tokens-lib (ctob/get-name set)))) (ctob/token-set-active? tokens-lib (ctob/get-name set))))
:set :set
(fn [_ value] (fn [_ value]
(let [value (u/coerce-1 value
(sm/schema [:boolean])
:setActiveSet
value)]
(when (some? value)
(let [set (u/locate-token-set file-id id)] (let [set (u/locate-token-set file-id id)]
(st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) value))))} (st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) value))))))}
:toggleActive :toggleActive
(fn [_] (fn [_]
@@ -153,8 +168,7 @@
:enumerable false :enumerable false
:get :get
(fn [_] (fn [_]
(let [file (u/locate-file file-id) (let [tokens-lib (u/locate-tokens-lib file-id)]
tokens-lib (->> file :data :tokens-lib)]
(->> (ctob/get-tokens tokens-lib id) (->> (ctob/get-tokens tokens-lib id)
(vals) (vals)
(map #(token-proxy plugin-id file-id id (:id %))) (map #(token-proxy plugin-id file-id id (:id %)))
@@ -165,8 +179,7 @@
:enumerable false :enumerable false
:get :get
(fn [_] (fn [_]
(let [file (u/locate-file file-id) (let [tokens-lib (u/locate-tokens-lib file-id)
tokens-lib (->> file :data :tokens-lib)
tokens (ctob/get-tokens tokens-lib id)] tokens (ctob/get-tokens tokens-lib id)]
(->> tokens (->> tokens
(vals) (vals)
@@ -193,30 +206,20 @@
(token-proxy plugin-id file-id id token-id))))) (token-proxy plugin-id file-id id token-id)))))
:addToken :addToken
(fn [type-str name value] (fn [attrs]
(let [type (cto/dtcg-token-type->token-type type-str)] (let [schema (-> (sm/schema (cfo/make-token-schema
(cond (-> (u/locate-tokens-lib file-id)
(nil? type) (ctob/get-tokens id))))
(u/display-not-valid :addTokenType type-str) (sm/dissoc-key :id)) ;; We don't allow plugins to set the id
attrs (u/coerce attrs schema :addToken "invalid token attrs")]
(not (string? name)) (when attrs
(u/display-not-valid :addTokenName name) (let [token (ctob/make-token attrs)]
:else
(let [token (ctob/make-token {:type type
:name name
:value value})]
(st/emit! (dwtl/create-token id token)) (st/emit! (dwtl/create-token id token))
(token-proxy plugin-id file-id (:id set) (:id token)))))) (token-proxy plugin-id file-id (:id set) (:id token))))))
:duplicate :duplicate
(fn [] (fn []
(let [set (u/locate-token-set file-id id) (st/emit! (dwtl/duplicate-token-set id)))
set' (ctob/make-token-set (-> (datafy set)
(dissoc :id
:modified-at)))]
(st/emit! (dwtl/create-token-set set'))
(token-set-proxy plugin-id file-id (:id set'))))
:remove :remove
(fn [] (fn []
@@ -247,12 +250,15 @@
(:group theme))) (:group theme)))
:set :set
(fn [_ value] (fn [_ value]
(let [theme (u/locate-token-theme file-id id)] (let [theme (u/locate-token-theme file-id id)
(cond group (u/coerce-1 value
(not (string? value)) (cfo/make-token-theme-group-schema
(u/display-not-valid :group value) (u/locate-tokens-lib file-id)
(:name theme)
:else (:id theme))
:group
"Invalid token theme group")]
(when group
(st/emit! (dwtl/update-token-theme id (assoc theme :group value))))))} (st/emit! (dwtl/update-token-theme id (assoc theme :group value))))))}
:name :name
@@ -264,16 +270,14 @@
:set :set
(fn [_ value] (fn [_ value]
(let [theme (u/locate-token-theme file-id id) (let [theme (u/locate-token-theme file-id id)
errors (theme-form/validate-theme-name name (u/coerce-1 value
(cfo/make-token-theme-name-schema
(u/locate-tokens-lib file-id) (u/locate-tokens-lib file-id)
(:group theme) (:id theme)
id (:group theme))
value)] :name
(cond "Invalid token theme name")]
(some? errors) (when name
(u/display-not-valid :name (first errors))
:else
(st/emit! (dwtl/update-token-theme id (assoc theme :name value))))))} (st/emit! (dwtl/update-token-theme id (assoc theme :name value))))))}
:active :active
@@ -328,8 +332,7 @@
:enumerable false :enumerable false
:get :get
(fn [_] (fn [_]
(let [file (u/locate-file file-id) (let [tokens-lib (u/locate-tokens-lib file-id)
tokens-lib (->> file :data :tokens-lib)
themes (->> (ctob/get-themes tokens-lib) themes (->> (ctob/get-themes tokens-lib)
(remove #(= (:id %) uuid/zero)))] (remove #(= (:id %) uuid/zero)))]
(apply array (map #(token-theme-proxy plugin-id file-id (ctob/get-id %)) themes))))} (apply array (map #(token-theme-proxy plugin-id file-id (ctob/get-id %)) themes))))}
@@ -339,36 +342,36 @@
:enumerable false :enumerable false
:get :get
(fn [_] (fn [_]
(let [file (u/locate-file file-id) (let [tokens-lib (u/locate-tokens-lib file-id)
tokens-lib (->> file :data :tokens-lib)
sets (ctob/get-sets tokens-lib)] sets (ctob/get-sets tokens-lib)]
(apply array (map #(token-set-proxy plugin-id file-id (ctob/get-id %)) sets))))} (apply array (map #(token-set-proxy plugin-id file-id (ctob/get-id %)) sets))))}
:addTheme :addTheme
(fn [group name] (fn [attrs]
(cond (let [schema (-> (sm/schema (cfo/make-token-theme-schema
(not (string? group)) (u/locate-tokens-lib file-id)
(u/display-not-valid :addThemeGroup group) (or (obj/get attrs "group") "")
(or (obj/get attrs "name") "")
(not (string? name)) nil))
(u/display-not-valid :addThemeName name) (sm/dissoc-key :id)) ;; We don't allow plugins to set the id
attrs (u/coerce attrs schema :addTheme "invalid theme attrs")]
:else (when attrs
(let [theme (ctob/make-token-theme {:group group (let [theme (ctob/make-token-theme attrs)]
:name name})]
(st/emit! (dwtl/create-token-theme theme)) (st/emit! (dwtl/create-token-theme theme))
(token-theme-proxy plugin-id file-id (:id theme))))) (token-theme-proxy plugin-id file-id (:id theme))))))
:addSet :addSet
(fn [name] (fn [attrs]
(cond (obj/update! attrs "name" ctob/normalize-set-name) ;; TODO: seems a quite weird way of doing this
(not (string? name)) (let [schema (-> (sm/schema (cfo/make-token-set-schema
(u/display-not-valid :addSetName name) (u/locate-tokens-lib file-id)
nil))
:else (sm/dissoc-key :id)) ;; We don't allow plugins to set the id
(let [set (ctob/make-token-set {:name name})] attrs (u/coerce attrs schema :addSet "invalid set attrs")]
(when attrs
(let [set (ctob/make-token-set attrs)]
(st/emit! (dwtl/create-token-set set)) (st/emit! (dwtl/create-token-set set))
(token-set-proxy plugin-id file-id (:id set))))) (token-set-proxy plugin-id file-id (ctob/get-id set))))))
:getThemeById :getThemeById
(fn [theme-id] (fn [theme-id]

View File

@@ -9,12 +9,15 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.json :as json]
[app.common.schema :as sm]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.store :as st] [app.main.store :as st]
[app.util.object :as obj])) [app.util.object :as obj]
[cuerdas.core :as str]))
(defn locate-file (defn locate-file
[id] [id]
@@ -218,7 +221,8 @@
(defn display-not-valid (defn display-not-valid
[code value] [code value]
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code))) (.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code))
nil)
(defn reject-not-valid (defn reject-not-valid
[reject code value] [reject code value]
@@ -226,6 +230,26 @@
(.error js/console msg) (.error js/console msg)
(reject msg))) (reject msg)))
(defn coerce
"Decodes a javascript object into clj and check against schema. If schema validation fails,
displays a not-valid message with the code and hint provided and returns nil."
[attrs schema code hint]
(let [decoder (sm/decoder schema sm/json-transformer)
explainer (sm/explainer schema)
attrs (-> attrs json/->clj decoder)]
(if-let [explain (explainer attrs)]
(display-not-valid code (str hint " " (sm/humanize-explain explain)))
attrs)))
(defn coerce-1
"Checks a single javascript value against schema. If schema validation fails,
displays a not-valid message with the code and hint provided and returns nil."
[value schema code hint]
(let [errors (sm/validation-errors value schema)]
(if (d/not-empty? errors)
(display-not-valid code (str hint " " (str/join ", " errors)))
value)))
(defn mixed-value (defn mixed-value
[values] [values]
(let [s (set values)] (let [s (set values)]

View File

@@ -8,6 +8,7 @@
"A i18n foundation." "A i18n foundation."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.i18n]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.time :as ct] [app.common.time :as ct]
[app.config :as cf] [app.config :as cf]
@@ -212,3 +213,7 @@
cv (get cv :locale)] cv (get cv :locale)]
(when (not= pv cv) (when (not= pv cv)
(ct/set-default-locale! cv))))) (ct/set-default-locale! cv)))))
;; We set the real translation function in the common i18n namespace,
;; so that when common code calls (tr ...) it uses this function.
(set! app.common.i18n/tr tr)

View File

@@ -6,7 +6,7 @@
(ns frontend-tests.tokens.helpers.tokens (ns frontend-tests.tokens.helpers.tokens
(:require (:require
[app.common.files.tokens :as cft] [app.common.files.tokens :as cfo]
[app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.ids-map :as thi]
[app.common.types.tokens-lib :as ctob])) [app.common.types.tokens-lib :as ctob]))
@@ -20,7 +20,7 @@
(let [first-page-id (get-in file [:data :pages 0]) (let [first-page-id (get-in file [:data :pages 0])
shape-id (thi/id shape-label) shape-id (thi/id shape-label)
token (get-token file token-label) token (get-token file token-label)
applied-attributes (cft/attributes-map attributes token)] applied-attributes (cfo/attributes-map attributes token)]
(update-in file [:data (update-in file [:data
:pages-index first-page-id :pages-index first-page-id
:objects shape-id :objects shape-id

View File

@@ -57,8 +57,7 @@
store (ths/setup-store file) store (ths/setup-store file)
tokens-lib (toht/get-tokens-lib file) tokens-lib (toht/get-tokens-lib file)
set-a (ctob/get-set-by-name tokens-lib "Set A") set-a (ctob/get-set-by-name tokens-lib "Set A")
events [(dwtl/update-token-set (ctob/rename set-a "Set A updated") events [(dwtl/rename-token-set set-a "Set A updated")]]
"Set A updated")]]
(tohs/run-store-async (tohs/run-store-async
store done events store done events