From 3c3cc6f1a35d0fcf86c60511b46bc44ea6423ff4 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 9 Sep 2026 15:45:59 +0200 Subject: [PATCH] :sparkles: Add missing plugin data validations --- common/src/app/common/files/tokens.cljc | 27 ++- common/src/app/common/schema.cljc | 14 ++ common/src/app/common/types/grid.cljc | 16 +- common/src/app/common/types/page.cljc | 21 +- common/src/app/common/types/shape.cljc | 18 +- .../common/types/shape/background_blur.cljc | 4 +- common/src/app/common/types/shape/blur.cljc | 2 +- common/src/app/common/types/shape/export.cljc | 2 +- .../app/common/types/shape/interactions.cljc | 52 ++++- common/src/app/common/types/shape/layout.cljc | 24 +-- common/src/app/common/types/shape/shadow.cljc | 3 +- common/src/app/common/types/text.cljc | 34 +++ common/src/app/common/types/token.cljc | 10 +- common/src/app/common/types/variant.cljc | 32 ++- .../common_tests/files_migrations_test.cljc | 133 ++++++++++++ frontend/src/app/main/data/comments.cljs | 9 +- .../app/main/data/workspace/libraries.cljs | 11 + .../src/app/main/data/workspace/variants.cljs | 40 +++- frontend/src/app/main/ui/comments.cljs | 6 +- .../sidebar/options/menus/component.cljs | 31 +-- .../sidebar/options/menus/interactions.cljs | 14 +- .../sidebar/options/menus/typography.cljs | 12 +- .../main/ui/workspace/sidebar/sitemap.cljs | 4 +- frontend/src/app/plugins/api.cljs | 25 +-- frontend/src/app/plugins/comments.cljs | 4 +- frontend/src/app/plugins/flex.cljs | 24 +-- frontend/src/app/plugins/fonts.cljs | 17 +- frontend/src/app/plugins/grid.cljs | 30 +-- frontend/src/app/plugins/library.cljs | 202 +++++++++++------- frontend/src/app/plugins/page.cljs | 73 ++++--- frontend/src/app/plugins/parser.cljs | 3 +- frontend/src/app/plugins/shape.cljs | 116 ++++++---- frontend/src/app/plugins/text.cljs | 33 ++- frontend/src/app/plugins/tokens.cljs | 149 +++++++++---- frontend/src/app/plugins/tracks.cljs | 2 +- 35 files changed, 831 insertions(+), 366 deletions(-) diff --git a/common/src/app/common/files/tokens.cljc b/common/src/app/common/files/tokens.cljc index 89678417d0..8c5ccbd81d 100644 --- a/common/src/app/common/files/tokens.cljc +++ b/common/src/app/common/files/tokens.cljc @@ -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) diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc index 3ecfd15c37..bdc48c3acb 100644 --- a/common/src/app/common/schema.cljc +++ b/common/src/app/common/schema.cljc @@ -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)) diff --git a/common/src/app/common/types/grid.cljc b/common/src/app/common/types/grid.cljc index 4fa1e8bf4e..2846e3d076 100644 --- a/common/src/app/common/types/grid.cljc +++ b/common/src/app/common/types/grid.cljc @@ -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}) - diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc index b4631a3698..e7c4e12efd 100644 --- a/common/src/app/common/types/page.cljc +++ b/common/src/app/common/types/page.cljc @@ -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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 6a617c21cc..166c041f3b 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -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]] diff --git a/common/src/app/common/types/shape/background_blur.cljc b/common/src/app/common/types/shape/background_blur.cljc index 6eb6b7ad38..b25f9f07c5 100644 --- a/common/src/app/common/types/shape/background_blur.cljc +++ b/common/src/app/common/types/shape/background_blur.cljc @@ -12,5 +12,5 @@ [:map {:title "BackgroundBlur"} [:id ::sm/uuid] [:type [:enum :background-blur]] - [:value ::sm/safe-number] - [:hidden :boolean]]) \ No newline at end of file + [:value ::sm/non-negative-safe-number] + [:hidden :boolean]]) diff --git a/common/src/app/common/types/shape/blur.cljc b/common/src/app/common/types/shape/blur.cljc index 59decef09e..671edb41d3 100644 --- a/common/src/app/common/types/shape/blur.cljc +++ b/common/src/app/common/types/shape/blur.cljc @@ -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]]) diff --git a/common/src/app/common/types/shape/export.cljc b/common/src/app/common/types/shape/export.cljc index 5c54f0455b..5a7bf9566a 100644 --- a/common/src/app/common/types/shape/export.cljc +++ b/common/src/app/common/types/shape/export.cljc @@ -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]]) diff --git a/common/src/app/common/types/shape/interactions.cljc b/common/src/app/common/types/shape/interactions.cljc index 38db81323d..71d23acf2b 100644 --- a/common/src/app/common/types/shape/interactions.cljc +++ b/common/src/app/common/types/shape/interactions.cljc @@ -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)) diff --git a/common/src/app/common/types/shape/layout.cljc b/common/src/app/common/types/shape/layout.cljc index b66aabc27d..84a828add5 100644 --- a/common/src/app/common/types/shape/layout.cljc +++ b/common/src/app/common/types/shape/layout.cljc @@ -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]] diff --git a/common/src/app/common/types/shape/shadow.cljc b/common/src/app/common/types/shape/shadow.cljc index 1398a7b5eb..49b9f1c80d 100644 --- a/common/src/app/common/types/shape/shadow.cljc +++ b/common/src/app/common/types/shape/shadow.cljc @@ -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)) - diff --git a/common/src/app/common/types/text.cljc b/common/src/app/common/types/text.cljc index f7aeb37664..9e2c17e986 100644 --- a/common/src/app/common/types/text.cljc +++ b/common/src/app/common/types/text.cljc @@ -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]) diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 880149afbb..b7f641709e 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -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))) diff --git a/common/src/app/common/types/variant.cljc b/common/src/app/common/types/variant.cljc index 8a37939756..660fec5304 100644 --- a/common/src/app/common/types/variant.cljc +++ b/common/src/app/common/types/variant.cljc @@ -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" diff --git a/common/test/common_tests/files_migrations_test.cljc b/common/test/common_tests/files_migrations_test.cljc index 7a8f757c45..5bf2e55540 100644 --- a/common/test/common_tests/files_migrations_test.cljc +++ b/common/test/common_tests/files_migrations_test.cljc @@ -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"))) diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index 90e83ff4de..3371dcd592 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -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))))))))) - diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 60baa7e15d..c259fb2975 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -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?] diff --git a/frontend/src/app/main/data/workspace/variants.cljs b/frontend/src/app/main/data/workspace/variants.cljs index 9a3ee11095..09f6003a9d 100644 --- a/frontend/src/app/main/data/workspace/variants.cljs +++ b/frontend/src/app/main/data/workspace/variants.cljs @@ -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))))))) - diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index 2d6b061ea0..0d40aeddaa 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -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* diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index 5036ad87a7..8d46f46a3f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -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)))))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index f6bebb8d8f..4f2eb3918f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 06ece36af4..f4974abc7b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index ac18b484d0..3f1ec3ab8d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -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)))) diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index 0b265a0148..226b324c1f 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -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"))))) diff --git a/frontend/src/app/plugins/comments.cljs b/frontend/src/app/plugins/comments.cljs index 6c9a7cbcf5..ee07ff96ac 100644 --- a/frontend/src/app/plugins/comments.cljs +++ b/frontend/src/app/plugins/comments.cljs @@ -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 diff --git a/frontend/src/app/plugins/flex.cljs b/frontend/src/app/plugins/flex.cljs index 229cd24f99..57bf9f1953 100644 --- a/frontend/src/app/plugins/flex.cljs +++ b/frontend/src/app/plugins/flex.cljs @@ -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")) diff --git a/frontend/src/app/plugins/fonts.cljs b/frontend/src/app/plugins/fonts.cljs index 3f460a83d3..062b761c17 100644 --- a/frontend/src/app/plugins/fonts.cljs +++ b/frontend/src/app/plugins/fonts.cljs @@ -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") diff --git a/frontend/src/app/plugins/grid.cljs b/frontend/src/app/plugins/grid.cljs index 15771ff7b3..39e7e46e7d 100644 --- a/frontend/src/app/plugins/grid.cljs +++ b/frontend/src/app/plugins/grid.cljs @@ -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")) diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs index 35ff505a65..385c7e4181 100644 --- a/frontend/src/app/plugins/library.cljs +++ b/frontend/src/app/plugins/library.cljs @@ -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")) diff --git a/frontend/src/app/plugins/page.cljs b/frontend/src/app/plugins/page.cljs index 2be488813f..c75133a692 100644 --- a/frontend/src/app/plugins/page.cljs +++ b/frontend/src/app/plugins/page.cljs @@ -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))) diff --git a/frontend/src/app/plugins/parser.cljs b/frontend/src/app/plugins/parser.cljs index 5923289407..d505246b52 100644 --- a/frontend/src/app/plugins/parser.cljs +++ b/frontend/src/app/plugins/parser.cljs @@ -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))))) diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index d42c0529a3..78a3c161b6 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -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")))))) diff --git a/frontend/src/app/plugins/text.cljs b/frontend/src/app/plugins/text.cljs index 901deb9f5b..66b2f2fb88 100644 --- a/frontend/src/app/plugins/text.cljs +++ b/frontend/src/app/plugins/text.cljs @@ -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")) diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index b27019c7ba..4d15a5590b 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -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 diff --git a/frontend/src/app/plugins/tracks.cljs b/frontend/src/app/plugins/tracks.cljs index c5f38664f2..b5b62689ca 100644 --- a/frontend/src/app/plugins/tracks.cljs +++ b/frontend/src/app/plugins/tracks.cljs @@ -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"))