diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 7f88db5d2..2adf6b559 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -1166,7 +1166,7 @@ ; We need to trigger a sync if the shape has changed any ; attribute that participates in components synchronization. (and (= (:type operation) :set) - (get ctk/sync-attrs (:attr operation)))) + (ctk/component-attr? (:attr operation)))) any-sync? (some need-sync? operations)] (when any-sync? (if page-id diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index bc9ab68b0..98db43816 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -27,6 +27,7 @@ [app.common.types.shape-tree :as ctst] [app.common.types.shape.interactions :as ctsi] [app.common.types.shape.layout :as ctl] + [app.common.types.token :as cto] [app.common.types.typography :as cty] [app.common.uuid :as uuid] [clojure.set :as set] @@ -1479,6 +1480,44 @@ [{:type :set-remote-synced :remote-synced (:remote-synced shape)}]})))))) +(defn- update-tokens + "Token synchronization algorithm. Copy the applied tokens that have changed + in the origin shape to the dest shape (applying or removing as necessary). + + Only the given token attributes are synced." + [changes container dest-shape orig-shape token-attrs] + (let [orig-tokens (get orig-shape :applied-tokens {}) + dest-tokens (get dest-shape :applied-tokens {}) + dest-tokens' (reduce (fn [dest-tokens' token-attr] + (let [orig-token (get orig-tokens token-attr) + dest-token (get dest-tokens token-attr)] + (if (= orig-token dest-token) + dest-tokens' + (if (nil? orig-token) + (dissoc dest-tokens' token-attr) + (assoc dest-tokens' token-attr orig-token))))) + dest-tokens + token-attrs)] + (if (= dest-tokens dest-tokens') + changes + (-> changes + (update :redo-changes conj (make-change + container + {:type :mod-obj + :id (:id dest-shape) + :operations [{:type :set + :attr :applied-tokens + :val dest-tokens' + :ignore-touched true}]})) + (update :undo-changes conj (make-change + container + {:type :mod-obj + :id (:id dest-shape) + :operations [{:type :set + :attr :applied-tokens + :val dest-tokens + :ignore-touched true}]})))))) + (defn- update-attrs "The main function that implements the attribute sync algorithm. Copy attributes that have changed in the origin shape to the dest shape. @@ -1511,37 +1550,41 @@ (loop [attrs (->> (seq (keys ctk/sync-attrs)) ;; We don't update the flex-child attrs (remove ctk/swap-keep-attrs) - ;; We don't do automatic update of the `layout-grid-cells` property. (remove #(= :layout-grid-cells %))) + applied-tokens #{} roperations [] uoperations '()] (let [attr (first attrs)] (if (nil? attr) - (if (empty? roperations) + (if (and (empty? roperations) (empty? applied-tokens)) changes (let [all-parents (cfh/get-parent-ids (:objects container) (:id dest-shape))] - (-> changes - (update :redo-changes conj (make-change - container - {:type :mod-obj - :id (:id dest-shape) - :operations roperations})) - (update :redo-changes conj (make-change - container - {:type :reg-objects - :shapes all-parents})) - (update :undo-changes conj (make-change - container - {:type :mod-obj - :id (:id dest-shape) - :operations (vec uoperations)})) - (update :undo-changes concat [(make-change - container - {:type :reg-objects - :shapes all-parents})])))) + (cond-> changes + (seq roperations) + (-> (update :redo-changes conj (make-change + container + {:type :mod-obj + :id (:id dest-shape) + :operations roperations})) + (update :redo-changes conj (make-change + container + {:type :reg-objects + :shapes all-parents})) + (update :undo-changes conj (make-change + container + {:type :mod-obj + :id (:id dest-shape) + :operations (vec uoperations)})) + (update :undo-changes concat [(make-change + container + {:type :reg-objects + :shapes all-parents})])) + (seq applied-tokens) + (update-tokens container dest-shape origin-shape applied-tokens)))) + (let [;; position-data is a special case because can be affected by :geometry-group and :content-group ;; so, if the position-data changes but the geometry is touched we need to reset the position-data ;; so it's calculated again @@ -1564,14 +1607,21 @@ :val (get dest-shape attr) :ignore-touched true} - attr-group (get ctk/sync-attrs attr)] + attr-group (get ctk/sync-attrs attr) + token-attrs (cto/shape-attr->token-attrs attr) + applied-tokens' (cond-> applied-tokens + (not (and (touched attr-group) + omit-touched?)) + (into token-attrs))] (if (or (= (get origin-shape attr) (get dest-shape attr)) (and (touched attr-group) omit-touched?)) (recur (next attrs) + applied-tokens' roperations uoperations) (recur (next attrs) + applied-tokens' (conj roperations roperation) (conj uoperations uoperation))))))))) diff --git a/common/src/app/common/test_helpers/tokens.cljc b/common/src/app/common/test_helpers/tokens.cljc new file mode 100644 index 000000000..7ddb78a3d --- /dev/null +++ b/common/src/app/common/test_helpers/tokens.cljc @@ -0,0 +1,82 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.test-helpers.tokens + (:require + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.shapes :as ths] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape-tree :as ctst] + [app.common.types.token :as cto] + [app.common.types.tokens-lib :as ctob])) + +(defn add-tokens-lib + [file] + (ctf/update-file-data file #(update % :tokens-lib ctob/ensure-tokens-lib))) + +(defn update-tokens-lib + [file f] + (ctf/update-file-data file #(update % :tokens-lib f))) + +(defn- set-stroke-width + [shape stroke-width] + (let [strokes (if (seq (:strokes shape)) + (:strokes shape) + [{:stroke-style :solid + :stroke-alignment :inner + :stroke-width 1 + :stroke-color "#000000" + :stroke-opacity 1}]) + new-strokes (update strokes 0 assoc :stroke-width stroke-width)] + (ctn/set-shape-attr shape :strokes new-strokes {:ignore-touched true}))) + +(defn- set-stroke-color + [shape stroke-color] + (let [strokes (if (seq (:strokes shape)) + (:strokes shape) + [{:stroke-style :solid + :stroke-alignment :inner + :stroke-width 1 + :stroke-color "#000000" + :stroke-opacity 1}]) + new-strokes (update strokes 0 assoc :stroke-color stroke-color)] + (ctn/set-shape-attr shape :strokes new-strokes {:ignore-touched true}))) + +(defn- set-fill-color + [shape fill-color] + (let [fills (if (seq (:fills shape)) + (:fills shape) + [{:fill-color "#000000" + :fill-opacity 1}]) + new-fills (update fills 0 assoc :fill-color fill-color)] + (ctn/set-shape-attr shape :fills new-fills {:ignore-touched true}))) + +(defn apply-token-to-shape + [file shape-label token-name token-attrs shape-attrs resolved-value] + (let [page (thf/current-page file) + shape (ths/get-shape file shape-label) + shape' (as-> shape $ + (cto/apply-token-to-shape {:shape $ + :token {:name token-name} + :attributes token-attrs}) + (reduce (fn [shape attr] + (case attr + :stroke-width (set-stroke-width shape resolved-value) + :stroke-color (set-stroke-color shape resolved-value) + :fill (set-fill-color shape resolved-value) + (ctn/set-shape-attr shape attr resolved-value {:ignore-touched true}))) + $ + shape-attrs))] + + (ctf/update-file-data + file + (fn [file-data] + (ctpl/update-page file-data + (:id page) + #(ctst/set-shape % shape')))))) + diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 2af68c8d8..fc4ea0093 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -140,6 +140,14 @@ :layout-item-z-index :layout-item-align-self}) +(defn component-attr? + "Check if some attribute is one that is involved in component syncrhonization. + Note that design tokens also are involved, although they go by an alternate + route and thus they are not part of :sync-attrs." + [attr] + (or (get sync-attrs attr) + (= :applied-tokens attr))) + (defn instance-root? "Check if this shape is the head of a top instance." [shape] diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index ca0181604..5c5673459 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -18,6 +18,7 @@ [app.common.types.plugins :as ctpg] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] + [app.common.types.token :as ctt] [app.common.uuid :as uuid])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -540,14 +541,28 @@ ;; --- SHAPE UPDATE +(defn- get-token-groups + [shape new-applied-tokens] + (let [old-applied-tokens (d/nilv (:applied-tokens shape) #{}) + changed-token-attrs (filter #(not= (get old-applied-tokens %) (get new-applied-tokens %)) + ctt/all-keys) + changed-groups (into #{} + (comp (map ctt/token-attr->shape-attr) + (map #(get ctk/sync-attrs %)) + (filter some?)) + changed-token-attrs)] + changed-groups)) + (defn set-shape-attr "Assign attribute to shape with touched logic. The returned shape will contain a metadata associated with it indicating if shape is touched or not." [shape attr val & {:keys [ignore-touched ignore-geometry]}] - (let [group (get ctk/sync-attrs attr) - shape-val (get shape attr) + (let [group (get ctk/sync-attrs attr) + token-groups (when (= attr :applied-tokens) + (get-token-groups shape val)) + shape-val (get shape attr) ignore? (or ignore-touched @@ -585,9 +600,15 @@ ;; set the "touched" flag for the group the attribute belongs to. ;; In some cases we need to ignore touched only if the attribute is ;; geometric (position, width or transformation). - (and in-copy? group (not ignore?) (not equal?) - (not (and ignore-geometry is-geometry?))) - (-> (update :touched ctk/set-touched-group group) + (and in-copy? + (or (and group (not equal?)) (seq token-groups)) + (not ignore?) (not (and ignore-geometry is-geometry?))) + (-> (update :touched (fn [touched] + (reduce #(ctk/set-touched-group %1 %2) + touched + (if group + (cons group token-groups) + token-groups)))) (dissoc :remote-synced)) (nil? val) diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 1075ba461..65ef9ba79 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -6,8 +6,10 @@ (ns app.common.types.token (:require + [app.common.data :as d] [app.common.schema :as sm] [app.common.schema.registry :as sr] + [clojure.data :as data] [clojure.set :as set] [malli.util :as mu])) @@ -150,6 +152,15 @@ (def rotation-keys (schema-keys ::rotation)) +(def all-keys (set/union color-keys + border-radius-keys + stroke-width-keys + sizing-keys + opacity-keys + spacing-keys + dimensions-keys + rotation-keys)) + (sm/register! ^{::sm/type ::tokens} [:map {:title "Applied Tokens"}]) @@ -163,3 +174,58 @@ ::spacing ::rotation ::dimensions]) + +(defn shape-attr->token-attrs + [shape-attr] + (cond + (= :fills shape-attr) #{:fill} + (= :strokes shape-attr) #{:stroke-color :stroke-width} + (border-radius-keys shape-attr) #{shape-attr} + (sizing-keys shape-attr) #{shape-attr} + (opacity-keys shape-attr) #{shape-attr} + (spacing-keys shape-attr) #{shape-attr} + (rotation-keys shape-attr) #{shape-attr})) + +(defn token-attr->shape-attr + [token-attr] + (case token-attr + :fill :fills + :stroke-color :strokes + :stroke-width :strokes + token-attr)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TOKENS IN SHAPES +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- toggle-or-apply-token + "Remove any shape attributes from token if they exists. + Othewise apply token attributes." + [shape token] + (let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)] + (merge {} shape-leftover token-leftover))) + +(defn- token-from-attributes [token attributes] + (->> (map (fn [attr] [attr (:name token)]) attributes) + (into {}))) + +(defn- apply-token-to-attributes [{:keys [shape token attributes]}] + (let [token (token-from-attributes token attributes)] + (toggle-or-apply-token shape token))) + +(defn apply-token-to-shape + [{:keys [shape token attributes] :as _props}] + (let [applied-tokens (apply-token-to-attributes {:shape shape + :token token + :attributes attributes})] + (update shape :applied-tokens #(merge % applied-tokens)))) + +(defn maybe-apply-token-to-shape + "When the passed `:token` is non-nil apply it to the `:applied-tokens` on a shape." + [{:keys [shape token _attributes] :as props}] + (if token + (apply-token-to-shape props) + shape)) + +(defn unapply-token-id [shape attributes] + (update shape :applied-tokens d/without-keys attributes)) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 6cfd4550c..f59b6106e 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -634,6 +634,11 @@ When `before-set-name` is nil, move set to bottom") (delete-token-from-set [_ set-name token-name] "delete a token from a set") (toggle-set-in-theme [_ group-name theme-name set-name] "toggle a set used / not used in a theme") (get-active-themes-set-names [_] "set of set names that are active in the the active themes") + (sets-at-path-all-active? [_ prefixed-path] "compute active state for child sets at `prefixed-path`. +Will return a value that matches this schema: +`:none` None of the nested sets are active +`:all` All of the nested sets are active +`:partial` Mixed active state of nested sets") (get-active-themes-set-tokens [_] "set of set names that are active in the the active themes") (encode-dtcg [_] "Encodes library to a dtcg compatible json string") (decode-dtcg-json [_ parsed-json] "Decodes parsed json containing tokens and converts to library") @@ -894,6 +899,19 @@ When `before-set-name` is nil, move set to bottom") (mapcat :sets) (get-active-themes this))) + (sets-at-path-all-active? [this prefixed-path] + (let [active-set-names (get-active-themes-set-names this)] + (if (seq active-set-names) + (let [path-active-set-names (->> (get-sets-at-prefix-path this prefixed-path) + (map :name) + (into #{})) + difference (set/difference path-active-set-names active-set-names)] + (cond + (empty? difference) :all + (seq (set/intersection path-active-set-names active-set-names)) :partial + :else :none)) + :none))) + (get-active-themes-set-tokens [this] (let [sets-order (get-ordered-set-names this) active-themes (get-active-themes this) diff --git a/common/test/common_tests/logic/comp_sync_test.cljc b/common/test/common_tests/logic/comp_sync_test.cljc index 94f093ff3..f970f5fcb 100644 --- a/common/test/common_tests/logic/comp_sync_test.cljc +++ b/common/test/common_tests/logic/comp_sync_test.cljc @@ -193,7 +193,6 @@ (ths/add-sample-shape :free-shape)) page (thf/current-page file) - main-root (ths/get-shape file :main-root) ;; ==== Action changes1 (cls/generate-relocate (pcb/empty-changes) @@ -203,9 +202,6 @@ 0 ; to-index #{(thi/id :free-shape)}) ; ids - - - updated-file (thf/apply-changes file changes1) changes2 (cll/generate-sync-file-changes (pcb/empty-changes) @@ -491,4 +487,4 @@ (t/is (= (:fill-color fill') "#fabada")) (t/is (= (:fill-opacity fill') 1)) (t/is (= (:touched copy2-root') nil)) - (t/is (= (:touched copy2-child') nil)))) \ No newline at end of file + (t/is (= (:touched copy2-child') nil)))) diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc index 2c1fcf077..3129e93fb 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -417,8 +417,39 @@ expected-tokens (ctob/get-active-themes-set-tokens tokens-lib) expected-token-names (mapv key expected-tokens)] (t/is (= '("set-a" "set-b" "inactive-set") expected-order)) - (t/is (= ["set-a-token" "set-b-token"] expected-token-names))))) + (t/is (= ["set-a-token" "set-b-token"] expected-token-names)))) + (t/testing "sets-at-path-active-state" + (let [tokens-lib (-> (ctob/make-tokens-lib) + + (ctob/add-set (ctob/make-token-set :name "foo/bar/baz")) + (ctob/add-set (ctob/make-token-set :name "foo/bar/bam")) + + (ctob/add-theme (ctob/make-token-theme :name "none")) + (ctob/add-theme (ctob/make-token-theme :name "partial" + :sets #{"foo/bar/baz"})) + (ctob/add-theme (ctob/make-token-theme :name "all" + :sets #{"foo/bar/baz" + "foo/bar/bam"})) + (ctob/add-theme (ctob/make-token-theme :name "invalid" + :sets #{"foo/missing"}))) + + expected-none (-> tokens-lib + (ctob/set-active-themes #{"/none"}) + (ctob/sets-at-path-all-active? "G-foo")) + expected-all (-> tokens-lib + (ctob/set-active-themes #{"/all"}) + (ctob/sets-at-path-all-active? "G-foo")) + expected-partial (-> tokens-lib + (ctob/set-active-themes #{"/partial"}) + (ctob/sets-at-path-all-active? "G-foo")) + expected-invalid-none (-> tokens-lib + (ctob/set-active-themes #{"/invalid"}) + (ctob/sets-at-path-all-active? "G-foo"))] + (t/is (= :none expected-none)) + (t/is (= :all expected-all)) + (t/is (= :partial expected-partial)) + (t/is (= :none expected-invalid-none))))) (t/deftest token-theme-in-a-lib (t/testing "add-token-theme" diff --git a/frontend/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs index 71bfd426c..d0bbc890b 100644 --- a/frontend/src/app/main/data/tokens.cljs +++ b/frontend/src/app/main/data/tokens.cljs @@ -6,7 +6,6 @@ (ns app.main.data.tokens (:require - [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] [app.common.geom.point :as gpt] @@ -15,11 +14,9 @@ [app.main.data.changes :as dch] [app.main.data.workspace.shapes :as dwsh] [app.main.refs :as refs] - [app.main.ui.workspace.tokens.token :as wtt] [app.main.ui.workspace.tokens.token-set :as wtts] [app.main.ui.workspace.tokens.update :as wtu] [beicon.v2.core :as rx] - [clojure.data :as data] [cuerdas.core :as str] [potok.v2.core :as ptk])) @@ -51,38 +48,6 @@ ;; TOKENS Actions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn toggle-or-apply-token - "Remove any shape attributes from token if they exists. - Othewise apply token attributes." - [shape token] - (let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)] - (merge {} shape-leftover token-leftover))) - -(defn token-from-attributes [token attributes] - (->> (map (fn [attr] [attr (wtt/token-identifier token)]) attributes) - (into {}))) - -(defn unapply-token-id [shape attributes] - (update shape :applied-tokens d/without-keys attributes)) - -(defn apply-token-to-attributes [{:keys [shape token attributes]}] - (let [token (token-from-attributes token attributes)] - (toggle-or-apply-token shape token))) - -(defn apply-token-to-shape - [{:keys [shape token attributes] :as _props}] - (let [applied-tokens (apply-token-to-attributes {:shape shape - :token token - :attributes attributes})] - (update shape :applied-tokens #(merge % applied-tokens)))) - -(defn maybe-apply-token-to-shape - "When the passed `:token` is non-nil apply it to the `:applied-tokens` on a shape." - [{:keys [shape token _attributes] :as props}] - (if token - (apply-token-to-shape props) - shape)) - (defn get-token-data-from-token-id [id] (let [workspace-data (deref refs/workspace-data)] diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index b11632496..dafd3de4e 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -82,7 +82,7 @@ (assoc-in [:workspace-global :picked-shift?] shift?))))) (defn transform-fill - [state ids color transform] + [state ids color transform & options] (let [objects (wsh/lookup-page-objects state) is-text? #(= :text (:type (get objects %))) @@ -118,8 +118,8 @@ (rx/concat (rx/of (dwu/start-undo-transaction undo-id)) - (rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids)) - (rx/of (dwsh/update-shapes shape-ids transform-attrs)) + (rx/from (map #(apply dwt/update-text-with-function % transform-attrs options) text-ids)) + (rx/of (dwsh/update-shapes shape-ids transform-attrs options)) (rx/of (dwu/commit-undo-transaction undo-id))))) (defn swap-attrs [shape attr index new-index] @@ -146,7 +146,7 @@ (rx/of (dwsh/update-shapes shape-ids transform-attrs))))))) (defn change-fill - [ids color position] + [ids color position & options] (ptk/reify ::change-fill ptk/WatchEvent (watch [_ state _] @@ -155,18 +155,18 @@ (cond-> (not (contains? shape :fills)) (assoc :fills [])) (assoc-in [:fills position] (into {} attrs))))] - (transform-fill state ids color change-fn))))) + (apply transform-fill state ids color change-fn options))))) (defn change-fill-and-clear - [ids color] + [ids color & options] (ptk/reify ::change-fill-and-clear ptk/WatchEvent (watch [_ state _] (let [set (fn [shape attrs] (assoc shape :fills [attrs]))] - (transform-fill state ids color set))))) + (apply transform-fill state ids color set options))))) (defn add-fill - [ids color] + [ids color & options] (dm/assert! "expected a valid color struct" @@ -182,10 +182,10 @@ (let [add (fn [shape attrs] (-> shape (update :fills #(into [attrs] %))))] - (transform-fill state ids color add))))) + (apply transform-fill state ids color add options))))) (defn remove-fill - [ids color position] + [ids color position & options] (dm/assert! "expected a valid color struct" @@ -203,10 +203,10 @@ (mapv second))) remove (fn [shape _] (update shape :fills remove-fill-by-index position))] - (transform-fill state ids color remove))))) + (apply transform-fill state ids color remove options))))) (defn remove-all-fills - [ids color] + [ids color & options] (dm/assert! "expected a valid color struct" @@ -220,7 +220,7 @@ ptk/WatchEvent (watch [_ state _] (let [remove-all (fn [shape _] (assoc shape :fills []))] - (transform-fill state ids color remove-all))))) + (apply transform-fill state ids color remove-all options))))) (defn change-hide-fill-on-export [ids hide-fill-on-export] @@ -237,7 +237,7 @@ (d/merge shape attrs) shape)))))))) (defn change-stroke - [ids attrs index] + [ids attrs index & options] (ptk/reify ::change-stroke ptk/WatchEvent (watch [_ _ _] @@ -286,7 +286,8 @@ (assoc :strokes []) :always - (assoc-in [:strokes index] new-attrs)))))))))) + (assoc-in [:strokes index] new-attrs)))) + options)))))) (defn change-shadow [ids attrs index] diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index e04b4dfb2..f4187ab2b 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -786,7 +786,6 @@ (rx/map #(reset-component %) (rx/from ids)) (rx/of (dwu/commit-undo-transaction undo-id))))))) - (defn update-component "Modify the component linked to the shape with the given id, in the current page, so that all attributes of its shapes are equal to the diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 773b4f146..ed745892a 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -465,8 +465,10 @@ ([] (apply-modifiers nil)) - ([{:keys [modifiers undo-transation? stack-undo? ignore-constraints ignore-snap-pixel undo-group] - :or {undo-transation? true stack-undo? false ignore-constraints false ignore-snap-pixel false}}] + ([{:keys [modifiers undo-transation? stack-undo? ignore-constraints + ignore-snap-pixel ignore-touched undo-group] + :or {undo-transation? true stack-undo? false ignore-constraints false + ignore-snap-pixel false ignore-touched false}}] (ptk/reify ::apply-modifiers ptk/WatchEvent (watch [_ state _] @@ -515,6 +517,7 @@ {:reg-objects? true :stack-undo? stack-undo? :ignore-tree ignore-tree + :ignore-touched ignore-touched :undo-group undo-group ;; Attributes that can change in the transform. This way we don't have to check ;; all the attributes diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index 3f7440c06..a8e614093 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -260,13 +260,13 @@ (rx/of (with-meta event (meta it))))))))) (defn update-layout - [ids changes] + [ids changes & options] (ptk/reify ::update-layout ptk/WatchEvent (watch [_ _ _] (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dwsh/update-shapes ids (d/patch-object changes)) + (dwsh/update-shapes ids (d/patch-object changes) options) (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))) @@ -516,7 +516,7 @@ (assoc :layout-item-v-sizing :fix)))) (defn update-layout-child - [ids changes] + [ids changes & options] (ptk/reify ::update-layout-child ptk/WatchEvent (watch [_ state _] @@ -525,8 +525,8 @@ parent-ids (->> ids (map #(cfh/get-parent-id objects %))) undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dwsh/update-shapes ids (d/patch-object changes)) - (dwsh/update-shapes children-ids (partial fix-child-sizing objects changes)) + (dwsh/update-shapes ids (d/patch-object changes) options) + (dwsh/update-shapes children-ids (partial fix-child-sizing objects changes) options) (dwsh/update-shapes parent-ids (fn [parent objects] @@ -534,7 +534,7 @@ (fix-parent-sizing objects (set ids) changes) (cond-> (ctl/grid-layout? parent) (ctl/assign-cells objects)))) - {:with-objects? true}) + (merge options {:with-objects? true})) (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 20e24611b..042fce3a5 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -432,7 +432,7 @@ (txt/transform-nodes (some-fn txt/is-text-node? txt/is-paragraph-node?) migrate-node content)) (defn update-text-with-function - [id update-node-fn] + [id update-node-fn & options] (ptk/reify ::update-text-with-function ptk/UpdateEvent (update [_ state] @@ -464,7 +464,7 @@ (-> shape (dissoc :fills) (d/update-when :content update-content)))] - (rx/of (dwsh/update-shapes shape-ids update-shape))))) + (rx/of (dwsh/update-shapes shape-ids update-shape options))))) ptk/EffectEvent (effect [_ state _] diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index c4e2a8064..1f51afc0d 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -301,7 +301,7 @@ (defn update-dimensions "Change size of shapes, from the sideber options form. Will ignore pixel snap used in the options side panel" - [ids attr value] + [ids attr value & options] (dm/assert! (number? value)) (dm/assert! "expected valid coll of uuids" @@ -324,7 +324,7 @@ ptk/WatchEvent (watch [_ _ _] - (rx/of (dwm/apply-modifiers))))) + (rx/of (dwm/apply-modifiers options))))) (defn change-orientation "Change orientation of shapes, from the sidebar options form. @@ -402,7 +402,7 @@ "Rotate shapes a fixed angle, from a keyboard action." ([ids rotation] (increase-rotation ids rotation nil)) - ([ids rotation params] + ([ids rotation params & options] (ptk/reify ::increase-rotation ptk/WatchEvent (watch [_ state _] @@ -411,7 +411,7 @@ shapes (->> ids (map #(get objects %)))] (rx/concat (rx/of (dwm/set-delta-rotation-modifiers rotation shapes params)) - (rx/of (dwm/apply-modifiers)))))))) + (rx/of (dwm/apply-modifiers options)))))))) ;; -- Move ---------------------------------------------------------- diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 16ebf7a76..dce5e7463 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -502,6 +502,14 @@ (def workspace-active-theme-paths (l/derived (d/nilf ctob/get-active-theme-paths) tokens-lib)) +(defn token-sets-at-path-all-active? + [prefixed-path] + (l/derived + (fn [lib] + (when lib + (ctob/sets-at-path-all-active? lib prefixed-path))) + tokens-lib)) + (def workspace-active-theme-paths-no-hidden (l/derived #(disj % ctob/hidden-token-theme-path) workspace-active-theme-paths)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 3be948976..b56788ab5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -13,9 +13,9 @@ [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] [app.common.types.shape.radius :as ctsr] + [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob] [app.main.constants :refer [size-presets]] - [app.main.data.tokens :as dt] [app.main.data.workspace :as udw] [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.shapes :as dwsh] @@ -342,7 +342,7 @@ (let [token-value (wtc/maybe-resolve-token-value token)] (st/emit! (change-radius (fn [shape] - (-> (dt/unapply-token-id shape (wtty/token-attributes :border-radius)) + (-> (cto/unapply-token-id shape (wtty/token-attributes :border-radius)) (ctsr/set-radius-1 token-value)))))))) on-radius-1-change @@ -352,9 +352,9 @@ (let [token-value (wtc/maybe-resolve-token-value value)] (st/emit! (change-radius (fn [shape] - (-> (dt/maybe-apply-token-to-shape {:token (when token-value value) - :shape shape - :attributes (wtty/token-attributes :border-radius)}) + (-> (cto/maybe-apply-token-to-shape {:token (when token-value value) + :shape shape + :attributes (wtty/token-attributes :border-radius)}) (ctsr/set-radius-1 (or token-value value))))))))) on-radius-multi-change diff --git a/frontend/src/app/main/ui/workspace/tokens/changes.cljs b/frontend/src/app/main/ui/workspace/tokens/changes.cljs index e0c0f0910..8e7bdf370 100644 --- a/frontend/src/app/main/ui/workspace/tokens/changes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/changes.cljs @@ -95,20 +95,9 @@ (when (ctsr/has-radius? shape) (ctsr/set-radius-1 shape value))) {:reg-objects? true + :ignore-touched true :attrs ctt/border-radius-keys})) -(defn update-opacity [value shape-ids] - (when (<= 0 value 1) - (dwsh/update-shapes shape-ids #(assoc % :opacity value)))) - -(defn update-rotation [value shape-ids] - (ptk/reify ::update-shape-rotation - ptk/WatchEvent - (watch [_ _ _] - (rx/of - (udw/trigger-bounding-box-cloaking shape-ids) - (udw/increase-rotation shape-ids value))))) - (defn update-shape-radius-single-corner [value shape-ids attributes] (dwsh/update-shapes shape-ids (fn [shape] @@ -117,8 +106,23 @@ (:rx shape) (ctsr/switch-to-radius-4) :always (ctsr/set-radius-4 (first attributes) value)))) {:reg-objects? true + :ignore-touched true :attrs [:rx :ry :r1 :r2 :r3 :r4]})) +(defn update-opacity [value shape-ids] + (when (<= 0 value 1) + (dwsh/update-shapes shape-ids + #(assoc % :opacity value) + {:ignore-touched true}))) + +(defn update-rotation [value shape-ids] + (ptk/reify ::update-shape-rotation + ptk/WatchEvent + (watch [_ _ _] + (rx/of + (udw/trigger-bounding-box-cloaking shape-ids) + (udw/increase-rotation shape-ids value nil :ignore-touched true))))) + (defn update-stroke-width [value shape-ids] (dwsh/update-shapes shape-ids @@ -126,6 +130,7 @@ (when (seq (:strokes shape)) (assoc-in shape [:strokes 0 :stroke-width] value))) {:reg-objects? true + :ignore-touched true :attrs [:strokes]})) (defn update-color [f value shape-ids] @@ -133,7 +138,7 @@ (tinycolor/valid-color) (tinycolor/->hex) (str "#"))] - (f shape-ids {:color color} 0))) + (apply f shape-ids {:color color} 0 [:ignore-touched true]))) (defn update-fill [value shape-ids] @@ -156,8 +161,8 @@ ptk/WatchEvent (watch [_ _ _] (rx/of - (when (:width attributes) (dwt/update-dimensions shape-ids :width value)) - (when (:height attributes) (dwt/update-dimensions shape-ids :height value)))))) + (when (:width attributes) (dwt/update-dimensions shape-ids :width value :ignore-touched true)) + (when (:height attributes) (dwt/update-dimensions shape-ids :height value :ignore-touched true)))))) (defn- attributes->layout-gap [attributes value] (let [layout-gap (-> (set/intersection attributes #{:column-gap :row-gap}) @@ -165,7 +170,9 @@ {:layout-gap layout-gap})) (defn update-layout-padding [value shape-ids attrs] - (dwsl/update-layout shape-ids {:layout-padding (zipmap attrs (repeat value))})) + (dwsl/update-layout shape-ids + {:layout-padding (zipmap attrs (repeat value))} + :ignore-touched true)) (defn update-layout-spacing [value shape-ids attributes] (ptk/reify ::update-layout-spacing @@ -177,7 +184,9 @@ (map :id))) layout-attributes (attributes->layout-gap attributes value)] (rx/of - (dwsl/update-layout layout-shape-ids layout-attributes)))))) + (dwsl/update-layout layout-shape-ids + layout-attributes + :ignore-touched true)))))) (defn update-shape-position [value shape-ids attributes] (ptk/reify ::update-shape-position @@ -195,4 +204,4 @@ :layout-item-max-w value :layout-item-max-h value} (select-keys attributes))] - (dwsl/update-layout-child shape-ids props))))) + (dwsl/update-layout-child shape-ids props :ignore-touched true))))) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs index 476156b61..a3ff1a8b1 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs @@ -254,41 +254,41 @@ [{:keys [state set-state]}] (let [{:keys [theme-path]} @state [_ theme-group theme-name] theme-path + ordered-token-sets (mf/deref refs/workspace-ordered-token-sets) token-sets (mf/deref refs/workspace-token-sets-tree) theme (mf/deref (refs/workspace-token-theme theme-group theme-name)) + theme-state (mf/use-state theme) + lib (-> (ctob/make-tokens-lib) + (ctob/add-theme @theme-state) + (ctob/add-sets ordered-token-sets) + (ctob/activate-theme (:group @theme-state) (:name @theme-state))) + + ;; Form / Modal handlers on-back #(set-state (constantly {:type :themes-overview})) on-submit #(st/emit! (wdt/update-token-theme [(:group theme) (:name theme)] %)) {:keys [dropdown-open? _on-open-dropdown on-close-dropdown on-toggle-dropdown]} (wtco/use-dropdown-open-state) - theme-state (mf/use-state theme) disabled? (-> (:name @theme-state) (str/trim) (str/empty?)) - token-set-active? (mf/use-callback - (mf/deps theme-state) - (fn [set-name] - (get-in @theme-state [:sets set-name]))) - on-toggle-token-set (mf/use-callback - (mf/deps theme-state) - (fn [set-name] - (swap! theme-state #(ctob/toggle-set % set-name)))) - on-click-token-set (mf/use-callback - (mf/deps on-toggle-token-set) - (fn [prefixed-set-path-str] - (let [set-name (ctob/prefixed-set-path-string->set-name-string prefixed-set-path-str)] - (on-toggle-token-set set-name)))) - on-change-field (fn [field value] - (swap! theme-state #(assoc % field value))) - on-save-form (mf/use-callback - (mf/deps theme-state on-submit) - (fn [e] - (dom/prevent-default e) - (let [theme (-> @theme-state - (update :name str/trim) - (update :group str/trim) - (update :description str/trim))] - (when-not (str/empty? (:name theme)) - (on-submit theme))) - (on-back))) + + on-change-field + (mf/use-fn + (fn [field value] + (swap! theme-state #(assoc % field value)))) + + on-save-form + (mf/use-callback + (mf/deps theme-state on-submit) + (fn [e] + (dom/prevent-default e) + (let [theme (-> @theme-state + (update :name str/trim) + (update :group str/trim) + (update :description str/trim))] + (when-not (str/empty? (:name theme)) + (on-submit theme))) + (on-back))) + close-modal (mf/use-fn (fn [e] @@ -300,7 +300,33 @@ (mf/deps theme on-back) (fn [] (st/emit! (wdt/delete-token-theme (:group theme) (:name theme))) - (on-back)))] + (on-back))) + + ;; Sets tree handlers + token-set-group-active? + (mf/use-callback + (mf/deps theme-state) + (fn [prefixed-path] + (ctob/sets-at-path-all-active? lib prefixed-path))) + + token-set-active? + (mf/use-callback + (mf/deps theme-state) + (fn [set-name] + (get-in @theme-state [:sets set-name]))) + + on-toggle-token-set + (mf/use-callback + (mf/deps theme-state) + (fn [set-name] + (swap! theme-state #(ctob/toggle-set % set-name)))) + + on-click-token-set + (mf/use-callback + (mf/deps on-toggle-token-set) + (fn [prefixed-set-path-str] + (let [set-name (ctob/prefixed-set-path-string->set-name-string prefixed-set-path-str)] + (on-toggle-token-set set-name))))] [:div {:class (stl/css :themes-modal-wrapper)} [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :themes-modal-title)} @@ -327,6 +353,7 @@ {:token-sets token-sets :token-set-selected? (constantly false) :token-set-active? token-set-active? + :token-set-group-active? token-set-group-active? :on-select on-click-token-set :on-toggle-token-set on-toggle-token-set :origin "theme-modal" diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index b53c03c2f..6d67e8232 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.tokens.sets (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.common.types.tokens-lib :as ctob] [app.main.data.tokens :as wdt] [app.main.refs :as refs] @@ -68,9 +69,28 @@ :auto-focus true :default-value default-value}])) +(mf/defc checkbox + [{:keys [checked aria-label on-click]}] + (let [all? (true? checked) + mixed? (= checked "mixed") + checked? (or all? mixed?)] + [:div {:role "checkbox" + :aria-checked (dm/str checked) + :tabindex 0 + :class (stl/css-case :checkbox-style true + :checkbox-checked-style checked?) + :on-click on-click} + (when checked? + [:> icon* + {:aria-label aria-label + :class (stl/css :check-icon) + :size "s" + :id (if mixed? ic/remove ic/tick)}])])) + (mf/defc sets-tree-set-group - [{:keys [label tree-depth tree-path selected? collapsed? editing? on-edit on-edit-reset on-edit-submit]}] + [{:keys [label tree-depth tree-path active? selected? collapsed? editing? on-edit on-edit-reset on-edit-submit]}] (let [editing?' (editing? tree-path) + active?' (active? tree-path) on-context-menu (mf/use-fn (mf/deps editing? tree-path) @@ -114,9 +134,16 @@ :on-cancel on-edit-reset :on-create on-edit-reset :on-submit on-edit-submit'}] - [:div {:class (stl/css :set-name) - :on-double-click on-double-click} - label])])) + [:* + [:div {:class (stl/css :set-name) + :on-double-click on-double-click} + label] + [:& checkbox + {:checked (case active?' + :all true + :partial "mixed" + :none false) + :arial-label (tr "workspace.token.select-set")}]])])) (mf/defc sets-tree-set [{:keys [set label tree-depth tree-path selected? on-select active? on-toggle editing? on-edit on-edit-reset on-edit-submit]}] @@ -173,18 +200,14 @@ [:div {:class (stl/css :set-name) :on-double-click on-double-click} label] - [:button {:type "button" - :on-click on-checkbox-click - :class (stl/css-case :checkbox-style true - :checkbox-checked-style active?')} - (when active?' - [:> icon* {:aria-label (tr "workspace.token.select-set") - :class (stl/css :check-icon) - :size "s" - :id ic/tick}])]])])) + [:& checkbox + {:on-click on-checkbox-click + :arial-label (tr "workspace.token.select-set") + :checked active?'}]])])) (mf/defc sets-tree [{:keys [active? + group-active? editing? on-edit on-edit-reset @@ -227,6 +250,7 @@ set-group? [:& sets-tree-set-group {:selected? (selected? tree-path) + :active? group-active? :on-select on-select :label set-fname :collapsed? collapsed? @@ -249,6 +273,7 @@ :selected? selected? :on-toggle on-toggle :active? active? + :group-active? group-active? :editing? editing? :on-edit on-edit :on-edit-reset on-edit-reset @@ -261,6 +286,7 @@ on-update-token-set-group token-set-selected? token-set-active? + token-set-group-active? on-create-token-set on-toggle-token-set origin @@ -268,10 +294,9 @@ context] :as _props}] (let [{:keys [editing? new? on-edit on-reset] :as ctx} (or context (sets-context/use-context))] - [:ul {:class (stl/css :sets-list)} - (if (and - (= origin "theme-modal") - (empty? token-sets)) + [:fieldset {:class (stl/css :sets-list)} + (if (and (= origin "theme-modal") + (empty? token-sets)) [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message-sets)} (tr "workspace.token.no-sets-create")] (if (and (= origin "theme-modal") @@ -284,6 +309,7 @@ :selected? token-set-selected? :on-select on-select :active? token-set-active? + :group-active? token-set-group-active? :on-toggle on-toggle-token-set :editing? editing? :on-edit on-edit @@ -314,11 +340,15 @@ token-set-active? (mf/use-fn (mf/deps active-token-set-names) (fn [set-name] - (get active-token-set-names set-name)))] + (get active-token-set-names set-name))) + token-set-group-active? (mf/use-fn + (fn [prefixed-path] + @(refs/token-sets-at-path-all-active? prefixed-path)))] [:& controlled-sets-list {:token-sets token-sets :token-set-selected? token-set-selected? :token-set-active? token-set-active? + :token-set-group-active? token-set-group-active? :on-select on-select-token-set-click :origin "set-panel" :on-toggle-token-set on-toggle-token-set-click diff --git a/frontend/src/app/main/ui/workspace/tokens/update.cljs b/frontend/src/app/main/ui/workspace/tokens/update.cljs index 2d4120b01..376e887ac 100644 --- a/frontend/src/app/main/ui/workspace/tokens/update.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/update.cljs @@ -2,8 +2,8 @@ (:require [app.common.types.token :as ctt] [app.main.data.workspace.shape-layout :as dwsl] + [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] - [app.main.refs :as refs] [app.main.ui.workspace.tokens.changes :as wtch] [app.main.ui.workspace.tokens.style-dictionary :as wtsd] [app.main.ui.workspace.tokens.token-set :as wtts] @@ -112,8 +112,8 @@ update-infos))) shapes-update-info)) -(defn update-tokens [resolved-tokens] - (->> @refs/workspace-page-objects +(defn update-tokens [state resolved-tokens] + (->> (wsh/lookup-page-objects state) (collect-shapes-update-info resolved-tokens) (actionize-shapes-update-info))) @@ -131,5 +131,5 @@ (let [undo-id (js/Symbol)] (rx/concat (rx/of (dwu/start-undo-transaction undo-id)) - (update-tokens sd-tokens) + (update-tokens state sd-tokens) (rx/of (dwu/commit-undo-transaction undo-id)))))))))) diff --git a/frontend/test/frontend_tests/helpers/state.cljs b/frontend/test/frontend_tests/helpers/state.cljs index 068a6cce9..4027ccf29 100644 --- a/frontend/test/frontend_tests/helpers/state.cljs +++ b/frontend/test/frontend_tests/helpers/state.cljs @@ -57,7 +57,7 @@ (fn [cause] (js/console.log "[error]:" cause)) (fn [_] - (js/console.log "[complete]")))) + #_(js/console.debug "[complete]")))) (doseq [event events] (ptk/emit! store event)) diff --git a/frontend/test/frontend_tests/logic/components_and_tokens.cljs b/frontend/test/frontend_tests/logic/components_and_tokens.cljs new file mode 100644 index 000000000..a51985ef8 --- /dev/null +++ b/frontend/test/frontend_tests/logic/components_and_tokens.cljs @@ -0,0 +1,397 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC +(ns frontend-tests.logic.components-and-tokens + (:require + [app.common.geom.point :as geom] + [app.common.math :as mth] + [app.common.test-helpers.components :as cthc] + [app.common.test-helpers.compositions :as ctho] + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.ids-map :as cthi] + [app.common.test-helpers.shapes :as cths] + [app.common.test-helpers.tokens :as ctht] + [app.common.types.tokens-lib :as ctob] + [app.main.data.tokens :as dt] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.ui.workspace.tokens.changes :as wtch] + [app.main.ui.workspace.tokens.update :as wtu] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.pages :as thp] + [frontend-tests.helpers.state :as ths] + [frontend-tests.tokens.helpers.state :as tohs] + [frontend-tests.tokens.helpers.tokens :as toht])) + +(t/use-fixtures :each + {:before thp/reset-idmap!}) + +(defn- setup-base-file + [] + (-> (cthf/sample-file :file1) + (ctht/add-tokens-lib) + (ctht/update-tokens-lib #(-> % + (ctob/add-set (ctob/make-token-set :name "test-token-set")) + (ctob/add-theme (ctob/make-token-theme :name "test-theme" + :sets #{"test-token-set"})) + (ctob/set-active-themes #{"/test-theme"}) + (ctob/add-token-in-set "test-token-set" + (ctob/make-token :name "test-token-1" + :type :border-radius + :value 25)) + (ctob/add-token-in-set "test-token-set" + (ctob/make-token :name "test-token-2" + :type :border-radius + :value 50)) + (ctob/add-token-in-set "test-token-set" + (ctob/make-token :name "test-token-3" + :type :border-radius + :value 75)))) + (ctho/add-frame :frame1) + (ctht/apply-token-to-shape :frame1 "test-token-1" [:rx :ry] [:rx :ry] 25))) + +(defn- setup-file-with-main + [] + (-> (setup-base-file) + (cthc/make-component :component1 :frame1))) + +(defn- setup-file-with-copy + [] + (-> (setup-file-with-main) + (cthc/instantiate-component :component1 :c-frame1))) + +(t/deftest create-component-with-token + (t/async + done + (let [;; ==== Setup + file (setup-base-file) + store (ths/setup-store file) + + ;; ==== Action + events + [(dws/select-shape (cthi/id :frame1)) + (dwl/add-component)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-store new-state) + frame1' (cths/get-shape file' :frame1) + tokens-frame1' (:applied-tokens frame1')] + + ;; ==== Check + (t/is (= (count tokens-frame1') 2)) + (t/is (= (get tokens-frame1' :rx) "test-token-1")) + (t/is (= (get tokens-frame1' :ry) "test-token-1")) + (t/is (= (get frame1' :rx) 25)) + (t/is (= (get frame1' :ry) 25)))))))) + +(t/deftest create-copy-with-token + (t/async + done + (let [;; ==== Setup + file (setup-file-with-main) + store (ths/setup-store file) + + ;; ==== Action + events + [(dwl/instantiate-component (:id file) + (cthi/id :component1) + (geom/point 0 0))]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + selected (wsh/lookup-selected new-state) + c-frame1' (wsh/lookup-shape new-state (first selected)) + tokens-frame1' (:applied-tokens c-frame1')] + + ;; ==== Check + (t/is (= (count tokens-frame1') 2)) + (t/is (= (get tokens-frame1' :rx) "test-token-1")) + (t/is (= (get tokens-frame1' :ry) "test-token-1")) + (t/is (= (get c-frame1' :rx) 25)) + (t/is (= (get c-frame1' :ry) 25)))))))) + +(t/deftest change-token-in-main + (t/async + done + (let [;; ==== Setup + file (setup-file-with-copy) + store (ths/setup-store file) + + ;; ==== Action + events [(wtch/apply-token {:shape-ids [(cthi/id :frame1)] + :attributes #{:rx :ry} + :token (toht/get-token file "test-token-2") + :on-update-shape wtch/update-shape-radius-all})] + + step2 (fn [_] + (let [events2 [(dwl/sync-file (:id file) (:id file))]] + (ths/run-store + store done events2 + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-store new-state) + c-frame1' (cths/get-shape file' :c-frame1) + tokens-frame1' (:applied-tokens c-frame1')] + + ;; ==== Check + (t/is (= (count tokens-frame1') 2)) + (t/is (= (get tokens-frame1' :rx) "test-token-2")) + (t/is (= (get tokens-frame1' :ry) "test-token-2")) + (t/is (= (get c-frame1' :rx) 50)) + (t/is (= (get c-frame1' :ry) 50)))))))] + + (tohs/run-store-async + store step2 events identity)))) + +(t/deftest remove-token-in-main + (t/async + done + (let [;; ==== Setup + file (setup-file-with-copy) + store (ths/setup-store file) + + ;; ==== Action + events [(wtch/unapply-token {:shape-ids [(cthi/id :frame1)] + :attributes #{:rx :ry} + :token (toht/get-token file "test-token-1")})] + + step2 (fn [_] + (let [events2 [(dwl/sync-file (:id file) (:id file))]] + (ths/run-store + store done events2 + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-store new-state) + c-frame1' (cths/get-shape file' :c-frame1) + tokens-frame1' (:applied-tokens c-frame1')] + + ;; ==== Check + (t/is (= (count tokens-frame1') 0)) + (t/is (= (get c-frame1' :rx) 25)) + (t/is (= (get c-frame1' :ry) 25)))))))] + + (tohs/run-store-async + store step2 events identity)))) + +(t/deftest modify-token + (t/async + done + (let [;; ==== Setup + file (setup-file-with-copy) + store (ths/setup-store file) + + ;; ==== Action + events [(dt/update-create-token {:token (ctob/make-token :name "test-token-1" + :type :border-radius + :value 66) + :prev-token-name "test-token-1"})] + + step2 (fn [_] + (let [events2 [(wtu/update-workspace-tokens) + (dwl/sync-file (:id file) (:id file))]] + (tohs/run-store-async + store done events2 + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-store new-state) + c-frame1' (cths/get-shape file' :c-frame1) + tokens-frame1' (:applied-tokens c-frame1')] + + ;; ==== Check + (t/is (= (count tokens-frame1') 2)) + (t/is (= (get tokens-frame1' :rx) "test-token-1")) + (t/is (= (get tokens-frame1' :ry) "test-token-1")) + (t/is (= (get c-frame1' :rx) 66)) + (t/is (= (get c-frame1' :ry) 66)))))))] + + (tohs/run-store-async + store step2 events identity)))) + +(t/deftest change-token-in-copy-then-change-main + (t/async + done + (let [;; ==== Setup + file (setup-file-with-copy) + store (ths/setup-store file) + + ;; ==== Action + events [(wtch/apply-token {:shape-ids [(cthi/id :c-frame1)] + :attributes #{:rx :ry} + :token (toht/get-token file "test-token-2") + :on-update-shape wtch/update-shape-radius-all}) + (wtch/apply-token {:shape-ids [(cthi/id :frame1)] + :attributes #{:rx :ry} + :token (toht/get-token file "test-token-3") + :on-update-shape wtch/update-shape-radius-all})] + + step2 (fn [_] + (let [events2 [(dwl/sync-file (:id file) (:id file))]] + (ths/run-store + store done events2 + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-store new-state) + c-frame1' (cths/get-shape file' :c-frame1) + tokens-frame1' (:applied-tokens c-frame1')] + + ;; ==== Check + (t/is (= (count tokens-frame1') 2)) + (t/is (= (get tokens-frame1' :rx) "test-token-2")) + (t/is (= (get tokens-frame1' :ry) "test-token-2")) + (t/is (= (get c-frame1' :rx) 50)) + (t/is (= (get c-frame1' :ry) 50)))))))] + + (tohs/run-store-async + store step2 events identity)))) + +(t/deftest remove-token-in-copy-then-change-main + (t/async + done + (let [;; ==== Setup + file (setup-file-with-copy) + store (ths/setup-store file) + + ;; ==== Action + events [(wtch/unapply-token {:shape-ids [(cthi/id :c-frame1)] + :attributes #{:rx :ry} + :token (toht/get-token file "test-token-1")}) + (wtch/apply-token {:shape-ids [(cthi/id :frame1)] + :attributes #{:rx :ry} + :token (toht/get-token file "test-token-3") + :on-update-shape wtch/update-shape-radius-all})] + + step2 (fn [_] + (let [events2 [(dwl/sync-file (:id file) (:id file))]] + (ths/run-store + store done events2 + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-store new-state) + c-frame1' (cths/get-shape file' :c-frame1) + tokens-frame1' (:applied-tokens c-frame1')] + + ;; ==== Check + (t/is (= (count tokens-frame1') 0)) + (t/is (= (get c-frame1' :rx) 25)) + (t/is (= (get c-frame1' :ry) 25)))))))] + + (tohs/run-store-async + store step2 events identity)))) + +(t/deftest modify-token-all-types + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (ctht/add-tokens-lib) + (ctht/update-tokens-lib #(-> % + (ctob/add-set (ctob/make-token-set :name "test-token-set")) + (ctob/add-theme (ctob/make-token-theme :name "test-theme" + :sets #{"test-token-set"})) + (ctob/set-active-themes #{"/test-theme"}) + (ctob/add-token-in-set "token-radius" + (ctob/make-token :name "token-radius" + :type :border-radius + :value 10)) + (ctob/add-token-in-set "token-rotation" + (ctob/make-token :name "token-rotation" + :type :rotation + :value 30)) + (ctob/add-token-in-set "token-opacity" + (ctob/make-token :name "token-opacity" + :type :opacity + :value 0.7)) + (ctob/add-token-in-set "token-stroke-width" + (ctob/make-token :name "token-stroke-width" + :type :stroke-width + :value 2)) + (ctob/add-token-in-set "token-color" + (ctob/make-token :name "token-color" + :type :color + :value "#00ff00")) + (ctob/add-token-in-set "token-dimensions" + (ctob/make-token :name "token-dimensions" + :type :dimensions + :value 100)))) + (ctho/add-frame :frame1) + (ctht/apply-token-to-shape :frame1 "token-radius" [:rx :ry] [:rx :ry] 10) + (ctht/apply-token-to-shape :frame1 "token-rotation" [:rotation] [:rotation] 30) + (ctht/apply-token-to-shape :frame1 "token-opacity" [:opacity] [:opacity] 0.7) + (ctht/apply-token-to-shape :frame1 "token-stroke-width" [:stroke-width] [:stroke-width] 2) + (ctht/apply-token-to-shape :frame1 "token-color" [:stroke-color] [:stroke-color] "#00ff00") + (ctht/apply-token-to-shape :frame1 "token-color" [:fill] [:fill] "#00ff00") + (ctht/apply-token-to-shape :frame1 "token-dimensions" [:width :height] [:width :height] 100) + (cthc/make-component :component1 :frame1) + (cthc/instantiate-component :component1 :c-frame1)) + store (ths/setup-store file) + + ;; ==== Action + events [(dt/update-create-token {:token (ctob/make-token :name "token-radius" + :type :border-radius + :value 30) + :prev-token-name "token-radius"}) + (dt/update-create-token {:token (ctob/make-token :name "token-rotation" + :type :rotation + :value 45) + :prev-token-name "token-rotation"}) + (dt/update-create-token {:token (ctob/make-token :name "token-opacity" + :type :opacity + :value 0.9) + :prev-token-name "token-opacity"}) + (dt/update-create-token {:token (ctob/make-token :name "token-stroke-width" + :type :stroke-width + :value 8) + :prev-token-name "token-stroke-width"}) + (dt/update-create-token {:token (ctob/make-token :name "token-color" + :type :color + :value "#ff0000") + :prev-token-name "token-color"}) + (dt/update-create-token {:token (ctob/make-token :name "token-dimensions" + :type :dimensions + :value 200) + :prev-token-name "token-dimensions"})] + + step2 (fn [_] + (let [events2 [(wtu/update-workspace-tokens) + (dwl/sync-file (:id file) (:id file))]] + (tohs/run-store-async + store done events2 + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-store new-state) + c-frame1' (cths/get-shape file' :c-frame1) + tokens-frame1' (:applied-tokens c-frame1')] + + ;; ==== Check + (t/is (= (count tokens-frame1') 9)) + (t/is (= (get tokens-frame1' :rx) "token-radius")) + (t/is (= (get tokens-frame1' :ry) "token-radius")) + (t/is (= (get tokens-frame1' :rotation) "token-rotation")) + (t/is (= (get tokens-frame1' :opacity) "token-opacity")) + (t/is (= (get tokens-frame1' :stroke-width) "token-stroke-width")) + (t/is (= (get tokens-frame1' :stroke-color) "token-color")) + (t/is (= (get tokens-frame1' :fill) "token-color")) + (t/is (= (get tokens-frame1' :width) "token-dimensions")) + (t/is (= (get tokens-frame1' :height) "token-dimensions")) + (t/is (= (get c-frame1' :rx) 30)) + (t/is (= (get c-frame1' :ry) 30)) + (t/is (= (get c-frame1' :rotation) 45)) + (t/is (= (get c-frame1' :opacity) 0.9)) + (t/is (= (get-in c-frame1' [:strokes 0 :stroke-width]) 8)) + (t/is (= (get-in c-frame1' [:strokes 0 :stroke-color]) "#ff0000")) + (t/is (= (get-in c-frame1' [:fills 0 :fill-color]) "#ff0000")) + (t/is (mth/close? (get c-frame1' :width) 200)) + (t/is (mth/close? (get c-frame1' :height) 200)) + + (t/is (empty? (:touched c-frame1'))))))))] + + (tohs/run-store-async + store step2 events identity)))) \ No newline at end of file diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index a42eb7203..bca0112e1 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -4,6 +4,7 @@ [frontend-tests.basic-shapes-test] [frontend-tests.helpers-shapes-test] [frontend-tests.logic.comp-remove-swap-slots-test] + [frontend-tests.logic.components-and-tokens] [frontend-tests.logic.copying-and-duplicating-test] [frontend-tests.logic.frame-guides-test] [frontend-tests.logic.groups-test] @@ -28,6 +29,7 @@ (t/run-tests 'frontend-tests.helpers-shapes-test 'frontend-tests.logic.comp-remove-swap-slots-test + 'frontend-tests.logic.components-and-tokens 'frontend-tests.logic.copying-and-duplicating-test 'frontend-tests.logic.frame-guides-test 'frontend-tests.logic.groups-test