From 174d91a519dc0ed1dcda73d06bf119d5567efa7a Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Fri, 28 Jun 2024 14:39:36 +0200 Subject: [PATCH] Add function to check if a token can be placed under a name path --- .../app/main/ui/workspace/tokens/form.cljs | 21 +++++++++++--- .../app/main/ui/workspace/tokens/token.cljs | 28 +++++++++++++++++++ frontend/test/token_tests/token_test.cljs | 9 +++++- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs index 34217e1cd..954340249 100644 --- a/frontend/src/app/main/ui/workspace/tokens/form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs @@ -13,6 +13,7 @@ [app.main.store :as st] [app.main.ui.workspace.tokens.common :as tokens.common] [app.main.ui.workspace.tokens.style-dictionary :as sd] + [app.main.ui.workspace.tokens.token :as wtt] [app.util.dom :as dom] [cuerdas.core :as str] [malli.core :as m] @@ -41,17 +42,24 @@ Token names should only contain letters and digits separated by . characters.")} (defn token-name-schema "Generate a dynamic schema validation to check if a token name already exists. `existing-token-names` should be a set of strings." - [existing-token-names] + [{:keys [existing-token-names tokens-tree]}] (let [non-existing-token-schema (m/-simple-schema {:type :token/name-exists :pred #(not (get existing-token-names %)) :type-properties {:error/fn #(str (:value %) " is an already existing token name") + :existing-token-names existing-token-names}}) + path-exists-schema + (m/-simple-schema + {:type :token/name-exists + :pred #(wtt/token-name-path-exists? % tokens-tree) + :type-properties {:error/fn #(str "A token already exists at the path: " (:value %)) :existing-token-names existing-token-names}})] (m/schema [:and [:string {:min 1 :max 255}] valid-token-name-schema + path-exists-schema non-existing-token-schema]))) (def token-description-schema @@ -100,7 +108,7 @@ Token names should only contain letters and digits separated by . characters.")} new-tokens (update tokens token-id merge {:id token-id :value input :name token-name})] - (-> (sd/resolve-tokens+ new-tokens) + (-> (sd/resolve-tokens+ new-tokens {:debug? true}) (p/then (fn [resolved-tokens] (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-id)] @@ -141,6 +149,9 @@ Token names should only contain letters and digits separated by . characters.")} {::mf/wrap-props false} [{:keys [token token-type] :as _args}] (let [tokens (sd/use-resolved-workspace-tokens) + tokens-tree (mf/use-memo + (mf/deps tokens) + #(wtt/token-names-tree tokens)) existing-token-names (mf/use-memo (mf/deps tokens) (fn [] @@ -152,10 +163,12 @@ Token names should only contain letters and digits separated by . characters.")} ;; Name name-ref (mf/use-var (:name token)) name-errors (mf/use-state nil) + _ (js/console.log "name-errors" @name-errors) validate-name (mf/use-callback - (mf/deps existing-token-names) + (mf/deps tokens-tree existing-token-names) (fn [value] - (let [schema (token-name-schema existing-token-names)] + (let [schema (token-name-schema {:existing-token-names existing-token-names + :tokens-tree tokens-tree})] (m/explain schema (finalize-name value))))) on-update-name-debounced (mf/use-callback (debounce (fn [e] diff --git a/frontend/src/app/main/ui/workspace/tokens/token.cljs b/frontend/src/app/main/ui/workspace/tokens/token.cljs index a7f2289ac..1762f1977 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token.cljs @@ -18,3 +18,31 @@ (let [path (token-name->path name)] (assoc-in acc path token)))) {} tokens)) + +(defn token-name-path-exists? + "Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists. + + It's not allowed to create a token inside a token. E.g.: + Creating a token with + + {:name \"foo.bar\"} + + in the tokens tree: + + {\"foo\" {:name \"other\"}}" + [token-name token-names-tree] + (let [name-path (token-name->path token-name) + result (reduce + (fn [acc cur] + (let [target (get acc cur)] + (cond + ;; Path segment doesn't exist yet + (nil? target) (reduced false) + ;; A token exists at this path + (:name target) (reduced true) + ;; Continue traversing the true + :else target))) + token-names-tree name-path)] + (if (map? result) + (some? (:name result)) + result))) diff --git a/frontend/test/token_tests/token_test.cljs b/frontend/test/token_tests/token_test.cljs index 0717130cb..f2e0332c4 100644 --- a/frontend/test/token_tests/token_test.cljs +++ b/frontend/test/token_tests/token_test.cljs @@ -14,7 +14,7 @@ (t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz"))) (t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz....")))) -(t/deftest tokens-name-tree +(t/deftest tokens-name-tree-test (t/is (= {"foo" {"bar" {"baz" {:name "foo.bar.baz", :value "a"}, @@ -26,3 +26,10 @@ :value "b"} :c {:name "baz.bar.foo" :value "{foo.bar.baz}"}})))) + +(t/deftest token-name-path-exists?-test + (t/is (true? (wtt/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}}))) + (t/is (true? (wtt/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}}))) + (t/is (true? (wtt/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}}))) + (t/is (false? (wtt/token-name-path-exists? "other" {"border-radius" {:name "sm"}}))) + (t/is (false? (wtt/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))