Add missing plugin data validations

This commit is contained in:
alonso.torres 2026-09-09 15:45:59 +02:00
parent 9a11dfa7f9
commit 3c3cc6f1a3
35 changed files with 831 additions and 366 deletions

View File

@ -76,15 +76,20 @@
::sm/text]) ;; Leave references or formulas to be checked by the resolver
(def schema:token-value-typography-map
[:map
[:font-family {:optional true} schema:token-value-font-family]
[:font-size {:optional true} schema:token-value-numeric]
[:font-weight {:optional true} schema:token-value-font-weight]
[:line-height {:optional true} schema:token-value-percent]
[:letter-spacing {:optional true} schema:token-value-generic]
[:paragraph-spacing {:optional true} schema:token-value-generic]
[:text-decoration {:optional true} schema:token-value-generic]
[:text-case {:optional true} schema:token-value-generic]])
[:and
[:map
[:font-family {:optional true} schema:token-value-font-family]
[:font-size {:optional true} schema:token-value-numeric]
[:font-weight {:optional true} schema:token-value-font-weight]
[:line-height {:optional true} schema:token-value-percent]
[:letter-spacing {:optional true} schema:token-value-generic]
[:paragraph-spacing {:optional true} schema:token-value-generic]
[:text-decoration {:optional true} schema:token-value-generic]
[:text-case {:optional true} schema:token-value-generic]]
[:fn (fn [value]
(and (seq value)
(or (not (contains? value :line-height))
(contains? value :font-size))))]])
(def schema:token-value-typography
[:or
@ -92,7 +97,7 @@
schema:token-value-composite-ref])
(def schema:token-value-shadow-vector
[:vector
[:vector {:min 1}
[:map
[:offset-x :string]
[:offset-y :string]
@ -276,6 +281,7 @@
[tokens-lib set-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn #(not (str/blank? (ctob/normalize-set-name %)))]
[:fn {:error/fn #(tr "errors.token-set-already-exists")}
(fn [name]
(or (nil? tokens-lib)
@ -316,6 +322,7 @@
[tokens-lib group theme-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn #(not (str/blank? %))]
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (str group "/" (:value %)))}
(fn [name]
(or (nil? tokens-lib)

View File

@ -868,6 +868,14 @@
(register! ::safe-number [::number {:gen/gen (sg/small-double)
:max max-safe-int
:min min-safe-int}])
(register! ::non-negative-safe-number
[:and {:gen/gen (sg/small-double :min 0)}
::safe-number
[:fn #(not (neg? %))]])
(register! ::positive-safe-number
[:and {:gen/gen (sg/small-double :min 0.01)}
::safe-number
[:fn pos?]])
(defn parse-boolean
[v]
@ -1093,6 +1101,12 @@
(def valid-safe-number?
(lazy-validator ::safe-number))
(def valid-non-negative-safe-number?
(lazy-validator ::non-negative-safe-number))
(def valid-positive-safe-number?
(lazy-validator ::positive-safe-number))
(def valid-safe-int?
(lazy-validator ::safe-int))

View File

@ -7,6 +7,7 @@
(ns app.common.types.grid
(:require
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.types.color :as clr]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -18,18 +19,28 @@
[:color clr/schema:hex-color]
[:opacity ::sm/safe-number]])
(def schema:grid-count
[:and {:gen/gen (sg/small-double :min 1)}
::sm/safe-number
[:fn #(<= 1 %)]])
(def schema:square-size
[:and {:gen/gen (sg/small-double :min 0.01)}
::sm/safe-number
[:fn #(<= 0.01 %)]])
(def schema:column-params
[:map {:title "ColumnGridParams"}
[:color schema:grid-color]
[:type {:optional true} [::sm/one-of #{:stretch :left :center :right}]]
[:size {:optional true} [:maybe ::sm/safe-number]]
[:size {:optional true} [:maybe schema:grid-count]]
[:margin {:optional true} [:maybe ::sm/safe-number]]
[:item-length {:optional true} [:maybe ::sm/safe-number]]
[:gutter {:optional true} [:maybe ::sm/safe-number]]])
(def schema:square-params
[:map {:title "SquareGridParams"}
[:size {:optional true} [:maybe ::sm/safe-number]]
[:size {:optional true} [:maybe schema:square-size]]
[:color schema:grid-color]])
(def schema:grid
@ -78,4 +89,3 @@
{:square default-square-params
:column default-layout-params
:row default-layout-params})

View File

@ -14,7 +14,8 @@
[app.common.types.grid :as ctg]
[app.common.types.plugins :as ctpg]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]))
[app.common.uuid :as uuid]
[cuerdas.core :as str]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS
@ -73,6 +74,24 @@
(def check-page
(sm/check-fn schema:page))
(defn normalize-page-name
[name]
(some-> name str/trim))
(defn valid-page-name?
[name]
(let [name (normalize-page-name name)]
(and (string? name) (not (str/blank? name)))))
(defn valid-flow-starting-frame?
[page frame-id flow-id]
(let [frame (get-in page [:objects frame-id])]
(and (= :frame (:type frame))
(not-any? (fn [[id flow]]
(and (not= id flow-id)
(= frame-id (:starting-frame flow))))
(:flows page)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INIT & HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -138,13 +138,13 @@
[:stroke-opacity {:optional true} ::sm/safe-number]
[:stroke-style {:optional true}
[::sm/one-of #{:solid :dotted :dashed :mixed}]]
[:stroke-width {:optional true} ::sm/safe-number]
[:stroke-width {:optional true} ::sm/non-negative-safe-number]
;; wasm-render only, backwards compatible
[:stroke-per-side {:optional true} :boolean]
[:stroke-width-top {:optional true} ::sm/safe-number]
[:stroke-width-right {:optional true} ::sm/safe-number]
[:stroke-width-bottom {:optional true} ::sm/safe-number]
[:stroke-width-left {:optional true} ::sm/safe-number]
[:stroke-width-top {:optional true} ::sm/non-negative-safe-number]
[:stroke-width-right {:optional true} ::sm/non-negative-safe-number]
[:stroke-width-bottom {:optional true} ::sm/non-negative-safe-number]
[:stroke-width-left {:optional true} ::sm/non-negative-safe-number]
[:stroke-dash {:optional true} ::sm/safe-number]
[:stroke-gap {:optional true} ::sm/safe-number]
[:stroke-alignment {:optional true}
@ -211,10 +211,10 @@
[:constraints-v {:optional true}
[::sm/one-of vertical-constraint-types]]
[:fixed-scroll {:optional true} :boolean]
[:r1 {:optional true} ::sm/safe-number]
[:r2 {:optional true} ::sm/safe-number]
[:r3 {:optional true} ::sm/safe-number]
[:r4 {:optional true} ::sm/safe-number]
[:r1 {:optional true} ::sm/non-negative-safe-number]
[:r2 {:optional true} ::sm/non-negative-safe-number]
[:r3 {:optional true} ::sm/non-negative-safe-number]
[:r4 {:optional true} ::sm/non-negative-safe-number]
[:opacity {:optional true} ::sm/safe-number]
[:grids {:optional true}
[:vector {:gen/max 2} ctg/schema:grid]]

View File

@ -12,5 +12,5 @@
[:map {:title "BackgroundBlur"}
[:id ::sm/uuid]
[:type [:enum :background-blur]]
[:value ::sm/safe-number]
[:hidden :boolean]])
[:value ::sm/non-negative-safe-number]
[:hidden :boolean]])

View File

@ -12,5 +12,5 @@
[:map {:title "Blur"}
[:id ::sm/uuid]
[:type [:enum :layer-blur]]
[:value ::sm/safe-number]
[:value ::sm/non-negative-safe-number]
[:hidden :boolean]])

View File

@ -13,5 +13,5 @@
(def schema:export
[:map {:title "ShapeExport"}
[:type [::sm/one-of types]]
[:scale ::sm/safe-number]
[:scale ::sm/positive-safe-number]
[:suffix :string]])

View File

@ -10,7 +10,9 @@
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]))
[app.common.schema.generators :as sg]
[app.common.uri :as uri]
[cuerdas.core :as str]))
;; WARNING: options are not deleted when changing event or action
;; type, so it can be restored if the user changes it back later.
@ -216,15 +218,17 @@
(declare calc-overlay-pos-initial)
(declare allowed-animation?)
(defn valid-event-type-for-shape?
[shape event-type]
(and (contains? event-types event-type)
(or (not= event-type :after-delay)
(cfh/frame-shape? shape))))
(defn set-event-type
[interaction event-type shape]
(assert (check-interaction interaction))
(assert (contains? event-types event-type)
"should be a valid event type")
(assert (or (not= event-type :after-delay)
(cfh/frame-shape? shape))
"the `:after-delay` event type incompatible with not frame shapes")
(assert (valid-event-type-for-shape? shape event-type)
"event type incompatible with shape")
(if (= (:event-type interaction) event-type)
interaction
@ -290,12 +294,19 @@
(defn set-delay
[interaction delay]
(assert (check-interaction interaction))
(assert (sm/check-safe-int delay))
(assert (and (sm/check-safe-int delay) (not (neg? delay))))
(assert (has-delay interaction)
"expected compatible interaction event type")
(assoc interaction :delay delay))
(defn valid-delay?
[interaction]
(or (not (has-delay interaction))
(let [delay (:delay interaction)]
(and (sm/valid-safe-int? delay)
(not (neg? delay))))))
;; FIXME: rename to proper name, very confusing one because it does
;; not checks if interaction has distination, it checks if it can have
;; one.
@ -325,6 +336,31 @@
(assoc :overlay-pos-type :center
:overlay-position (gpt/point 0 0))))
(defn valid-destination?
[objects shape destination]
(or (nil? destination)
(let [target (get objects destination)]
(and (cfh/frame-shape? target)
(not= destination (:id shape))
(not= destination (:frame-id shape))))))
(defn normalize-url
[value]
(when (string? value)
(let [value (str/trim value)
explicit-scheme? (re-find #"(?i)^[a-z][a-z0-9+.-]*:" value)]
(when (or (not explicit-scheme?)
(re-find #"(?i)^https?://" value))
(let [value (if explicit-scheme? value (str "http://" value))]
(try
(let [parsed (uri/uri value)]
(when (and (not (re-find #"\s" value))
(contains? #{"http" "https"} (:scheme parsed))
(seq (:host parsed)))
value))
(catch #?(:clj Exception :cljs :default) _
nil)))))))
(defn has-preserve-scroll
[interaction]
(= (:action-type interaction) :navigate))

View File

@ -25,7 +25,7 @@
;; :layout-justify-content ;; :start :center :end :space-between :space-around :space-evenly
;; :layout-wrap-type ;; :wrap, :nowrap
;; :layout-padding-type ;; :simple, :multiple
;; :layout-padding ;; {:p1 num :p2 num :p3 num :p4 num} number could be negative
;; :layout-padding ;; {:p1 num :p2 num :p3 num :p4 num}
;; layout-grid-rows ;; vector of grid-track
;; layout-grid-columns ;; vector of grid-track
@ -103,7 +103,7 @@
(def ^:private schema:grid-track
[:map {:title "GridTrack"}
[:type [::sm/one-of grid-track-types]]
[:value {:optional true} [:maybe ::sm/safe-number]]])
[:value {:optional true} [:maybe ::sm/non-negative-safe-number]]])
(def schema:layout-attrs
[:map {:title "LayoutAttrs"}
@ -111,17 +111,17 @@
[:layout-flex-dir {:optional true} [::sm/one-of flex-direction-types]]
[:layout-gap {:optional true}
[:map
[:row-gap {:optional true} ::sm/safe-number]
[:column-gap {:optional true} ::sm/safe-number]]]
[:row-gap {:optional true} ::sm/non-negative-safe-number]
[:column-gap {:optional true} ::sm/non-negative-safe-number]]]
[:layout-gap-type {:optional true} [::sm/one-of gap-types]]
[:layout-wrap-type {:optional true} [::sm/one-of wrap-types]]
[:layout-padding-type {:optional true} [::sm/one-of padding-type]]
[:layout-padding {:optional true}
[:map
[:p1 ::sm/safe-number]
[:p2 ::sm/safe-number]
[:p3 ::sm/safe-number]
[:p4 ::sm/safe-number]]]
[:p1 ::sm/non-negative-safe-number]
[:p2 ::sm/non-negative-safe-number]
[:p3 ::sm/non-negative-safe-number]
[:p4 ::sm/non-negative-safe-number]]]
[:layout-justify-content {:optional true} [::sm/one-of justify-content-types]]
[:layout-justify-items {:optional true} [::sm/one-of justify-items-types]]
[:layout-align-content {:optional true} [::sm/one-of align-content-types]]
@ -163,10 +163,10 @@
[:m2 {:optional true} ::sm/safe-number]
[:m3 {:optional true} ::sm/safe-number]
[:m4 {:optional true} ::sm/safe-number]]]
[:layout-item-max-h {:optional true} ::sm/safe-number]
[:layout-item-min-h {:optional true} ::sm/safe-number]
[:layout-item-max-w {:optional true} ::sm/safe-number]
[:layout-item-min-w {:optional true} ::sm/safe-number]
[:layout-item-max-h {:optional true} ::sm/non-negative-safe-number]
[:layout-item-min-h {:optional true} ::sm/non-negative-safe-number]
[:layout-item-max-w {:optional true} ::sm/non-negative-safe-number]
[:layout-item-min-w {:optional true} ::sm/non-negative-safe-number]
[:layout-item-h-sizing {:optional true} [::sm/one-of item-h-sizing-types]]
[:layout-item-v-sizing {:optional true} [::sm/one-of item-v-sizing-types]]
[:layout-item-align-self {:optional true} [::sm/one-of item-align-self-types]]

View File

@ -25,7 +25,7 @@
[:style [::sm/one-of styles]]
[:offset-x ::sm/safe-number]
[:offset-y ::sm/safe-number]
[:blur ::sm/safe-number]
[:blur ::sm/non-negative-safe-number]
[:spread ::sm/safe-number]
[:hidden :boolean]
[:color schema:color]])
@ -35,4 +35,3 @@
(def valid-shadow?
(sm/validator schema:shadow))

View File

@ -56,6 +56,40 @@
(def text-transform-attrs
[:text-transform])
(def font-size-min 3)
(def font-size-max 1000)
(def spacing-min -200)
(def spacing-max 200)
(def text-transform-values
#{"uppercase" "capitalize" "lowercase" "none" "unset"})
(def ^:private numeric-text-re
#"^-?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$")
(defn- valid-numeric-text-in-range?
[value min-value max-value]
(and (string? value)
(re-matches numeric-text-re value)
(let [value (d/parse-double value)]
(and (some? value)
(<= min-value value max-value)))))
(defn valid-font-size?
[value]
(valid-numeric-text-in-range? value font-size-min font-size-max))
(defn valid-line-height?
[value]
(valid-numeric-text-in-range? value spacing-min spacing-max))
(defn valid-letter-spacing?
[value]
(valid-numeric-text-in-range? value spacing-min spacing-max))
(defn valid-text-transform?
[value]
(contains? text-transform-values value))
(def text-fills
[:fills])

View File

@ -122,9 +122,13 @@
(def composite-dtcg-token-type->token-type
"Same as above, in the opposite direction."
(assoc dtcg-token-type->token-type
"lineHeights" :line-height
"lineHeight" :line-height))
(let [mapping (assoc dtcg-token-type->token-type
"lineHeights" :line-height
"lineHeight" :line-height)]
(into mapping
(map (fn [[key value]]
[(keyword (str/kebab key)) value]))
mapping)))
(def token-types
(into #{} (keys token-type->dtcg-token-type)))

View File

@ -16,10 +16,12 @@
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def property-max-length 60)
(def schema:variant-property
[:map
[:name :string]
[:value :string]])
[:name [:string {:max property-max-length}]]
[:value [:string {:max property-max-length}]]])
(def schema:variant-component
"A component that is part of a variant set"
@ -47,9 +49,29 @@
(def property-prefix "Property ")
(def property-regex (re-pattern (str property-prefix "(\\d+)")))
(def property-max-length 60)
(def value-prefix "Value ")
(defn normalize-property-text
[value]
(some-> value str/trim))
(defn valid-property-name?
[value]
(let [value (normalize-property-text value)]
(and (string? value)
(not (str/blank? value))
(<= (count value) property-max-length))))
(defn valid-property-value?
[value]
(let [value (normalize-property-text value)]
(and (string? value)
(<= (count value) property-max-length))))
(defn can-remove-property?
[properties]
(> (count properties) 1))
(defn properties-to-name
"Transform the properties into a name, with the values separated by comma"
[properties]
@ -124,8 +146,8 @@
(mapv #(str/split % "=" 2))
(every? #(and (= 2 (count %))
(not (str/blank? (first %)))
(< (count (first %)) property-max-length)
(< (count (second %)) property-max-length)))))
(<= (count (first %)) property-max-length)
(<= (count (second %)) property-max-length)))))
(defn find-properties-to-remove
"Compares two property maps to find which properties should be removed"

View File

@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.files.migrations :as cfm]
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[clojure.test :as t]))
@ -73,3 +74,135 @@
(let [shape (get-in data' [:pages-index page-id :objects shape-id])]
(t/is (nil? (:stroke-cap-start shape)) "top-level cap removed even with no strokes")
(t/is (nil? (:stroke-cap-end shape)) "top-level cap removed even with no strokes"))))
(t/deftest migration-0027-normalizes-constrained-shape-values
(let [file-id (uuid/next)
page-id (uuid/next)
shape-id (uuid/next)
shape (-> (cts/setup-shape {:id shape-id :type :frame})
(assoc :r1 -1
:r2 -2
:r3 -3
:r4 -4
:layout-gap {:row-gap -5 :column-gap -6}
:layout-padding {:p1 -7 :p2 -8 :p3 -9 :p4 -10}
:layout-grid-rows [{:type :fixed :value -11}]
:layout-grid-columns [{:type :percent :value -12}]
:layout-item-min-w -13
:layout-item-max-w -14
:layout-item-min-h -15
:layout-item-max-h -16
:strokes [{:stroke-color "#000000"
:stroke-width -17
:stroke-width-top -18
:stroke-width-right -19
:stroke-width-bottom -20
:stroke-width-left -21}]
:shadow [{:id nil
:style :drop-shadow
:offset-x 0
:offset-y 0
:blur -22
:spread 0
:hidden false
:color {:color "#000000" :opacity 1}}]
:blur {:id (uuid/next)
:type :layer-blur
:value -23
:hidden false}
:background-blur {:id (uuid/next)
:type :background-blur
:value -24
:hidden false}
:exports [{:type :png :scale 0 :suffix ""}]
:grids [{:type :square
:display true
:params {:size 0
:color {:color "#000000" :opacity 1}}}
{:type :column
:display true
:params {:size -25
:color {:color "#000000" :opacity 1}}}]))
data (-> (ctf/make-file-data file-id page-id)
(assoc-in [:pages-index page-id :objects shape-id] shape))]
(t/is (thrown? #?(:clj Exception :cljs js/Error)
(ctf/check-file-data data))
"new schemas reject legacy negative values")
(let [data' (cfm/migrate-data data "0027-normalize-constrained-values")
shape' (get-in data' [:pages-index page-id :objects shape-id])]
(t/is (= data' (ctf/check-file-data data')) "migrated file data passes the schema")
(t/is (every? zero? (map #(get shape' %) [:r1 :r2 :r3 :r4])) "corner radii clamped")
(t/is (= {:row-gap 0 :column-gap 0} (:layout-gap shape')) "layout gaps clamped")
(t/is (= {:p1 0 :p2 0 :p3 0 :p4 0} (:layout-padding shape')) "layout padding clamped")
(t/is (= [0] (mapv :value (:layout-grid-rows shape'))) "row tracks clamped")
(t/is (= [0] (mapv :value (:layout-grid-columns shape'))) "column tracks clamped")
(t/is (every? zero?
(map #(get shape' %)
[:layout-item-min-w :layout-item-max-w
:layout-item-min-h :layout-item-max-h]))
"layout item bounds clamped")
(t/is (every? zero?
(map (first (:strokes shape'))
[:stroke-width :stroke-width-top :stroke-width-right
:stroke-width-bottom :stroke-width-left]))
"stroke widths clamped")
(t/is (zero? (get-in shape' [:shadow 0 :blur])) "shadow blur clamped")
(t/is (zero? (get-in shape' [:blur :value])) "layer blur clamped")
(t/is (zero? (get-in shape' [:background-blur :value])) "background blur clamped")
(t/is (= 1 (get-in shape' [:exports 0 :scale])) "export scale reset to default")
(t/is (= 0.01 (get-in shape' [:grids 0 :params :size])) "square grid size clamped")
(t/is (= 1 (get-in shape' [:grids 1 :params :size])) "column grid count clamped"))))
(t/deftest migration-0027-normalizes-page-grids-and-variant-properties
(let [file-id (uuid/next)
page-id (uuid/next)
component-id (uuid/next)
long-name (apply str (repeat 61 "n"))
long-value (apply str (repeat 61 "v"))
data (-> (ctf/make-file-data file-id page-id)
(assoc-in [:pages-index page-id :default-grids]
{:square {:size -1
:color {:color "#000000" :opacity 1}}
:row {:size 0.5
:color {:color "#000000" :opacity 1}}
:column {:size -2
:color {:color "#000000" :opacity 1}}})
(assoc-in [:components component-id]
{:id component-id
:name "Variant"
:variant-properties [{:name long-name
:value long-value}]}))
data' (cfm/migrate-data data "0027-normalize-constrained-values")]
(t/is (= 0.01 (get-in data' [:pages-index page-id :default-grids :square :size]))
"default square grid size clamped")
(t/is (= 1 (get-in data' [:pages-index page-id :default-grids :row :size]))
"default row grid count reset")
(t/is (= 1 (get-in data' [:pages-index page-id :default-grids :column :size]))
"default column grid count reset")
(t/is (= 60 (count (get-in data' [:components component-id :variant-properties 0 :name])))
"variant property name truncated")
(t/is (= 60 (count (get-in data' [:components component-id :variant-properties 0 :value])))
"variant property value truncated")
(t/is (= data' (cfm/migrate-data data' "0027-normalize-constrained-values"))
"migration is idempotent")))
(t/deftest migration-0027-runs-through-file-migration
(let [migration-id "0027-normalize-constrained-values"
shape-id (uuid/next)
file (ctf/make-file {:name "Legacy constrained values"})
page-id (first (get-in file [:data :pages]))
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :r1 -1))
file (-> file
(assoc :migrations (disj cfm/available-migrations migration-id))
(assoc-in [:data :pages-index page-id :objects shape-id] shape))
file' (cfm/migrate-file file {})]
(t/is (cfm/need-migration? file) "new migration detected")
(t/is (not (cfm/need-migration? file')) "new migration recorded")
(t/is (contains? (:migrations file') migration-id) "migration id persisted")
(t/is (zero? (get-in file' [:data :pages-index page-id :objects shape-id :r1]))
"migration repaired file data before schema validation")))

View File

@ -21,6 +21,7 @@
[app.util.i18n :as i18n :refer [tr]]
[app.util.storage :as storage]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(def ^:private schema:comment-thread
@ -67,6 +68,13 @@
(def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)")
(defn valid-comment-content?
[content]
(when (string? content)
(let [content (str/trim content)]
(and (not (str/blank? content))
(not= content "\u200b")))))
(defn extract-mentions
"Retrieves the mentions in the content as an array of uuids"
[content]
@ -729,4 +737,3 @@
(rx/map (fn [profiles]
#(update % :profiles merge (d/index-by :id profiles)))))))))

View File

@ -1042,6 +1042,17 @@
second)
0)))))
(defn component-swap-nesting-loop?
[objects shape library-data component-id]
(let [component (ctkl/get-component library-data component-id true)
page (ctf/get-component-page library-data component)
root (ctf/get-component-root library-data component)]
(and page
root
(cfh/components-nesting-loop?
(cfh/get-children-with-self (:objects page) (:id root))
(cfh/get-parents-with-self objects (:parent-id shape))))))
(defn component-swap
"Swaps a component with another one"
[shape file-id id-new-component keep-touched?]

View File

@ -733,6 +733,45 @@
(redirect-to-page page-id)
(combine current-page))))))
(defn valid-components-for-variants?
[state page-id ids]
(let [ids (distinct ids)
objects (dsh/lookup-page-objects state page-id)
data (dsh/lookup-file-data state)]
(and (= page-id (:current-page-id state))
(> (count ids) 1)
(every?
(fn [id]
(let [shape (get objects id)
component (ctkl/get-component data (:component-id shape) false)]
(and (ctc/main-instance? shape)
component
(not (ctc/is-variant? component)))))
ids))))
(defn valid-variant-switch?
[state shape pos val]
(let [libraries (dsh/lookup-libraries state)
component (ctf/get-component libraries
(:component-file shape)
(:component-id shape)
:include-deleted? false)
component-file-data (dm/get-in libraries [(:component-file shape) :data])
component-page (dsh/get-page component-file-data (:main-instance-page component))
component-page-objects (:objects component-page)
variant-components (when component
(cfv/find-variant-components component-file-data
component-page-objects
(:variant-id component)))]
(and (ctc/instance-head? shape)
(ctc/in-component-copy? shape)
(ctc/is-variant? component)
(nat-int? pos)
(< pos (count (:variant-properties component)))
(string? val)
(some #(= val (dm/get-in % [:variant-properties pos :value]))
variant-components))))
(defn combine-selected-as-variants
[options]
(ptk/reify ::combine-selected-as-variants
@ -799,4 +838,3 @@
(with-meta (meta it))))))
(rx/of (dwu/commit-undo-transaction undo-id)
(dws/select-shapes ids)))))))

View File

@ -146,11 +146,7 @@
(defn- blank-content?
[content]
(let [content (str/trim content)]
(or (str/blank? content)
(str/empty? content)
(and (= (count content) 1)
(= (first content) zero-width-space)))))
(not (dcm/valid-comment-content? content)))
;; Component that renders the component content
(mf/defc comment-content*

View File

@ -9,7 +9,6 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.path-names :as cpn]
[app.common.types.component :as ctk]
@ -390,7 +389,7 @@
(mf/use-fn
(mf/deps component-ids)
(fn [pos value]
(let [value (d/nilv (str/trim value) "")]
(let [value (ctv/normalize-property-text (d/nilv value ""))]
(doseq [id component-ids]
(st/emit!
(ev/event {::ev/name "variant-edit-property-value" ::ev/origin "workspace:combo-design-tab"})
@ -401,11 +400,11 @@
(mf/use-fn
(mf/deps variant-id)
(fn [event]
(let [value (str/trim (dom/get-target-val event))
(let [value (ctv/normalize-property-text (dom/get-target-val event))
pos (-> (dom/get-current-target event)
(dom/get-data "position")
int)]
(when (seq value)
(when (ctv/valid-property-name? value)
(st/emit!
(dwv/update-property-name variant-id pos value {:trigger "workspace:design-tab-variant"}))))))
@ -743,17 +742,6 @@
(->> (concat groups components)
(sort-by :name)))
find-parent-components
(mf/use-fn
(mf/deps objects)
(fn [shape]
(->> (cfh/get-parents objects (:id shape))
(map :component-id)
(remove nil?))))
;; Get the ids of the components that are parents of the shapes, to avoid loops
parent-components (mapcat find-parent-components shapes)
libraries-options (map (fn [library] {:value (:id library)
:label (:name library)})
(vals libraries))
@ -843,10 +831,9 @@
(let [data (dm/get-in libraries [current-library-id :data])
container (ctf/get-component-page data item)
root-shape (ctf/get-component-root data item)
components (->> (cfh/get-children-with-self (:objects container) (:id root-shape))
(keep :component-id)
set)
loop? (some #(contains? components %) parent-components)]
loop? (some #(dwl/component-swap-nesting-loop?
objects % data (:id item))
shapes)]
[:> component-swap-item* {:key (dm/str (:id item))
:item item
:loop loop?
@ -1243,11 +1230,11 @@
(mf/use-fn
(mf/deps variant-id)
(fn [event]
(let [value (dom/get-target-val event)
(let [value (ctv/normalize-property-text (dom/get-target-val event))
pos (-> (dom/get-current-target event)
(dom/get-data "position")
int)]
(when (seq value)
(when (ctv/valid-property-name? value)
(st/emit!
(dwv/update-property-name variant-id pos value {:trigger "workspace:design-tab-component"}))))))
@ -1258,7 +1245,7 @@
(let [pos (-> (dom/get-current-target event)
(dom/get-data "position")
int)]
(when (> (count properties) 1)
(when (ctv/can-remove-property? properties)
(st/emit!
(ev/event {::ev/name "variant-remove-property" ::ev/origin "workspace:button-design-tab"})
(dwv/remove-property variant-id pos))))))

View File

@ -245,17 +245,13 @@
(fn [event]
(let [target (dom/get-target event)
value (dom/get-value target)
has-prefix? (or (str/starts-with? value "http://")
(str/starts-with? value "https://"))
value (if has-prefix?
value
(str "http://" value))]
(when-not has-prefix?
(dom/set-value! target value))
(if (dom/valid? target)
normalized (ctsi/normalize-url value)]
(when (and normalized (not= normalized value))
(dom/set-value! target normalized))
(if normalized
(do
(dom/remove-class! target "error")
(update-interaction index #(ctsi/set-url % value)))
(update-interaction index #(ctsi/set-url % normalized)))
(dom/add-class! target "error")))))
change-overlay-pos-type

View File

@ -620,8 +620,8 @@
:options size-options
:type "number"
:placeholder (tr "settings.multiple")
:min 3
:max 1000
:min txt/font-size-min
:max txt/font-size-max
:on-change on-font-size-change
:on-blur on-blur}])]
@ -669,8 +669,8 @@
:alt (tr "workspace.options.text-options.line-height")}
deprecated-icon/text-lineheight]
[:> deprecated-input/numeric-input*
{:min -200
:max 200
{:min txt/spacing-min
:max txt/spacing-max
:step 0.1
:default-value "1.2"
:class (stl/css :line-height-input)
@ -688,8 +688,8 @@
:alt (tr "workspace.options.text-options.letter-spacing")}
deprecated-icon/text-letterspacing]
[:> deprecated-input/numeric-input*
{:min -200
:max 200
{:min txt/spacing-min
:max txt/spacing-max
:step 0.1
:default-value "0"
:class (stl/css :letter-spacing-input)

View File

@ -138,8 +138,8 @@
(mf/use-fn
(mf/deps id is-separator?)
(fn [event]
(let [new-name (str/trim (dom/get-target-val event))]
(if (str/empty? new-name)
(let [new-name (ctp/normalize-page-name (dom/get-target-val event))]
(if (not (ctp/valid-page-name? new-name))
(when is-separator?
(st/emit! (dw/delete-page id)))
(st/emit! (dw/rename-page id new-name))))

View File

@ -14,7 +14,6 @@
[app.common.geom.point :as gpt]
[app.common.schema :as sm]
[app.common.types.color :as ctc]
[app.common.types.component :as ctk]
[app.common.types.shape :as cts]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
@ -705,13 +704,15 @@
:createVariantFromComponents
(fn [shapes]
(cond
(or (not (seq shapes))
(or (not (array? shapes))
(not (seq shapes))
(not (every? u/is-main-component-proxy? shapes)))
(u/not-valid plugin-id :shapes shapes)
:else
(let [file-id (obj/get (first shapes) "$file")
page-id (obj/get (first shapes) "$page")
(let [state @st/state
file-id (:current-file-id state)
page-id (:current-page-id state)
;; Keep the input order: it determines the order of the
;; resulting variant components (see combine-as-variants)
ids (->> shapes
@ -719,23 +720,17 @@
(distinct)
(vec))
;; Check that every component is:
;; - in the same page
;; - not already a variant
valid?
(every?
(fn [id]
(let [shape (u/locate-shape file-id page-id id)
component (u/locate-library-component file-id (:component-id shape))]
(not (ctk/is-variant? component))))
ids)]
valid? (and (every? #(and (= file-id (obj/get % "$file"))
(= page-id (obj/get % "$page")))
shapes)
(dwv/valid-components-for-variants? state page-id ids))]
(if valid?
(let [variant-id (uuid/next)]
(st/emit! (-> (dwv/combine-as-variants
ids
{:trigger "plugin:combine-as-variants" :variant-id variant-id})
(se/add-event plugin-id)))
(shape/shape-proxy plugin-id variant-id))
(shape/shape-proxy plugin-id file-id page-id variant-id))
(u/not-valid plugin-id :shapes "One of the components is not on the same page or is already a variant")))))

View File

@ -61,7 +61,7 @@
(fn [content]
(let [profile (:profile @st/state)]
(cond
(or (not (string? content)) (empty? content))
(not (dc/valid-comment-content? content))
(u/not-valid plugin-id :content "Not valid")
(not= (:id profile) (:owner-id data))
@ -188,7 +188,7 @@
(not (r/check-permission plugin-id "comment:write"))
(u/not-valid plugin-id :reply "Plugin doesn't have 'comment:write' permission")
(or (not (string? content)) (empty? content))
(not (dc/valid-comment-content? content))
(u/not-valid plugin-id :reply "Not valid")
:else

View File

@ -165,7 +165,7 @@
:set
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :rowGap value)
(not (r/check-permission plugin-id "content:write"))
@ -183,7 +183,7 @@
:set
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :columnGap value)
(not (r/check-permission plugin-id "content:write"))
@ -201,7 +201,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :verticalPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -219,7 +219,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -237,7 +237,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :topPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -255,7 +255,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :rightPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -273,7 +273,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :bottomPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -291,7 +291,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :leftPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -641,7 +641,7 @@
:set
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :maxWidth value)
(not (r/check-permission plugin-id "content:write"))
@ -659,7 +659,7 @@
:set
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :minWidth value)
(not (r/check-permission plugin-id "content:write"))
@ -677,7 +677,7 @@
:set
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :maxHeight value)
(not (r/check-permission plugin-id "content:write"))
@ -695,7 +695,7 @@
:set
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :minHeight value)
(not (r/check-permission plugin-id "content:write"))

View File

@ -21,8 +21,9 @@
(defn font-variant-proxy? [p]
(obj/type-of? p "FontVariantProxy"))
(defn font-variant-proxy [name id weight style]
(defn font-variant-proxy [font-id name id weight style]
(obj/reify {:name "FontVariantProxy"}
:$font-id {:enumerable false :get (constantly font-id)}
:name {:get (fn [] name)}
:fontVariantId {:get (fn [] id)}
:fontWeight {:get (fn [] weight)}
@ -47,8 +48,8 @@
{:get
(fn []
(format/format-array
(fn [{:keys [id name style weight]}]
(font-variant-proxy name id weight style))
(fn [{variant-id :id :keys [name style weight]}]
(font-variant-proxy id name variant-id weight style))
variants))}
:applyToText
@ -63,6 +64,11 @@
(not (u/page-active? (obj/get text "$page")))
(u/not-valid plugin-id :applyToText "Cannot modify a page that is not currently active")
(and (some? variant)
(or (not (font-variant-proxy? variant))
(not= id (obj/get variant "$font-id"))))
(u/not-valid plugin-id :applyToText variant)
:else
(let [text-id (obj/get text "$id")
values {:font-id id
@ -84,6 +90,11 @@
(not (u/page-active? (obj/get range "$page")))
(u/not-valid plugin-id :applyToRange "Cannot modify a page that is not currently active")
(and (some? variant)
(or (not (font-variant-proxy? variant))
(not= id (obj/get variant "$font-id"))))
(u/not-valid plugin-id :applyToRange variant)
:else
(let [range-id (obj/get range "$id")
start (obj/get range "$start")

View File

@ -190,7 +190,7 @@
:set
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :rowGap value)
(not (r/check-permission plugin-id "content:write"))
@ -208,7 +208,7 @@
:set
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :columnGap value)
(not (r/check-permission plugin-id "content:write"))
@ -226,7 +226,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :verticalPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -244,7 +244,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -262,7 +262,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :topPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -280,7 +280,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :rightPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -298,7 +298,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :bottomPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -316,7 +316,7 @@
:set
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :leftPadding value)
(not (r/check-permission plugin-id "content:write"))
@ -356,7 +356,7 @@
(u/not-valid plugin-id :addRow-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-safe-number? value)))
(not (sm/valid-non-negative-safe-number? value)))
(u/not-valid plugin-id :addRow-value value)
(not (r/check-permission plugin-id "content:write"))
@ -383,7 +383,7 @@
(u/not-valid plugin-id :addRowAtIndex-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-safe-number? value)))
(not (sm/valid-non-negative-safe-number? value)))
(u/not-valid plugin-id :addRowAtIndex-value value)
(not (r/check-permission plugin-id "content:write"))
@ -402,8 +402,8 @@
(not (contains? ctl/grid-track-types type))
(u/not-valid plugin-id :addColumn-type type)
(and (or (= :percent type) (= :flex type) (= :lex type))
(not (sm/valid-safe-number? value)))
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-non-negative-safe-number? value)))
(u/not-valid plugin-id :addColumn-value value)
(not (r/check-permission plugin-id "content:write"))
@ -430,7 +430,7 @@
(u/not-valid plugin-id :addColumnAtIndex-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-safe-number? value)))
(not (sm/valid-non-negative-safe-number? value)))
(u/not-valid plugin-id :addColumnAtIndex-value value)
(not (r/check-permission plugin-id "content:write"))
@ -495,7 +495,7 @@
(u/not-valid plugin-id :setColumn-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-safe-number? value)))
(not (sm/valid-non-negative-safe-number? value)))
(u/not-valid plugin-id :setColumn-value value)
(not (r/check-permission plugin-id "content:write"))
@ -522,7 +522,7 @@
(u/not-valid plugin-id :setRow-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-safe-number? value)))
(not (sm/valid-non-negative-safe-number? value)))
(u/not-valid plugin-id :setRow-value value)
(not (r/check-permission plugin-id "content:write"))

View File

@ -14,12 +14,15 @@
[app.common.types.color :as clr]
[app.common.types.component :as ctk]
[app.common.types.file :as ctf]
[app.common.types.text :as txt]
[app.common.types.typography :as ctt]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
[app.main.data.plugins :as dp]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.variants :as dwv]
[app.main.fonts :as fonts]
[app.main.repo :as rp]
[app.main.store :as st]
[app.plugins.format :as format]
@ -32,6 +35,7 @@
[app.plugins.utils :as u]
[app.util.object :as obj]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(declare lib-color-proxy)
@ -289,6 +293,20 @@
(defn lib-typography-proxy? [p]
(obj/type-of? p "LibraryTypographyProxy"))
(defn- font-data
[font variant]
{:font-id (:id font)
:font-family (:family font)
:font-variant-id (:id variant)
:font-style (:style variant)
:font-weight (:weight variant)})
(defn- variant-data
[variant]
{:font-variant-id (:id variant)
:font-style (:style variant)
:font-weight (:weight variant)})
(defn lib-typography-proxy
[plugin-id file-id id]
(assert (uuid? file-id))
@ -341,136 +359,150 @@
:get #(-> % u/proxy->library-typography :font-id)
:set
(fn [self value]
(cond
(not (string? value))
(u/not-valid plugin-id :fontId value)
(let [font (when (string? value) (fonts/get-font-data value))
variant (fonts/get-default-variant font)]
(cond
(nil? font)
(u/not-valid plugin-id :fontId value)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontId "Plugin doesn't have 'library:write' permission")
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontId "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :font-id value))]
(st/emit! (dwl/update-typography typo file-id)))))}
:else
(let [typo (-> (u/proxy->library-typography self)
(merge (font-data font variant)))]
(st/emit! (dwl/update-typography typo file-id))))))}
:fontFamily
{:this true
:get #(-> % u/proxy->library-typography :font-family)
:set
(fn [self value]
(cond
(not (string? value))
(u/not-valid plugin-id :fontFamily value)
(let [font (when (string? value) (fonts/find-font-data {:family value}))
variant (fonts/get-default-variant font)]
(cond
(nil? font)
(u/not-valid plugin-id :fontFamily value)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontFamily "Plugin doesn't have 'library:write' permission")
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontFamily "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :font-family value))]
(st/emit! (dwl/update-typography typo file-id)))))}
:else
(let [typo (-> (u/proxy->library-typography self)
(merge (font-data font variant)))]
(st/emit! (dwl/update-typography typo file-id))))))}
:fontVariantId
{:this true
:get #(-> % u/proxy->library-typography :font-variant-id)
:set
(fn [self value]
(cond
(not (string? value))
(u/not-valid plugin-id :fontVariantId value)
(let [typo (u/proxy->library-typography self)
font (fonts/get-font-data (:font-id typo))
variant (when (string? value) (fonts/find-variant font {:id value}))]
(cond
(nil? variant)
(u/not-valid plugin-id :fontVariantId value)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'library:write' permission")
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :font-variant-id value))]
(st/emit! (dwl/update-typography typo file-id)))))}
:else
(st/emit! (dwl/update-typography (merge typo (variant-data variant)) file-id)))))}
:fontSize
{:this true
:get #(-> % u/proxy->library-typography :font-size)
:set
(fn [self value]
(cond
(not (string? value))
(u/not-valid plugin-id :fontSize value)
(let [value (some-> value str/trim)]
(cond
(not (txt/valid-font-size? value))
(u/not-valid plugin-id :fontSize value)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontSize "Plugin doesn't have 'library:write' permission")
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontSize "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :font-size value))]
(st/emit! (dwl/update-typography typo file-id)))))}
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :font-size value))]
(st/emit! (dwl/update-typography typo file-id))))))}
:fontWeight
{:this true
:get #(-> % u/proxy->library-typography :font-weight)
:set
(fn [self value]
(cond
(not (string? value))
(u/not-valid plugin-id :fontWeight value)
(let [typo (u/proxy->library-typography self)
font (fonts/get-font-data (:font-id typo))
variant (when (string? value)
(or (fonts/find-variant font {:style (:font-style typo) :weight value})
(fonts/find-variant font {:weight value})))]
(cond
(nil? variant)
(u/not-valid plugin-id :fontWeight value)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontWeight "Plugin doesn't have 'library:write' permission")
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontWeight "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :font-weight value))]
(st/emit! (dwl/update-typography typo file-id)))))}
:else
(st/emit! (dwl/update-typography (merge typo (variant-data variant)) file-id)))))}
:fontStyle
{:this true
:get #(-> % u/proxy->library-typography :font-style)
:set
(fn [self value]
(cond
(not (string? value))
(u/not-valid plugin-id :fontStyle value)
(let [typo (u/proxy->library-typography self)
font (fonts/get-font-data (:font-id typo))
variant (when (string? value)
(or (fonts/find-variant font {:weight (:font-weight typo) :style value})
(fonts/find-variant font {:style value})))]
(cond
(nil? variant)
(u/not-valid plugin-id :fontStyle value)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontStyle "Plugin doesn't have 'library:write' permission")
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :fontStyle "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :font-style value))]
(st/emit! (dwl/update-typography typo file-id)))))}
:else
(st/emit! (dwl/update-typography (merge typo (variant-data variant)) file-id)))))}
:lineHeight
{:this true
:get #(-> % u/proxy->library-typography :font-height)
:get #(-> % u/proxy->library-typography :line-height)
:set
(fn [self value]
(cond
(not (string? value))
(u/not-valid plugin-id :lineHeight value)
(let [value (some-> value str/trim)]
(cond
(not (txt/valid-line-height? value))
(u/not-valid plugin-id :lineHeight value)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :lineHeight "Plugin doesn't have 'library:write' permission")
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :lineHeight "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :font-height value))]
(st/emit! (dwl/update-typography typo file-id)))))}
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :line-height value))]
(st/emit! (dwl/update-typography typo file-id))))))}
:letterSpacing
{:this true
:get #(-> % u/proxy->library-typography :letter-spacing)
:set
(fn [self value]
(cond
(not (string? value))
(u/not-valid plugin-id :letterSpacing value)
(let [value (some-> value str/trim)]
(cond
(not (txt/valid-letter-spacing? value))
(u/not-valid plugin-id :letterSpacing value)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'library:write' permission")
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :letter-spacing value))]
(st/emit! (dwl/update-typography typo file-id)))))}
:else
(let [typo (-> (u/proxy->library-typography self)
(assoc :letter-spacing value))]
(st/emit! (dwl/update-typography typo file-id))))))}
:textTransform
{:this true
@ -478,7 +510,7 @@
:set
(fn [self value]
(cond
(not (string? value))
(not (txt/valid-text-transform? value))
(u/not-valid plugin-id :textTransform value)
(not (r/check-permission plugin-id "library:write"))
@ -495,6 +527,11 @@
(not (obj/type-of? font "FontProxy"))
(u/not-valid plugin-id :setFont font)
(and (some? variant)
(or (not (obj/type-of? variant "FontVariantProxy"))
(not= (obj/get font "fontId") (obj/get variant "$font-id"))))
(u/not-valid plugin-id :setFont variant)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :setFont "Plugin doesn't have 'library:write' permission")
@ -720,7 +757,8 @@
:removeProperty
(fn [pos]
(let [nprops (->> (get-variant-components file-id id) first :variant-properties count)]
(let [properties (->> (get-variant-components file-id id) first :variant-properties)
nprops (count properties)]
(cond
(or (not (nat-int? pos)) (>= pos nprops))
(u/not-valid plugin-id :pos pos)
@ -728,6 +766,9 @@
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :removeProperty "Plugin doesn't have 'library:write' permission")
(not (ctv/can-remove-property? properties))
(u/not-valid plugin-id :removeProperty "A variant must keep at least one property")
:else
(st/emit!
(se/event plugin-id "remove-property")
@ -740,7 +781,7 @@
(or (not (nat-int? pos)) (>= pos nprops))
(u/not-valid plugin-id :pos pos)
(not (string? name))
(not (ctv/valid-property-name? name))
(u/not-valid plugin-id :name name)
(not (r/check-permission plugin-id "library:write"))
@ -748,7 +789,8 @@
:else
(st/emit!
(dwv/update-property-name id pos name {:trigger "plugin:rename-property"})))))))
(dwv/update-property-name id pos (ctv/normalize-property-text name)
{:trigger "plugin:rename-property"})))))))
(set! shape/variant-proxy variant-proxy)
@ -978,8 +1020,8 @@
(or (not (nat-int? pos)) (>= pos nprops))
(u/not-valid plugin-id :pos (str pos))
(not (string? value))
(u/not-valid plugin-id :name value)
(not (ctv/valid-property-value? value))
(u/not-valid plugin-id :value value)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :setVariantProperty "Plugin doesn't have 'library:write' permission")
@ -987,7 +1029,7 @@
:else
(st/emit!
(se/event plugin-id "variant-edit-property-value")
(dwv/update-property-value id pos value)))))))
(dwv/update-property-value id pos (ctv/normalize-property-text value))))))))
(defn library-proxy? [p]
(obj/type-of? p "LibraryProxy"))

View File

@ -12,6 +12,7 @@
[app.common.geom.point :as gpt]
[app.common.schema :as sm]
[app.common.types.color :as cc]
[app.common.types.page :as ctp]
[app.common.uuid :as uuid]
[app.main.data.comments :as dc]
[app.main.data.common :as dcm]
@ -78,15 +79,19 @@
(shape/shape-proxy plugin-id file-id page-id frame)))
:set
(fn [_ value]
(cond
(not (shape/shape-proxy? value))
(u/not-valid plugin-id :startingBoard value)
(let [page (u/locate-page file-id page-id)]
(cond
(or (not (shape/shape-proxy? value))
(not= file-id (obj/get value "$file"))
(not= page-id (obj/get value "$page"))
(not (ctp/valid-flow-starting-frame? page (obj/get value "$id") id)))
(u/not-valid plugin-id :startingBoard value)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :startingBoard "Plugin doesn't have 'content:write' permission")
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :startingBoard "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwi/update-flow page-id id #(assoc % :starting-frame (obj/get value "$id"))))))}
:else
(st/emit! (dwi/update-flow page-id id #(assoc % :starting-frame (obj/get value "$id")))))))}
:remove
(fn []
@ -115,15 +120,16 @@
:get #(-> % u/proxy->page :name)
:set
(fn [_ value]
(cond
(not (string? value))
(u/not-valid plugin-id :name value)
(let [value (ctp/normalize-page-name value)]
(cond
(not (ctp/valid-page-name? value))
(u/not-valid plugin-id :name value)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dw/rename-page id value))))}
:else
(st/emit! (dw/rename-page id value)))))}
:getRoot
(fn []
@ -319,22 +325,26 @@
:createFlow
(fn [name frame]
(cond
(or (not (string? name)) (empty? name))
(u/not-valid plugin-id :createFlow-name name)
(let [page (u/locate-page file-id id)]
(cond
(or (not (string? name)) (empty? name))
(u/not-valid plugin-id :createFlow-name name)
(not (shape/shape-proxy? frame))
(u/not-valid plugin-id :createFlow-frame frame)
(or (not (shape/shape-proxy? frame))
(not= file-id (obj/get frame "$file"))
(not= id (obj/get frame "$page"))
(not (ctp/valid-flow-starting-frame? page (obj/get frame "$id") nil)))
(u/not-valid plugin-id :createFlow-frame frame)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :createFlow "Plugin doesn't have 'content:write' permission")
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :createFlow "Plugin doesn't have 'content:write' permission")
:else
(let [flow-id (uuid/next)]
(st/emit!
(dwi/add-flow flow-id id name (obj/get frame "$id"))
(se/event plugin-id "add-flow"))
(flow-proxy plugin-id file-id id flow-id))))
:else
(let [flow-id (uuid/next)]
(st/emit!
(dwi/add-flow flow-id id name (obj/get frame "$id"))
(se/event plugin-id "add-flow"))
(flow-proxy plugin-id file-id id flow-id)))))
:removeFlow
(fn [flow]
@ -352,7 +362,8 @@
:addRulerGuide
(fn [orientation value board]
(let [shape (u/proxy->shape board)]
(let [shape (when (shape/shape-proxy? board)
(u/locate-shape file-id id (obj/get board "$id")))]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :addRulerGuide "Value not a safe number")
@ -360,8 +371,10 @@
(not (contains? #{"vertical" "horizontal"} orientation))
(u/not-valid plugin-id :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
(and (some? shape)
(and (some? board)
(or (not (shape/shape-proxy? board))
(not= file-id (obj/get board "$file"))
(not= id (obj/get board "$page"))
(not (cfh/frame-shape? shape))))
(u/not-valid plugin-id :addRulerGuide "The shape is not a board")
@ -405,7 +418,7 @@
(let [shape (when board (u/proxy->shape board))
position (parser/parse-point position)]
(cond
(or (not (string? content)) (empty? content))
(not (dc/valid-comment-content? content))
(u/not-valid plugin-id :addCommentThread "Content not valid")
(or (not (sm/valid-safe-number? (:x position)))

View File

@ -10,6 +10,7 @@
[app.common.geom.point :as gpt]
[app.common.json :as json]
[app.common.types.path :as path]
[app.common.types.shape.interactions :as ctsi]
[app.common.uuid :as uuid]
[app.util.object :as obj]
[cuerdas.core :as str]))
@ -506,7 +507,7 @@
:open-url
{:action-type action-type
:url (obj/get action "url")}
:url (ctsi/normalize-url (obj/get action "url"))}
nil)))))

View File

@ -64,7 +64,7 @@
[app.plugins.strokes :as strokes]
[app.plugins.system-events :as se]
[app.plugins.text :as text]
[app.plugins.tokens :refer [applied-tokens-plugin->applied-tokens token-attr-plugin->token-attr token-attr?]]
[app.plugins.tokens :refer [applied-tokens-plugin->applied-tokens token-attr-plugin->token-attr token-attr? valid-token-resolution?]]
[app.plugins.utils :as u]
[app.util.http :as http]
[app.util.object :as obj]
@ -79,6 +79,21 @@
(defn interaction-proxy? [p]
(obj/type-of? p "InteractionProxy"))
(defn- valid-interaction-action?
[file-id page-id source raw-action interaction]
(let [page (u/locate-page file-id page-id)
destination-proxy (obj/get raw-action "destination")
destination-id (:destination interaction)
animation-type (get-in interaction [:animation :animation-type])]
(and (sm/validate ctsi/schema:interaction interaction)
(ctsi/valid-delay? interaction)
(or (nil? destination-proxy)
(and (shape-proxy? destination-proxy)
(= file-id (obj/get destination-proxy "$file"))
(= page-id (obj/get destination-proxy "$page"))))
(ctsi/valid-destination? (:objects page) source destination-id)
(ctsi/allowed-animation? (:action-type interaction) animation-type))))
(defn interaction-proxy
"Proxy over one interaction of a shape.
@ -105,9 +120,10 @@
:get #(-> % u/proxy->interaction :event-type format/format-key)
:set
(fn [_ value]
(let [value (parser/parse-keyword value)]
(let [value (parser/parse-keyword value)
shape (u/locate-shape file-id page-id shape-id)]
(cond
(not (contains? ctsi/event-types value))
(not (ctsi/valid-event-type-for-shape? shape value))
(u/not-valid plugin-id :trigger value)
(not (r/check-permission plugin-id "content:write"))
@ -116,11 +132,11 @@
:else
(do
(st/emit! (dwi/update-interaction
(u/locate-shape file-id page-id shape-id)
shape
(locate-index)
#(assoc % :event-type value)
#(ctsi/set-event-type % value shape)
{:page-id page-id}))
(swap! current assoc :event-type value)))))}
(swap! current ctsi/set-event-type value shape)))))}
:delay
{:this true
@ -148,12 +164,12 @@
:get #(-> % u/proxy->interaction (format/format-action plugin-id file-id page-id))
:set
(fn [self value]
(let [params (parser/parse-action value)
interaction
(-> (u/proxy->interaction self)
(d/patch-object params))]
(let [shape (u/locate-shape file-id page-id shape-id)
params (parser/parse-action value)
interaction (-> (u/proxy->interaction self)
(d/patch-object params))]
(cond
(not (sm/validate ctsi/schema:interaction interaction))
(not (valid-interaction-action? file-id page-id shape value interaction))
(u/not-valid plugin-id :action interaction)
(not (r/check-permission plugin-id "content:write"))
@ -162,7 +178,7 @@
:else
(do
(st/emit! (dwi/update-interaction
(u/locate-shape file-id page-id shape-id)
shape
(locate-index)
#(d/patch-object % params)
{:page-id page-id}))
@ -520,7 +536,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :borderRadiusTopLeft value)
(not (r/check-permission plugin-id "content:write"))
@ -539,7 +555,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :borderRadiusTopRight value)
(not (r/check-permission plugin-id "content:write"))
@ -558,7 +574,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :borderRadiusBottomRight value)
(not (r/check-permission plugin-id "content:write"))
@ -577,7 +593,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :borderRadiusBottomLeft value)
(not (r/check-permission plugin-id "content:write"))
@ -1514,7 +1530,12 @@
:swapComponent
(fn [component]
(let [shape (u/locate-shape file-id page-id id)]
(let [shape (u/locate-shape file-id page-id id)
objects (u/locate-objects file-id page-id)
valid-component? (obj/type-of? component "LibraryComponentProxy")
target-file (when valid-component? (obj/get component "$file"))
target-id (when valid-component? (obj/get component "$id"))
target-data (some-> (u/locate-file target-file) :data)]
(cond
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :swapComponent "Plugin doesn't have 'content:write' permission")
@ -1522,16 +1543,19 @@
(not (u/page-active? page-id))
(u/not-valid plugin-id :swapComponent "Cannot modify a page that is not currently active")
(not (obj/type-of? component "LibraryComponentProxy"))
(not valid-component?)
(u/not-valid plugin-id :swapComponent "Component not valid")
(not (ctk/in-component-copy? shape))
(u/not-valid plugin-id :swapComponent "The shape is not a component copy instance")
(dwl/component-swap-nesting-loop? objects shape target-data target-id)
(u/not-valid plugin-id :swapComponent "The swap would create a component nesting loop")
:else
(st/emit! (dwl/component-swap shape
(obj/get component "$file")
(obj/get component "$id")
target-file
target-id
true)))))
:resetOverrides
@ -1625,11 +1649,15 @@
;; Interactions
:addInteraction
(fn [trigger action delay]
(let [interaction
(-> ctsi/default-interaction
(d/patch-object (parser/parse-interaction trigger action delay)))]
(let [shape (u/locate-shape file-id page-id id)
event-type (parser/parse-keyword trigger)
interaction (when (ctsi/valid-event-type-for-shape? shape event-type)
(-> ctsi/default-interaction
(ctsi/set-event-type event-type shape)
(d/patch-object (parser/parse-action action))
(cond-> (some? delay) (assoc :delay delay))))]
(cond
(not (sm/validate ctsi/schema:interaction interaction))
(not (valid-interaction-action? file-id page-id shape action interaction))
(u/not-valid plugin-id :addInteraction interaction)
(not (r/check-permission plugin-id "content:write"))
@ -1732,15 +1760,23 @@
[:fn token-proxy?]
[:maybe [::sm/set [:and ::sm/keyword [:fn token-attr?]]]]]
:fn (fn [token attrs]
(let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id"))
(let [set-id (obj/get token "$set-id")
token-id (obj/get token "$id")
token (u/locate-token file-id set-id token-id)
kw-attrs (into #{} (map token-attr-plugin->token-attr attrs))]
(cond
(some #(not (token-attr? %)) kw-attrs)
(u/not-valid plugin-id :applyToken attrs)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :applyToken "Plugin doesn't have 'content:write' permission")
(nil? token)
(u/not-valid plugin-id :applyToken token-id)
(not (valid-token-resolution? file-id set-id token-id))
(u/not-valid plugin-id :applyToken (:value token))
(some #(not (token-attr? %)) kw-attrs)
(u/not-valid plugin-id :applyToken attrs)
:else
(st/emit!
(-> (dwta/toggle-token {:token token
@ -1763,6 +1799,12 @@
:switchVariant
(fn [pos value]
(cond
(not (u/page-active? page-id))
(u/not-valid plugin-id :switchVariant "Cannot modify a page that is not currently active")
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :switchVariant "Plugin doesn't have 'content:write' permission")
(not (nat-int? pos))
(u/not-valid plugin-id :pos pos)
@ -1773,11 +1815,11 @@
(u/not-valid plugin-id :switchVariant "Plugin doesn't have 'content:write' permission")
:else
(let [shape (u/locate-shape file-id page-id id)
component (u/locate-library-component file-id (:component-id shape))]
(when (and component (ctk/is-variant? component))
(let [shape (u/locate-shape file-id page-id id)]
(if (dwv/valid-variant-switch? @st/state shape pos value)
(st/emit! (-> (dwv/variants-switch {:shapes [shape] :pos pos :val value})
(se/add-event plugin-id)))))))
(se/add-event plugin-id)))
(u/not-valid plugin-id :switchVariant "Shape, property, or value is not valid")))))
:combineAsVariants
(fn [ids]
@ -1799,13 +1841,7 @@
(distinct))
ids)
valid?
(every?
(fn [id]
(let [shape (u/locate-shape file-id page-id id)
component (u/locate-library-component file-id (:component-id shape))]
(not (ctk/is-variant? component))))
ids)]
valid? (dwv/valid-components-for-variants? @st/state page-id ids)]
(if valid?
(let [variant-id (uuid/next)]
@ -1813,7 +1849,7 @@
ids
{:trigger "plugin:combine-as-variants" :variant-id variant-id})
(se/add-event plugin-id)))
(shape-proxy plugin-id variant-id))
(shape-proxy plugin-id file-id page-id variant-id))
(u/not-valid plugin-id :ids "One of the components is not on the same page or is already a variant"))))))

View File

@ -28,13 +28,6 @@
[app.util.text-editor :as ted]
[cuerdas.core :as str]))
;; This regex seems duplicated but probably in the future when we support diferent units
;; this will need to reflect changes for each property
(def ^:private font-size-re #"^\d*\.?\d*$")
(def ^:private line-height-re #"^\d*\.?\d*$")
(def ^:private letter-spacing-re #"^-?\d*\.?\d*$")
(def ^:private text-transform-re #"uppercase|capitalize|lowercase|none")
(def ^:private text-decoration-re #"underline|line-through|none")
(def ^:private text-direction-re #"ltr|rtl")
(def ^:private text-align-re #"left|center|right|justify")
@ -160,7 +153,7 @@
(let [font (fonts/find-font-data {:family value})
variant (fonts/get-default-variant font)]
(cond
(not (string? value))
(nil? font)
(u/not-valid plugin-id :fontFamily value)
(not (r/check-permission plugin-id "content:write"))
@ -182,9 +175,10 @@
:set
(fn [self value]
(let [font (fonts/get-font-data (obj/get self "fontId"))
variant (fonts/get-variant font value)]
variant (when (string? value)
(fonts/find-variant font {:id value}))]
(cond
(not (string? value))
(nil? variant)
(u/not-valid plugin-id :fontVariantId value)
(not (r/check-permission plugin-id "content:write"))
@ -207,7 +201,7 @@
(fn [_ value]
(let [value (str/trim (dm/str value))]
(cond
(or (empty? value) (not (re-matches font-size-re value)))
(not (txt/valid-font-size? value))
(u/not-valid plugin-id :fontSize value)
(not (r/check-permission plugin-id "content:write"))
@ -289,7 +283,7 @@
(fn [_ value]
(let [value (str/trim (dm/str value))]
(cond
(or (empty? value) (not (re-matches line-height-re value)))
(not (txt/valid-line-height? value))
(u/not-valid plugin-id :lineHeight value)
(not (r/check-permission plugin-id "content:write"))
@ -312,7 +306,7 @@
(fn [_ value]
(let [value (str/trim (dm/str value))]
(cond
(or (not (string? value)) (not (re-matches letter-spacing-re value)))
(not (txt/valid-letter-spacing? value))
(u/not-valid plugin-id :letterSpacing value)
(not (r/check-permission plugin-id "content:write"))
@ -334,7 +328,7 @@
:set
(fn [_ value]
(cond
(and (string? value) (not (re-matches text-transform-re value)))
(not (txt/valid-text-transform? value))
(u/not-valid plugin-id :textTransform value)
(not (r/check-permission plugin-id "content:write"))
@ -550,7 +544,8 @@
(fn [self value]
(let [id (obj/get self "$id")
font (fonts/get-font-data (obj/get self "fontId"))
variant (fonts/get-variant font value)]
variant (when (string? value)
(fonts/find-variant font {:id value}))]
(cond
(not variant)
(u/not-valid plugin-id :fontVariantId value)
@ -571,7 +566,7 @@
(let [id (obj/get self "$id")
value (str/trim (dm/str value))]
(cond
(or (empty? value) (not (re-matches font-size-re value)))
(not (txt/valid-font-size? value))
(u/not-valid plugin-id :fontSize value)
(not (r/check-permission plugin-id "content:write"))
@ -640,7 +635,7 @@
(let [id (obj/get self "$id")
value (str/trim (dm/str value))]
(cond
(or (empty? value) (not (re-matches line-height-re value)))
(not (txt/valid-line-height? value))
(u/not-valid plugin-id :lineHeight value)
(not (r/check-permission plugin-id "content:write"))
@ -659,7 +654,7 @@
(let [id (obj/get self "$id")
value (str/trim (dm/str value))]
(cond
(or (not (string? value)) (not (re-matches letter-spacing-re value)))
(not (txt/valid-letter-spacing? value))
(u/not-valid plugin-id :letterSpacing value)
(not (r/check-permission plugin-id "content:write"))
@ -677,7 +672,7 @@
(fn [self value]
(let [id (obj/get self "$id")]
(cond
(or (not (string? value)) (not (re-matches text-transform-re value)))
(not (txt/valid-text-transform? value))
(u/not-valid plugin-id :textTransform value)
(not (r/check-permission plugin-id "content:write"))

View File

@ -7,6 +7,7 @@
(ns app.plugins.tokens
(:require
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.files.tokens :as cfo]
[app.common.json :as json]
[app.common.schema :as sm]
@ -86,6 +87,50 @@
[attr]
(cto/token-attr? (token-attr-plugin->token-attr attr)))
(defn- token-name-schema
[file-id set-id token]
(let [tokens-lib (u/locate-tokens-lib file-id)
tokens (-> (ctob/get-tokens tokens-lib set-id)
(dissoc (:name token))
(ctob/tokens-tree))]
(cfo/make-token-name-schema tokens)))
(defn- token-value-schema
[token]
(let [base (cfo/make-token-value-schema (:type token))]
(if (= :font-family (:type token))
[:or :string base]
base)))
(defn- normalize-token-value
[token value]
(case (:type token)
:font-family (ctob/convert-dtcg-font-family value)
:typography (ctob/convert-dtcg-typography-composite value)
:shadow (ctob/convert-dtcg-shadow-composite value)
value))
(defn- valid-token-candidate?
[file-id set-id token attrs]
(let [tokens-lib (u/locate-tokens-lib file-id)
candidate (merge (datafy token) attrs)
tokens (-> (merge (ctob/get-all-tokens-map tokens-lib)
(ctob/get-tokens tokens-lib set-id))
(dissoc (:name token))
(assoc (:name candidate) candidate))
resolved (get (ts/resolve-tokens tokens) (:name candidate))]
(and (sm/validate (token-name-schema file-id set-id token) (:name candidate))
(sm/validate (cfo/make-token-value-schema (:type candidate)) (:value candidate))
(or (nil? (:description candidate))
(sm/validate cfo/schema:token-description (:description candidate)))
(contains? resolved :resolved-value)
(empty? (:errors resolved)))))
(defn valid-token-resolution?
[file-id set-id id]
(when-let [token (u/locate-token file-id set-id id)]
(valid-token-candidate? file-id set-id token {})))
(defn- apply-token-to-shapes
[plugin-id file-id set-id id shape-ids attrs]
(cond
@ -94,8 +139,17 @@
:else
(let [token (u/locate-token file-id set-id id)]
(if (some #(not (token-attr? %)) attrs)
(cond
(nil? token)
(u/not-valid plugin-id :applyToSelected id)
(not (valid-token-candidate? file-id set-id token {}))
(u/not-valid plugin-id :applyToSelected (:value token))
(some #(not (token-attr? %)) attrs)
(u/not-valid plugin-id :applyToSelected attrs)
:else
(st/emit!
(-> (dwta/toggle-token {:token token
:attrs (into #{} (map token-attr-plugin->token-attr) attrs)
@ -206,9 +260,8 @@
(fn [_]
(let [token (u/locate-token file-id set-id id)]
(ctob/get-name token)))
:schema (cfo/make-token-name-schema
(some-> (u/locate-tokens-lib file-id)
(ctob/get-tokens set-id)))
:schema (fn [_]
(token-name-schema file-id set-id (u/locate-token file-id set-id id)))
:set
(fn [_ value]
(cond
@ -216,8 +269,11 @@
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (-> (dwtl/update-token set-id id {:name value})
(se/add-event plugin-id)))))}
(let [token (u/locate-token file-id set-id id)]
(if (valid-token-candidate? file-id set-id token {:name value})
(st/emit! (-> (dwtl/update-token set-id id {:name value})
(se/add-event plugin-id)))
(u/not-valid plugin-id :name value)))))}
:type
{:this true
@ -232,14 +288,11 @@
(fn [_]
(let [token (u/locate-token file-id set-id id)]
(json/->js (:value token))))
:schema (let [token (u/locate-token file-id set-id id)
base (cfo/make-token-value-schema (:type token))]
;; plugin-types declares the fontFamilies value as
;; `string | string[]`, but the core schema only accepts a
;; vector/ref; also accept a plain string (normalized in :set).
(if (= :font-family (:type token))
[:or :string base]
base))
:decode/fn (fn [value]
(let [token (u/locate-token file-id set-id id)]
(normalize-token-value token (json/->clj value))))
:schema (fn [_]
(token-value-schema (u/locate-token file-id set-id id)))
:set
(fn [_ value]
(cond
@ -248,10 +301,10 @@
:else
(let [token (u/locate-token file-id set-id id)
value (cond-> value
(= :font-family (:type token))
(ctob/convert-dtcg-font-family))]
(st/emit! (dwtl/update-token set-id id {:value value})))))}
value (normalize-token-value token value)]
(if (valid-token-candidate? file-id set-id token {:value value})
(st/emit! (dwtl/update-token set-id id {:value value}))
(u/not-valid plugin-id :value value)))))}
:resolvedValue
{:this true
@ -303,9 +356,11 @@
;; - 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)
names (map :name (vals (ctob/get-tokens (u/locate-tokens-lib file-id) set-id)))
name (cfh/generate-unique-name (:name token) names :suffix "copy")
token' (ctob/make-token (-> (datafy token)
(dissoc :id
:modified-at)))]
(assoc :name name)
(dissoc :id :modified-at)))]
(st/emit! (-> (dwtl/create-token set-id token')
(se/add-event plugin-id)))
(token-proxy plugin-id file-id set-id (:id token')))))
@ -364,9 +419,10 @@
(if (some? set)
(ctob/get-name set)
initial-name)))
:schema (cfo/make-token-set-name-schema
(u/locate-tokens-lib file-id)
id)
:schema (fn [_]
(cfo/make-token-set-name-schema
(u/locate-tokens-lib file-id)
id))
:set
(fn [_ name]
(cond
@ -556,11 +612,12 @@
(fn [_]
(let [theme (u/locate-token-theme file-id id)]
(:group theme)))
:schema (let [theme (u/locate-token-theme file-id id)]
(cfo/make-token-theme-group-schema
(u/locate-tokens-lib file-id)
(:name theme)
(:id theme)))
:schema (fn [_]
(let [theme (u/locate-token-theme file-id id)]
(cfo/make-token-theme-group-schema
(u/locate-tokens-lib file-id)
(:name theme)
(:id theme))))
:set
(fn [_ group]
(cond
@ -577,11 +634,12 @@
(fn [_]
(let [theme (u/locate-token-theme file-id id)]
(:name theme)))
:schema (let [theme (u/locate-token-theme file-id id)]
(cfo/make-token-theme-name-schema
(u/locate-tokens-lib file-id)
(:id theme)
(:group theme)))
:schema (fn [_]
(let [theme (u/locate-token-theme file-id id)]
(cfo/make-token-theme-name-schema
(u/locate-tokens-lib file-id)
(:group theme)
(:id theme))))
:set
(fn [_ name]
(cond
@ -667,8 +725,14 @@
(u/not-valid plugin-id :duplicate "Plugin doesn't have 'content:write' permission")
:else
(let [theme (u/locate-token-theme file-id id)
(let [tokens-lib (u/locate-tokens-lib file-id)
theme (u/locate-token-theme file-id id)
names (->> (ctob/get-themes tokens-lib)
(filter #(= (:group theme) (:group %)))
(map :name))
name (cfh/generate-unique-name (:name theme) names :suffix "copy")
theme' (ctob/make-token-theme (-> (datafy theme)
(assoc :name name)
(dissoc :id
:modified-at)))]
(st/emit! (dwtl/create-token-theme theme'))
@ -732,17 +796,12 @@
:addSet
{:enumerable false
:schema [:tuple (-> (sm/schema (cfo/make-token-set-schema
(u/locate-tokens-lib file-id)
nil))
(sm/dissoc-key :id) ;; We don't allow plugins to set the id
;; Allow an optional `active` flag so a plugin can create
;; an already-active set in a single call. Newly created
;; sets are inactive by default (only active sets affect
;; shapes and reference resolution). `active` is not part
;; of the token-set data model, so the :fn strips it and
;; applies it through the set-activation logic.
(sm/merge [:map [:active {:optional true} ::sm/boolean]]))]
:schema (fn [_]
[:tuple (-> (sm/schema (cfo/make-token-set-schema
(u/locate-tokens-lib file-id)
nil))
(sm/dissoc-key :id)
(sm/merge [:map [:active {:optional true} ::sm/boolean]]))])
:fn (fn [attrs]
(cond

View File

@ -43,7 +43,7 @@
:set
(fn [value]
(cond
(not (sm/valid-safe-number? value))
(not (sm/valid-non-negative-safe-number? value))
(u/not-valid plugin-id :value value)
(not (r/check-permission plugin-id "content:write"))