🔧 Revert migration for tokens with clashing names (#9950)

* Revert "🐛 Detect duplicated token names in the whole library (#9034)"

This reverts commit 61cd7573553b1c5e9fc2d7300cf9b2c36b4dcbb6.

* 🔧 Preserve some enhancements and fixes that are still valid

* 🔧 Fix broken integration tests
This commit is contained in:
Andrés Moya 2026-06-02 09:09:58 +02:00 committed by GitHub
parent 53a4d2a18a
commit 06c9a18ab0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 196 additions and 670 deletions

View File

@ -1805,13 +1805,6 @@
{})] {})]
(cfcp/sync-component-id-with-ref-shape data libraries))) (cfcp/sync-component-id-with-ref-shape data libraries)))
(defmethod migrate-data "0021-repair-bad-tokens"
[data _]
(d/update-when data :tokens-lib
#(-> %
(ctob/fix-conflicting-token-names)
(ctob/fix-missing-sets-in-themes))))
(defmethod migrate-data "0021-fix-shape-svg-attrs" (defmethod migrate-data "0021-fix-shape-svg-attrs"
[data _] [data _]
(some-> cfeat/*new* (swap! conj "fdata/shape-data-type")) (some-> cfeat/*new* (swap! conj "fdata/shape-data-type"))
@ -1840,6 +1833,10 @@
(cfcp/fix-missing-swap-slots libraries) (cfcp/fix-missing-swap-slots libraries)
(cfcp/sync-component-id-with-ref-shape libraries)))) (cfcp/sync-component-id-with-ref-shape libraries))))
(defmethod migrate-data "0023-repair-token-themes-with-inexistent-sets"
[data _]
(d/update-when data :tokens-lib ctob/fix-missing-sets-in-themes))
(def available-migrations (def available-migrations
(into (d/ordered-set) (into (d/ordered-set)
["legacy-2" ["legacy-2"
@ -1918,6 +1915,6 @@
"0018-remove-unneeded-objects-from-components" "0018-remove-unneeded-objects-from-components"
"0019-fix-missing-swap-slots" "0019-fix-missing-swap-slots"
"0020-sync-component-id-with-near-main" "0020-sync-component-id-with-near-main"
"0021-repair-bad-tokens"
"0021-fix-shape-svg-attrs" "0021-fix-shape-svg-attrs"
"0022-normalize-component-root-and-resync"])) "0022-normalize-component-root-and-resync"
"0023-repair-token-themes-with-inexistent-sets"]))

View File

@ -134,26 +134,24 @@
(defn make-token-name-schema (defn make-token-name-schema
"Dynamically generates a schema to check a token name, adding translated error messages "Dynamically generates a schema to check a token name, adding translated error messages
and additional validations: and two additional validations:
- Min and max length. - Min and max length.
- Checks if other token with a path derived from the name already exists in the library. - Checks if other token with a path derived from the name already exists at `tokens-tree`.
e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists. e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists."
- Also checks if there is a token with the exact same name in the current set, but different [tokens-tree]
from the current token."
[tokens-lib set-id token-id]
[:and [:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(-> cto/schema:token-name (-> cto/schema:token-name
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))) (sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(or (nil? tokens-lib) #(and (some? tokens-tree)
(not (ctob/token-name-path-exists? % tokens-lib set-id token-id)))]]) (not (ctob/token-name-path-exists? % tokens-tree)))]])
(defn make-node-token-name-schema (defn make-node-token-name-schema
"Dynamically generates a schema to check the name of a token node, that may be a final token or a group. "Dynamically generates a schema to check the name of a token node, that may be a final token or a group.
This runs same checks as make-token-name-schema, but for all tokens that will be renamed by this change, This runs same checks as make-token-name-schema, but for all tokens that will be renamed by this change,
if the group already contains tokens." if the group already contains tokens."
[active-tokens tokens-lib node set-id] [active-tokens tokens-tree node]
[:and [:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(-> cto/schema:token-node-name (-> cto/schema:token-node-name
@ -164,20 +162,20 @@
current-name (:name node) current-name (:name node)
new-tokens (ctob/update-tokens-group active-tokens current-path current-name name)] new-tokens (ctob/update-tokens-group active-tokens current-path current-name name)]
(and (some? new-tokens) (and (some? new-tokens)
(some (fn [[token-name token]] (some (fn [[token-name _]]
(not (ctob/token-name-path-exists? token-name tokens-lib set-id (ctob/get-id token)))) (not (ctob/token-name-path-exists? token-name tokens-tree)))
new-tokens))))]]) new-tokens))))]])
(def schema:token-description (def schema:token-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]) [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-schema (defn make-token-schema
[tokens-lib token-type set-id token-id] [tokens-tree token-type]
[:and [:and
(sm/merge (sm/merge
cto/schema:token-attrs cto/schema:token-attrs
[:map [:map
[:name (make-token-name-schema tokens-lib set-id token-id)] [:name (make-token-name-schema tokens-tree)]
[:value (make-token-value-schema token-type)] [:value (make-token-value-schema token-type)]
[:description {:optional true} schema:token-description]]) [:description {:optional true} schema:token-description]])
[:fn {:error/field :value [:fn {:error/field :value
@ -187,9 +185,9 @@
(not (cto/token-value-self-reference? name value))))]]) (not (cto/token-value-self-reference? name value))))]])
(defn make-node-token-schema (defn make-node-token-schema
[active-tokens tokens-lib node set-id] [active-tokens tokens-tree node]
[:map [:map
[:name (make-node-token-name-schema active-tokens tokens-lib node set-id)]]) [:name (make-node-token-name-schema active-tokens tokens-tree node)]])
(defn convert-dtcg-token (defn convert-dtcg-token
"Convert token attributes as they come from a decoded json, with DTCG types, to internal types. "Convert token attributes as they come from a decoded json, with DTCG types, to internal types.

View File

@ -1484,63 +1484,49 @@ Will return a value that matches this schema:
(rename copy-name) (rename copy-name)
(reid (uuid/next)))))) (reid (uuid/next))))))
(defn- token-name->path-selector
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
`:selector` is the last item of the names path
`:path` is everything leading up the the `:selector`."
[token-name]
(let [path-segments (get-token-path {:name token-name})
last-idx (dec (count path-segments))
[path [selector]] (split-at last-idx path-segments)]
{:path (seq path)
:selector selector}))
(defn token-name-path-exists? (defn token-name-path-exists?
"Check if a token name or fragment exists in any part of the library, to prevent creating "Traverses the path from `token-name` down a `tokens-tree` and checks if a token at that path exists.
duplicated names that may clash when merging sets and resolving tokens.
Matches any combination of of names completely included inside group or token names. It's not allowed to create a token inside a token. E.g.:
For example: Creating a token with
- Matches the name \"foo.bar\" with an existing token named \"foo.bar.baz\" or \"foo\".
- Does not match the name \"foo.bar\" with an existing token named \"foo.baz\".
You can give a current set id, and it will check if there is a token with the exact same {:name \"foo.bar\"}
name in this set (there may be tokens with same name in different sets for overriding
values, but not in the same set). You can also give a token id to ignore, to search for
a token that is a different one.
If the function finds a match, it returns the part of the name that is duplicated; in the tokens tree:
if not, it returns null."
[token-name tokens-lib current-set-id token-id-to-ignore]
(letfn [(exists-in-set?
[set]
(let [tokens-tree (-> set (get-tokens-) (tokens-tree))
token-name-path (get-token-path {:name token-name})]
(loop [path-segment token-name-path
subtree tokens-tree]
(if (empty? path-segment)
;; All path segments found -> return full name
token-name
(let [node (get subtree (first path-segment))]
(cond
;; Path segment doesn't exist
(nil? node) nil
;; A token exists at this path
(token? node)
(if (and (some? token-id-to-ignore)
(= (get-id node) token-id-to-ignore))
;; This is the token to ignore
nil
(if (and (not= (get-id set) current-set-id)
(= (get-name node) token-name))
;; A token with the same name in a different set is allowed
nil
;; If we are in the same set or the name of the token is a subpath of the
;; current name: this is a conflict
;; -> return the part of the name until this point
(str/join "." (take (- (count token-name-path) (count (rest path-segment)))
token-name-path))))
;; Continue traversing the tree
:else (recur (rest path-segment) node)))))))]
(if (or (nil? tokens-lib) (empty? (get-sets tokens-lib)) {\"foo\" {:name \"other\"}}"
(nil? token-name) (str/empty? token-name)) [token-name tokens-tree]
nil (let [{:keys [path selector]} (token-name->path-selector token-name)
(do path-target (reduce
(assert (or (nil? current-set-id) (fn [acc cur]
(some? (get-set tokens-lib current-set-id))) (let [target (get acc cur)]
(str "Set '" current-set-id "' does not exist in the library")) (cond
(assert (or (nil? token-id-to-ignore) (uuid? token-id-to-ignore))) ;; Path segment doesn't exist yet
(some exists-in-set? (get-sets tokens-lib)))))) (nil? target) (reduced false)
;; A token exists at this path
(:name target) (reduced true)
;; Continue traversing the true
:else target)))
tokens-tree
path)]
(cond
(boolean? path-target) path-target
(get path-target :name) true
:else (-> (get path-target selector)
(seq)
(boolean)))))
(defn update-tokens-group (defn update-tokens-group
"Updates the active tokens path when renaming a group node. "Updates the active tokens path when renaming a group node.
@ -1554,7 +1540,6 @@ Will return a value that matches this schema:
new-name: the new name for the group being renamed, e.g. \"baz\" new-name: the new name for the group being renamed, e.g. \"baz\"
Returns a sequence of [name token] for each renamed token." Returns a sequence of [name token] for each renamed token."
[active-tokens current-path current-name new-name] [active-tokens current-path current-name new-name]
(let [path-prefix (str/replace current-path current-name "")] (let [path-prefix (str/replace current-path current-name "")]
(mapv (fn [[token-path token-obj]] (mapv (fn [[token-path token-obj]]
@ -1927,11 +1912,7 @@ Will return a value that matches this schema:
library library
(reduce (fn [library name] (reduce (fn [library name]
(if-let [tokens (get sets name)] (if-let [tokens (get sets name)]
(do (doseq [token (vals tokens)] (add-set library (make-token-set :name name :tokens tokens))
(when (token-name-path-exists? (get-name token) library nil (get-id token))
(throw (ex-info (get-name token)
{:error/code :error.import/duplicated-token-name}))))
(add-set library (make-token-set :name name :tokens tokens)))
library)) library))
library library
ordered-set-names) ordered-set-names)
@ -2248,29 +2229,6 @@ Will return a value that matches this schema:
(map->tokens-lib) (map->tokens-lib)
(check))))) (check)))))
(defn fix-conflicting-token-names
[tokens-lib]
(let [counter (atom 0)
match-suffixes (atom {})
generate-name
(fn [name match]
(let [matches (if (contains? @match-suffixes match)
@match-suffixes
(swap! match-suffixes assoc match (swap! counter inc)))
suffix (get matches match)]
(str (str/slice name 0 (count match))
"-" suffix
(str/slice name (count match)))))]
(update-all-tokens
tokens-lib
(fn [lib set token]
(let [name (get-name token)]
(if-let [match (token-name-path-exists? name lib (:id set) (get-id token))]
(rename token (generate-name name match))
token))))))
(defn fix-missing-sets-in-themes (defn fix-missing-sets-in-themes
[tokens-lib] [tokens-lib]
(let [existing-set-names (into #{} (map get-name) (get-sets tokens-lib)) (let [existing-set-names (into #{} (map get-name) (get-sets tokens-lib))
@ -2321,7 +2279,7 @@ Will return a value that matches this schema:
#?(:clj #?(:clj
(defn- migrate-to-v1-3 (defn- migrate-to-v1-3
"Migrate the TokensLib data structure internals to v1.3 version; it "Migrate the TokensLib data structure internals to v1.3 version; it
expects input from v1.2 version" expects input from v1.2 version"
[{:keys [sets themes] :as params}] [{:keys [sets themes] :as params}]
(let [migrate-token (let [migrate-token
(fn [token] (fn [token]
@ -2369,7 +2327,7 @@ Will return a value that matches this schema:
#?(:clj #?(:clj
(defn- migrate-to-v1-4 (defn- migrate-to-v1-4
"Migrate the TokensLib data structure internals to v1.4 version; it "Migrate the TokensLib data structure internals to v1.4 version; it
expects input from v1.3 version" expects input from v1.3 version"
[params] [params]
(let [migrate-set-node (let [migrate-set-node
(fn recurse [node] (fn recurse [node]

View File

@ -2071,235 +2071,12 @@
(t/is (= (:value imported-ref) (:value original-ref)))))))) (t/is (= (:value imported-ref) (:value original-ref))))))))
(t/deftest token-name-path-exists?-test (t/deftest token-name-path-exists?-test
(let [tokens-lib (ctob/make-tokens-lib) (t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
add-set (fn [lib set-label set-name token-names] (t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
(ctob/add-set lib (ctob/make-token-set (t/is (true? (ctob/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
:id (thi/new-id! set-label) (t/is (true? (ctob/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
:name set-name (t/is (false? (ctob/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
:tokens (into {} (t/is (false? (ctob/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))
(map (fn [token-name]
[token-name (ctob/make-token
{:name token-name
:type :border-radius
:value "1"})]))
token-names))))]
;; Empty cases
(t/testing "returns match for no library or empty library or empty name"
(t/is (not (ctob/token-name-path-exists? nil nil nil nil)))
(t/is (not (ctob/token-name-path-exists? nil tokens-lib nil nil)))
(t/is (not (ctob/token-name-path-exists? "" tokens-lib nil nil)))
(t/is (not (ctob/token-name-path-exists? "bad-name" tokens-lib nil nil)))
(t/is (not (ctob/token-name-path-exists? "bad-name"
(ctob/add-theme tokens-lib
(ctob/make-token-theme {:name "theme1"}))
nil
nil))))
(t/testing "throws error when giving a bad set id"
(t/is (thrown-with-msg? #?(:clj AssertionError :cljs js/Error)
#"Set '[0-9a-f-]+' does not exist in the library"
(ctob/token-name-path-exists? "some-name"
(-> tokens-lib
(add-set :empty-set "empty-set" []))
(thi/new-id! :non-existent-set) nil))))
(t/testing "does not throw error when giving a nil set id"
(t/is (not (ctob/token-name-path-exists? "some-name"
(-> tokens-lib
(add-set :empty-set "empty-set" []))
nil nil))))
(t/testing "returns not match for empty set"
(t/is (not (ctob/token-name-path-exists? "some-name"
(-> tokens-lib
(add-set :empty-set "empty-set" []))
(thi/id :empty-set) nil))))
;; Search in the current set
(t/testing "returns match when name matches exactly a token in the set without groups"
(t/is (= "token1"
(ctob/token-name-path-exists? "token1"
(-> tokens-lib
(add-set :set1 "set1" ["token1" "token2" "token3"]))
(thi/id :set1) nil))))
(t/testing "returns match when name matches exactly a token in the set with groups"
(t/is (= "group1.subgroup1.token2"
(ctob/token-name-path-exists? "group1.subgroup1.token2"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"]))
(thi/id :set1) nil))))
(t/testing "returns match when name is a subpath of a token in the set"
(t/is (= "group1"
(ctob/token-name-path-exists? "group1"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"]))
(thi/id :set1) nil)))
(t/is (= "group1.subgroup1"
(ctob/token-name-path-exists? "group1.subgroup1"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"]))
(thi/id :set1) nil))))
(t/testing "returns match when one of the token names in the set is a subpath of the name"
(t/is (= "group2.subgroup2.token3"
(ctob/token-name-path-exists? "group2.subgroup2.token3.subtoken"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"]))
(thi/id :set1) nil))))
(t/testing "returns not match when name matches part of the path but not the full token name"
(t/is (not (ctob/token-name-path-exists? "group1.subgroup1.token4"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"]))
(thi/id :set1) nil))))
(t/testing "returns not match when name does not match any part of the token names in the set"
(t/is (not (ctob/token-name-path-exists? "token4"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"]))
(thi/id :set1) nil))))
;; Search in other set
(t/testing "returns not match when name matches exactly a token in other set without groups"
(t/is (not (ctob/token-name-path-exists? "token1"
(-> tokens-lib
(add-set :set1 "set1" ["token1" "token2" "token3"])
(add-set :set2 "set2" []))
(thi/id :set2) nil))))
(t/testing "returns not match when name matches exactly a token in other set with groups"
(t/is (not (ctob/token-name-path-exists? "group1.subgroup1.token2"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" []))
(thi/id :set2) nil))))
(t/testing "returns match when name is a subpath of a token in other set"
(t/is (= "group1"
(ctob/token-name-path-exists? "group1"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" []))
(thi/id :set2) nil)))
(t/is (= "group1.subgroup1"
(ctob/token-name-path-exists? "group1.subgroup1"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" []))
(thi/id :set2) nil))))
(t/testing "returns match when one of the token names in other set is a subpath of the name"
(t/is (= "group2.subgroup2.token3"
(ctob/token-name-path-exists? "group2.subgroup2.token3.subtoken"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" []))
(thi/id :set2) nil))))
(t/testing "returns not match when name matches part of the path but not the full token name"
(t/is (not (ctob/token-name-path-exists? "group1.subgroup1.token4"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" []))
(thi/id :set2) nil))))
(t/testing "returns not match when name does not match any part of the token names in the set"
(t/is (not (ctob/token-name-path-exists? "token4"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" []))
(thi/id :set2) nil))))
;; Additional cases
(t/testing "returns match when matches an exact token with several sets"
(t/is (= "group3.subgroup3.token4"
(ctob/token-name-path-exists? "group3.subgroup3.token4"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" ["group3.subgroup3.token4"]))
(thi/id :set2) nil))))
(t/testing "returns match when matches in one of the sets, even if the set is disabled"
(let [tokens-lib (-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" ["group3.subgroup3.token4"]))
hidden-theme (ctob/get-hidden-theme tokens-lib)
tokens-lib (ctob/toggle-set-in-theme tokens-lib (:id hidden-theme) "set2")]
(t/is (= "group3.subgroup3.token4"
(ctob/token-name-path-exists? "group3.subgroup3.token4"
tokens-lib
(thi/id :set2)
nil)))))
(t/testing "returns not match when does not match in any of the sets"
(t/is (not (ctob/token-name-path-exists? "group3.subgroup3.token5"
(-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" ["group3.subgroup3.token4"]))
(thi/id :set1)
nil))))
(t/testing "returns not match when the token exists but is the one we have told it to ignore"
(let [tokens-lib (-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" ["group3.subgroup3.token4"]))
token4 (ctob/get-token-by-name tokens-lib "set2" "group3.subgroup3.token4")]
(t/is (not (ctob/token-name-path-exists? "group3.subgroup3.token4"
tokens-lib
(thi/id :set2)
(:id token4))))))
(t/testing "returns match when we give an id to ignore but is not the token that matches"
(let [tokens-lib (-> tokens-lib
(add-set :set1 "set1" ["group1.subgroup1.token1"
"group1.subgroup1.token2"
"group2.subgroup2.token3"])
(add-set :set2 "set2" ["group3.subgroup3.token4"]))
token4 (ctob/get-token-by-name tokens-lib "set2" "group3.subgroup3.token4")]
(t/is (= "group1.subgroup1.token1"
(ctob/token-name-path-exists? "group1.subgroup1.token1"
tokens-lib
(thi/id :set1)
(:id token4))))))))
#?(:clj #?(:clj
(t/deftest token-set-encode-decode-roundtrip-with-invalid-set-name (t/deftest token-set-encode-decode-roundtrip-with-invalid-set-name

View File

@ -13,205 +13,7 @@
[clojure.datafy :refer [datafy]] [clojure.datafy :refer [datafy]]
[clojure.test :as t])) [clojure.test :as t]))
(t/deftest test-v1-5-fix-token-names (t/deftest test-fix-missing-sets-in-themes
(t/testing "empty tokens-lib should not need any action"
(let [tokens-lib (ctob/make-tokens-lib)
tokens-lib' (ctob/fix-conflicting-token-names tokens-lib)]
(t/is (empty? (d/map-diff (datafy tokens-lib) (datafy tokens-lib'))))))
(t/testing "tokens with valid names should not need any action"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set1)
:name "set1"
:tokens {"name1" (ctob/make-token
{:id (thi/new-id! :token1)
:name "name1"
:type :border-radius
:value "1"})}))
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set2)
:name "set2"
:tokens {"name1" (ctob/make-token ;; Same name in different
{:id (thi/new-id! :token2) ;; sets is ok
:name "name1"
:type :border-radius
:value "2"})})))
tokens-lib' (ctob/fix-conflicting-token-names tokens-lib)]
(t/is (empty? (d/map-diff (datafy tokens-lib) (datafy tokens-lib'))))))
(t/testing "tokens with conflicting names should be renamed, and the rest of the library should be unchanged"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set1)
:name "set1"
:tokens {"name1" (ctob/make-token
{:id (thi/new-id! :token1)
:name "name1"
:type :border-radius
:value "1"})}))
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set2)
:name "set2"
:tokens {"name1.name2" (ctob/make-token
{:id (thi/new-id! :token2)
:name "name1.name2"
:type :border-radius
:value "2"})})))
tokens-lib' (ctob/fix-conflicting-token-names tokens-lib)
token-sets (ctob/get-set-tree tokens-lib)
set1 (ctob/get-set tokens-lib (thi/id :set1))
set2 (ctob/get-set tokens-lib (thi/id :set2))
tokens1 (ctob/get-tokens tokens-lib (thi/id :set1))
tokens2 (ctob/get-tokens tokens-lib (thi/id :set2))
token1 (ctob/get-token tokens-lib (thi/id :set1) (thi/id :token1))
token2 (ctob/get-token tokens-lib (thi/id :set2) (thi/id :token2))
token-sets' (ctob/get-set-tree tokens-lib')
set1' (ctob/get-set tokens-lib' (thi/id :set1))
set2' (ctob/get-set tokens-lib' (thi/id :set2))
tokens1' (ctob/get-tokens tokens-lib' (thi/id :set1))
tokens2' (ctob/get-tokens tokens-lib' (thi/id :set2))
token1' (ctob/get-token tokens-lib' (thi/id :set1) (thi/id :token1))
token2' (ctob/get-token tokens-lib' (thi/id :set2) (thi/id :token2))]
(t/is (= (count token-sets') (count token-sets)))
(t/is (= (ctob/get-id set1') (ctob/get-id set1)))
(t/is (= (ctob/get-name set1') (ctob/get-name set1)))
(t/is (= (ctob/get-description set1') (ctob/get-description set1)))
(t/is (ct/is-after-or-equal? (ctob/get-modified-at set1') (ctob/get-modified-at set1))) ;; <-- MODIFIED
(t/is (= (ctob/get-id set2') (ctob/get-id set2)))
(t/is (= (ctob/get-name set2') (ctob/get-name set2)))
(t/is (= (ctob/get-description set2') (ctob/get-description set2)))
(t/is (= (ctob/get-modified-at set2') (ctob/get-modified-at set2)))
(t/is (= (count tokens1') (count tokens1)))
(t/is (= (count tokens2') (count tokens2)))
(t/is (= (ctob/get-id token1') (ctob/get-id token1)))
(t/is (= (ctob/get-name token1') "name1-1")) ;; <-- RENAMED
(t/is (= (ctob/get-description token1') (ctob/get-description token1)))
(t/is (ct/is-after-or-equal? (ctob/get-modified-at set1') (ctob/get-modified-at set1))) ;; <-- MODIFIED
(t/is (= (:type token1') (:type token1)))
(t/is (= (:value token1') (:value token1)))
(t/is (= (ctob/get-id token2') (ctob/get-id token2)))
(t/is (= (ctob/get-name token2') (ctob/get-name token2)))
(t/is (= (ctob/get-description token2') (ctob/get-description token2)))
(t/is (= (ctob/get-modified-at token2') (ctob/get-modified-at token2)))
(t/is (= (:type token2') (:type token2)))
(t/is (= (:value token2') (:value token2)))))
(t/testing "the renamed token is always the first one found with a conflicting name"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set1)
:name "set1"
:tokens {"name1.name2" (ctob/make-token
{:id (thi/new-id! :token1)
:name "name1.name2"
:type :border-radius
:value "1"})}))
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set2)
:name "set2"
:tokens {"name1" (ctob/make-token
{:id (thi/new-id! :token2)
:name "name1"
:type :border-radius
:value "2"})})))
tokens-lib' (ctob/fix-conflicting-token-names tokens-lib)
token1' (ctob/get-token tokens-lib' (thi/id :set1) (thi/id :token1))
token2' (ctob/get-token tokens-lib' (thi/id :set2) (thi/id :token2))]
(t/is (= "name1-1.name2" (ctob/get-name token1')))
(t/is (= "name1" (ctob/get-name token2')))))
(t/testing "several tokens with the same conflicting prefix should be assigned the same number as suffixes"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set1)
:name "set1"
:tokens {"name1.name2" (ctob/make-token
{:id (thi/new-id! :token1)
:name "name1.name2"
:type :border-radius
:value "1"})
"name1.name3" (ctob/make-token
{:id (thi/new-id! :token2)
:name "name1.name3"
:type :border-radius
:value "2"})}))
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set2)
:name "set2"
:tokens {"name1" (ctob/make-token
{:id (thi/new-id! :token3)
:name "name1"
:type :border-radius
:value "3"})})))
tokens-lib' (ctob/fix-conflicting-token-names tokens-lib)
token1' (ctob/get-token tokens-lib' (thi/id :set1) (thi/id :token1))
token2' (ctob/get-token tokens-lib' (thi/id :set1) (thi/id :token2))
token3' (ctob/get-token tokens-lib' (thi/id :set2) (thi/id :token3))]
(t/is (= "name1-1.name2" (ctob/get-name token1')))
(t/is (= "name1-1.name3" (ctob/get-name token2')))
(t/is (= "name1" (ctob/get-name token3')))))
(t/testing "tokens with diferent conflicting prefixes should be assigned consecutive numbers as suffixes"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set1)
:name "set1"
:tokens {"name1" (ctob/make-token
{:id (thi/new-id! :token1)
:name "name1"
:type :border-radius
:value "1"})
"name2" (ctob/make-token
{:id (thi/new-id! :token2)
:name "name2"
:type :border-radius
:value "2"})}))
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set2)
:name "set2"
:tokens {"name1.subname1" (ctob/make-token
{:id (thi/new-id! :token3)
:name "name1.subname1"
:type :border-radius
:value "3"})}))
(ctob/add-set (ctob/make-token-set
:id (thi/new-id! :set3)
:name "set3"
:tokens {"name2.subname2" (ctob/make-token
{:id (thi/new-id! :token4)
:name "name2.subname2"
:type :border-radius
:value "3"})})))
tokens-lib' (ctob/fix-conflicting-token-names tokens-lib)
token1' (ctob/get-token tokens-lib' (thi/id :set1) (thi/id :token1))
token2' (ctob/get-token tokens-lib' (thi/id :set1) (thi/id :token2))
token3' (ctob/get-token tokens-lib' (thi/id :set2) (thi/id :token3))
token4' (ctob/get-token tokens-lib' (thi/id :set3) (thi/id :token4))]
(t/is (= "name1-1" (ctob/get-name token1')))
(t/is (= "name2-2" (ctob/get-name token2')))
(t/is (= "name1.subname1" (ctob/get-name token3')))
(t/is (= "name2.subname2" (ctob/get-name token4'))))))
(t/deftest test-v1-6-fix-token-names
(t/testing "empty tokens-lib should not need any action" (t/testing "empty tokens-lib should not need any action"
(let [tokens-lib (ctob/make-tokens-lib) (let [tokens-lib (ctob/make-tokens-lib)

View File

@ -1721,72 +1721,29 @@ test.describe("Tokens - creation", () => {
// Submit button should remain disabled when value is empty // Submit button should remain disabled when value is empty
await expect(submitButton).toBeDisabled(); await expect(submitButton).toBeDisabled();
}); });
});
test("User cannot create token with a conflicting name in other set", async ({ test("User duplicate color token", async ({ page }) => {
page, const { tokensSidebar, tokenContextMenuForToken } =
}) => { await setupTokensFileRender(page);
const {
tokensUpdateCreateModal,
tokenThemesSetsSidebar,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFileRender(page);
await expect(tokensSidebar).toBeVisible(); await expect(tokensSidebar).toBeVisible();
await tokenThemesSetsSidebar await unfoldTokenType(tokensSidebar, "color");
.getByRole("button", { name: "light", exact: true })
.click();
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); const colorToken = tokensSidebar.getByRole("button", {
await tokensTabPanel name: "colors.blue.100",
.getByRole("button", { name: "Add Token: Color" }) });
.click();
await expect(tokensUpdateCreateModal).toBeVisible(); await colorToken.click({ button: "right" });
await expect(tokenContextMenuForToken).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name"); await tokenContextMenuForToken.getByText("Duplicate token").click();
const valueField = tokensUpdateCreateModal.getByLabel("Value"); await expect(tokenContextMenuForToken).not.toBeVisible();
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save", await expect(
tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }),
).toBeVisible();
}); });
// Initially submit button should be disabled
await expect(submitButton).toBeDisabled();
await nameField.click();
// Fill in the name of an existing token in the current set
await nameField.fill("accent.default");
// An error message should appear and submit button should be disabled
await expect(
tokensUpdateCreateModal.getByText(
"A token already exists at the path: accent.default",
),
).toBeVisible();
await expect(submitButton).toBeDisabled();
// Fill in a name that clashes with tokens like colors.red.600 in set core
await nameField.fill("colors.red");
// An error message should appear and submit button should be disabled
await expect(
tokensUpdateCreateModal.getByText(
"A token already exists at the path: colors.red",
),
).toBeVisible();
await expect(submitButton).toBeDisabled();
// Fill in a name that matches exactly a token in another set
await nameField.fill("colors.red.600");
await valueField.fill("#6000000");
// Submit button should be enabled now
await expect(submitButton).toBeEnabled();
}); });
test("User creates grouped color token", async ({ page }) => { test("User creates grouped color token", async ({ page }) => {

View File

@ -276,3 +276,64 @@ test.describe("Tokens - node tree", () => {
await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false"); await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false");
}); });
}); });
test("User can see an error on token pill and token modal form when token has an error", async ({
page,
}) => {
const {
tokensSidebar,
tokensUpdateCreateModal,
tokenContextMenuForToken,
tokenThemesSetsSidebar,
} = await setupTokensFileRender(page);
await createSet(tokenThemesSetsSidebar, "set/first");
await tokenThemesSetsSidebar.getByRole("button", { name: "first" }).click();
await tokenThemesSetsSidebar
.getByRole("button", { name: "first" })
.getByRole("checkbox")
.click();
await createSet(tokenThemesSetsSidebar, "set/second");
await tokenThemesSetsSidebar.getByRole("button", { name: "second" }).click();
await tokenThemesSetsSidebar
.getByRole("button", { name: "second" })
.getByRole("checkbox")
.click();
await createToken(page, "Border radius", "a.b", "Value", "textbox", "23");
await tokenThemesSetsSidebar.getByRole("button", { name: "first" }).click();
await createToken(page, "Border radius", "a", "Value", "textbox", "25");
await tokenThemesSetsSidebar.getByRole("button", { name: "second" }).click();
const brokenTokenPill = tokensSidebar.getByRole("button", {
name: "Group name of a.b conflicts",
});
await expect(brokenTokenPill).toBeVisible();
await brokenTokenPill.click({ button: "right" });
const editTokenButton = page
.getByRole("listitem")
.filter({ hasText: "Edit token" });
await expect(editTokenButton).toBeVisible();
await editTokenButton.click();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await expect(nameField).toBeVisible();
await expect(nameField).toHaveValue("a.b");
const errorMessage = tokensUpdateCreateModal.getByText(
"Group name of a.b conflicts",
);
await expect(errorMessage).toBeVisible();
await nameField.fill("new-name");
await expect(errorMessage).not.toBeVisible();
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeEnabled();
});

View File

@ -27,11 +27,6 @@
:error/fn #(tr "errors.tokens.invalid-json-token-name") :error/fn #(tr "errors.tokens.invalid-json-token-name")
:error/detail #(tr "errors.tokens.invalid-json-token-name-detail" %)} :error/detail #(tr "errors.tokens.invalid-json-token-name-detail" %)}
:error.import/duplicated-token-name
{:error/code :error.import/duplicated-token-name
:error/fn #(tr "workspace.tokens.duplicated-json-token-name")
:error/detail #(tr "workspace.tokens.duplicated-json-token-name-detail" %)}
:error.import/style-dictionary-reference-errors :error.import/style-dictionary-reference-errors
{:error/code :error.import/style-dictionary-reference-errors {:error/code :error.import/style-dictionary-reference-errors
:error/fn #(str (tr "errors.tokens.import-error") "\n\n" (first %)) :error/fn #(str (tr "errors.tokens.import-error") "\n\n" (first %))

View File

@ -8,13 +8,12 @@
(:require (:require
[app.common.files.tokens :as cfo] [app.common.files.tokens :as cfo]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.tokens-lib :as ctob]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls] [app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic] [app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc form* (mf/defc form*
[{:keys [token token-type selected-token-set-id] :as props}] [{:keys [token token-type] :as props}]
(let [initial (let [initial
(mf/with-memo [token-type token] (mf/with-memo [token-type token]
{:type token-type {:type token-type
@ -23,11 +22,7 @@
:description (:description token "") :description (:description token "")
:color-result ""}) :color-result ""})
props (mf/spread-props props {:make-schema #(-> (cfo/make-token-schema %1 props (mf/spread-props props {:make-schema #(-> (cfo/make-token-schema %1 token-type)
token-type
selected-token-set-id
(when (ctob/token? token)
(ctob/get-id token)))
(sm/dissoc-key :id) (sm/dissoc-key :id)
(sm/assoc-key :color-result :string)) (sm/assoc-key :color-result :string))
:initial initial :initial initial

View File

@ -9,7 +9,6 @@
[app.common.files.tokens :as cfo] [app.common.files.tokens :as cfo]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.errors :as wte] [app.main.data.workspace.tokens.errors :as wte]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls] [app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic] [app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
@ -30,7 +29,7 @@
(default-validate-token))) (default-validate-token)))
(mf/defc form* (mf/defc form*
[{:keys [token token-type selected-token-set-id] :rest props}] [{:keys [token token-type] :rest props}]
(let [token (let [token
(mf/with-memo [token] (mf/with-memo [token]
(if token (if token
@ -38,11 +37,7 @@
{:type token-type})) {:type token-type}))
props (mf/spread-props props {:token token props (mf/spread-props props {:token token
:token-type token-type :token-type token-type
:make-schema #(-> (cfo/make-token-schema %1 :make-schema #(-> (cfo/make-token-schema %1 token-type)
token-type
selected-token-set-id
(when (ctob/token? token)
(ctob/get-id token)))
(sm/dissoc-key :id) (sm/dissoc-key :id)
;; The value as edited in the form is a simple stirng. ;; The value as edited in the form is a simple stirng.
;; It's converted to vector in the validator. ;; It's converted to vector in the validator.

View File

@ -27,13 +27,13 @@
selected-token-set-id selected-token-set-id
(mf/deref refs/selected-token-set-id) (mf/deref refs/selected-token-set-id)
tokens-in-selected-set
(mf/deref refs/workspace-all-tokens-in-selected-set)
token-path token-path
(mf/with-memo [token] (mf/with-memo [token]
(ctob/get-token-path token)) (ctob/get-token-path token))
tokens-in-selected-set
(mf/deref refs/workspace-all-tokens-in-selected-set)
tokens-tree-in-selected-set tokens-tree-in-selected-set
(mf/with-memo [token-path tokens-in-selected-set] (mf/with-memo [token-path tokens-in-selected-set]
(-> (ctob/tokens-tree tokens-in-selected-set) (-> (ctob/tokens-tree tokens-in-selected-set)

View File

@ -69,6 +69,7 @@
action action
is-create is-create
selected-token-set-id selected-token-set-id
tokens-tree-in-selected-set
token-type token-type
make-schema make-schema
input-component input-component
@ -78,11 +79,7 @@
value-subfield value-subfield
input-value-placeholder] :as props}] input-value-placeholder] :as props}]
(let [make-schema (or make-schema #(-> (cfo/make-token-schema % (let [make-schema (or make-schema #(-> (cfo/make-token-schema % token-type)
token-type
selected-token-set-id
(when (ctob/token? token)
(ctob/get-id token)))
(sm/dissoc-key :id))) (sm/dissoc-key :id)))
input-component (or input-component token.controls/input*) input-component (or input-component token.controls/input*)
validate-token (or validator default-validate-token) validate-token (or validator default-validate-token)
@ -99,8 +96,6 @@
token-title (str/lower (:title token-properties)) token-title (str/lower (:title token-properties))
tokens-lib (mf/deref refs/tokens-lib)
;; All tokens in the lib, as a map name -> token, flattened ;; All tokens in the lib, as a map name -> token, flattened
;; including tokens in inactive sets. ;; including tokens in inactive sets.
tokens-tree (mf/deref refs/workspace-all-tokens-map) tokens-tree (mf/deref refs/workspace-all-tokens-map)
@ -138,8 +133,8 @@
resolved-active-tokens)))) resolved-active-tokens))))
schema schema
(mf/with-memo [tokens-lib active-tab] (mf/with-memo [tokens-tree-in-selected-set active-tab]
(make-schema tokens-lib active-tab)) (make-schema tokens-tree-in-selected-set active-tab))
initial initial
(mf/with-memo [token initial] (mf/with-memo [token initial]

View File

@ -3,8 +3,8 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.files.tokens :as cfo] [app.common.files.tokens :as cfo]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
@ -18,8 +18,8 @@
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc rename-node-form* (mf/defc rename-node-form*
[{:keys [new-node-name node active-tokens tokens-lib selected-token-set-id variant on-close on-submit]}] [{:keys [new-node-name node active-tokens tokens-tree variant on-close on-submit]}]
(let [make-schema #(cfo/make-node-token-schema active-tokens tokens-lib node selected-token-set-id) (let [make-schema #(cfo/make-node-token-schema active-tokens tokens-tree node)
schema schema
(mf/with-memo [active-tokens] (mf/with-memo [active-tokens]
@ -82,9 +82,10 @@
(let [variant (d/nilv variant "rename") ;; "rename" or "duplicate" (let [variant (d/nilv variant "rename") ;; "rename" or "duplicate"
selected-token-set-id (mf/deref refs/selected-token-set-id) tokens-tree-in-selected-set
(mf/with-memo [tokens-in-active-set node]
tokens-lib (mf/deref refs/tokens-lib) (-> (ctob/tokens-tree tokens-in-active-set)
(d/dissoc-in (:name node))))
close-modal close-modal
(mf/use-fn (mf/use-fn
@ -117,7 +118,6 @@
:node node :node node
:variant variant :variant variant
:active-tokens tokens-in-active-set :active-tokens tokens-in-active-set
:tokens-lib tokens-lib :tokens-tree tokens-tree-in-selected-set
:selected-token-set-id selected-token-set-id
:on-close close-modal :on-close close-modal
:on-submit rename}]]])) :on-submit rename}]]]))

View File

@ -260,7 +260,7 @@
;; TODO: use cfo/make-schema:token-value and extend it with shadow and reference fields ;; TODO: use cfo/make-schema:token-value and extend it with shadow and reference fields
(defn- make-schema (defn- make-schema
[set-id token-id tokens-lib active-tab] [tokens-tree active-tab]
(sm/schema (sm/schema
[:and [:and
[:map [:map
@ -271,7 +271,7 @@
(sm/update-properties cto/schema:token-name assoc (sm/update-properties cto/schema:token-name assoc
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-lib set-id token-id))]]] #(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [:value
[:map [:map
@ -348,7 +348,8 @@
:shadow [default-token-shadow]})) :shadow [default-token-shadow]}))
(mf/defc form* (mf/defc form*
[{:keys [token token-type selected-token-set-id] :as props}] [{:keys [token
token-type] :as props}]
(let [token (let [token
(mf/with-memo [token] (mf/with-memo [token]
(or token (or token
@ -359,12 +360,6 @@
{:type token-type {:type token-type
:value {:reference nil :value {:reference nil
:shadow [default-token-shadow]}}))) :shadow [default-token-shadow]}})))
make-schema
(mf/with-memo [selected-token-set-id token]
(partial make-schema selected-token-set-id (when (ctob/token? token)
(ctob/get-id token))))
initial initial
(mf/with-memo [token] (mf/with-memo [token]
(let [raw-value (:value token) (let [raw-value (:value token)

View File

@ -209,7 +209,7 @@
;; TODO: use cfo/make-schema:token-value and extend it with typography and reference fields ;; TODO: use cfo/make-schema:token-value and extend it with typography and reference fields
(defn- make-schema (defn- make-schema
[set-id token-id tokens-lib active-tab] [tokens-tree active-tab]
(sm/schema (sm/schema
[:and [:and
[:map [:map
@ -220,7 +220,7 @@
(sm/update-properties cto/schema:token-name assoc (sm/update-properties cto/schema:token-name assoc
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-lib set-id token-id))]]] #(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [:value
[:map [:map
@ -269,7 +269,7 @@
result))]])) result))]]))
(mf/defc form* (mf/defc form*
[{:keys [token selected-token-set-id] :as props}] [{:keys [token] :as props}]
(let [initial (let [initial
(mf/with-memo [token] (mf/with-memo [token]
(let [value (:value token) (let [value (:value token)
@ -296,12 +296,6 @@
{:name (:name token "") {:name (:name token "")
:value processed-value :value processed-value
:description (:description token "")})) :description (:description token "")}))
make-schema
(mf/with-memo [selected-token-set-id token]
(partial make-schema selected-token-set-id (when (ctob/token? token)
(ctob/get-id token))))
props (mf/spread-props props {:initial initial props (mf/spread-props props {:initial initial
:make-schema make-schema :make-schema make-schema
:token token :token token

View File

@ -132,7 +132,7 @@
on-popover-open-click on-popover-open-click
(mf/use-fn (mf/use-fn
(mf/deps type title modal selected-token-set-id) (mf/deps type title modal)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (st/emit!
@ -144,8 +144,7 @@
:fields (:fields modal) :fields (:fields modal)
:title title :title title
:action "create" :action "create"
:token-type type :token-type type})))))
:selected-token-set-id selected-token-set-id})))))
on-token-pill-click on-token-pill-click
(mf/use-fn (mf/use-fn

View File

@ -122,9 +122,7 @@
(ctob/get-name token))) (ctob/get-name token)))
:schema (cfo/make-token-name-schema :schema (cfo/make-token-name-schema
(some-> (u/locate-tokens-lib file-id) (some-> (u/locate-tokens-lib file-id)
(ctob/get-tokens set-id)) (ctob/get-tokens set-id)))
set-id
id)
:set :set
(fn [_ value] (fn [_ value]
(st/emit! (-> (dwtl/update-token set-id id {:name value}) (st/emit! (-> (dwtl/update-token set-id id {:name value})
@ -313,17 +311,19 @@
:addToken :addToken
{:enumerable false {:enumerable false
:schema (fn [args] :schema (fn [args]
[:tuple (-> (cfo/make-token-schema (let [tokens-tree (-> (u/locate-tokens-lib file-id)
(u/locate-tokens-lib file-id) (ctob/get-tokens id)
(cto/dtcg-token-type->token-type (-> args (first) (get "type"))) ;; Convert to the adecuate format for schema
id (ctob/tokens-tree))]
(-> args (first) (get "id"))) [:tuple (-> (cfo/make-token-schema
;; Don't allow plugins to set the id tokens-tree
(sm/dissoc-key :id) (cto/dtcg-token-type->token-type (-> args (first) (get "type"))))
;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below) ;; Don't allow plugins to set the id
;; and set a converter that changes DTCG types to internal types (:decode/json). (sm/dissoc-key :id)
;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width ;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below)
(sm/update-properties assoc :decode/json cfo/convert-dtcg-token))]) ;; and set a converter that changes DTCG types to internal types (:decode/json).
;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width
(sm/update-properties assoc :decode/json cfo/convert-dtcg-token))]))
:decode/options {:key-fn identity} :decode/options {:key-fn identity}
:fn (fn [attrs] :fn (fn [attrs]
(let [tokens-lib (u/locate-tokens-lib file-id) (let [tokens-lib (u/locate-tokens-lib file-id)

View File

@ -1917,6 +1917,10 @@ msgid "errors.tokens.value-with-units"
msgstr "Invalid value: Units are not allowed." msgstr "Invalid value: Units are not allowed."
#: src/app/main/data/media.cljs:74 #: src/app/main/data/media.cljs:74
msgid "errors.token-theme-not-existing-sets"
msgstr "The theme refers to some not existing sets: %s"
#: src/app/main/data/media.cljs:73
msgid "errors.unexpected-error" msgid "errors.unexpected-error"
msgstr "An unexpected error occurred." msgstr "An unexpected error occurred."

View File

@ -1871,6 +1871,10 @@ msgid "errors.tokens.value-with-units"
msgstr "Valor no válido: No se permiten unidades." msgstr "Valor no válido: No se permiten unidades."
#: src/app/main/data/media.cljs:74 #: src/app/main/data/media.cljs:74
msgid "errors.token-theme-not-existing-sets"
msgstr "El tema referencia sets que no existen: %s"
#: src/app/main/data/media.cljs:73
msgid "errors.unexpected-error" msgid "errors.unexpected-error"
msgstr "Ha ocurrido un error inesperado." msgstr "Ha ocurrido un error inesperado."