diff --git a/common/src/app/common/files/helpers.cljc b/common/src/app/common/files/helpers.cljc index 3795812c3..bb0fb744f 100644 --- a/common/src/app/common/files/helpers.cljc +++ b/common/src/app/common/files/helpers.cljc @@ -427,11 +427,6 @@ (map #(str/concat base-name (suffix-fn %)) (iterate inc 1)))) -(defn ^:private get-suffix - "Default suffix impelemtation" - [copy-count] - (str/concat " " copy-count)) - (defn generate-unique-name "Generates a unique name by selecting the first available name from a generated sequence. The sequence consists of `base-name` and its variants, avoiding conflicts with `existing-names`. @@ -445,8 +440,7 @@ Returns: - A unique name not present in `existing-names`." - [base-name existing-names & {:keys [suffix-fn immediate-suffix?] - :or {suffix-fn get-suffix}}] + [base-name existing-names & {:keys [suffix-fn immediate-suffix? suffix]}] (dm/assert! "expected a set of strings" (coll? existing-names)) @@ -454,9 +448,21 @@ (dm/assert! "expected a string for `basename`." (string? base-name)) - (let [existing-name-set (cond-> (set existing-names) + (let [suffix-fn (if suffix-fn + suffix-fn + (if suffix + (fn [copy-count] + (str/concat "-" + suffix + (when (> copy-count 1) + (str "-" copy-count)))) + (fn [copy-count] + (str/concat " " copy-count)))) + + existing-name-set (cond-> (set existing-names) immediate-suffix? (conj base-name)) names (name-seq base-name suffix-fn)] + (->> names (remove #(contains? existing-name-set %)) first))) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 429629cf1..ed43f5547 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -9,6 +9,7 @@ #?(:clj [app.common.fressian :as fres]) [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.schema :as sm] [app.common.time :as dt] [app.common.transit :as t] @@ -321,6 +322,7 @@ (assoc-in [:ids temp-id] token)))) {:tokens-tree {} :ids {}} tokens)) + (defprotocol ITokenSet (update-name [_ set-name] "change a token set name while keeping the path") (add-token [_ token] "add a token at the end of the list") @@ -920,6 +922,7 @@ Will return a value that matches this schema: this))) + (delete-set [_ set-name] (let [prefixed-path (set-name->prefixed-full-path set-name)] (TokensLib. (d/dissoc-in sets prefixed-path) @@ -1468,6 +1471,14 @@ Will return a value that matches this schema: {:encode/json encode-dtcg :decode/json decode-dtcg}}) +(defn duplicate-set [set-name lib & {:keys [suffix]}] + (let [sets (get-sets lib) + unames (map :name sets) + copy-name (cfh/generate-unique-name set-name unames :suffix suffix)] + (some-> (get-set lib set-name) + (assoc :name copy-name) + (assoc :modified-at (dt/now))))) + (sm/register! type:tokens-lib) ;; === Serialization handlers for RPC API (transit) and database (fressian) diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc index 0b80eed16..ab660fcba 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -120,7 +120,6 @@ (t/is (= ["Foo/Foo" "Foo/Baz" "Foo/Bar"] (move ["Foo"] ["Foo" "Foo"] ["Foo" "Baz"] false))) (t/is (= ["Foo/Baz" "Foo/Bar" "Foo/Foo"] (move ["Foo"] ["Foo" "Foo"] nil false)))))) - (t/deftest move-token-set-nested-2 (let [tokens-lib (-> (ctob/make-tokens-lib) (ctob/add-set (ctob/make-token-set :name "a/b")) @@ -220,7 +219,6 @@ (t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception) #"expected valid params for token-theme" (ctob/make-token-theme params))))) - (t/deftest make-tokens-lib (let [tokens-lib (ctob/make-tokens-lib)] (t/is (= (ctob/set-count tokens-lib) 0)))) @@ -315,6 +313,58 @@ (t/is (= (:sets token-theme') #{})) (t/is (nil? token-set')))) +(t/deftest duplicate-token-set + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "test-token-set" + :tokens {"test-token" + (ctob/make-token :name "test-token" + :type :boolean + :value true)}))) + token-set-copy (ctob/duplicate-set "test-token-set" tokens-lib {:suffix "copy"}) + token (get-in token-set-copy [:tokens "test-token"])] + + (t/is (some? token-set-copy)) + (t/is (= (:name token-set-copy) "test-token-set-copy")) + (t/is (= (count (:tokens token-set-copy)) 1)) + (t/is (= (:name token) "test-token")))) + +(t/deftest duplicate-token-set-twice + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "test-token-set" + :tokens {"test-token" + (ctob/make-token :name "test-token" + :type :boolean + :value true)}))) + + tokens-lib (ctob/add-set tokens-lib (ctob/duplicate-set "test-token-set" tokens-lib {:suffix "copy"})) + + token-set-copy (ctob/duplicate-set "test-token-set" tokens-lib {:suffix "copy"}) + token (get-in token-set-copy [:tokens "test-token"])] + + (t/is (some? token-set-copy)) + (t/is (= (:name token-set-copy) "test-token-set-copy-2")) + (t/is (= (count (:tokens token-set-copy)) 1)) + (t/is (= (:name token) "test-token")))) + +(t/deftest duplicate-empty-token-set + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "test-token-set"))) + + token-set-copy (ctob/duplicate-set "test-token-set" tokens-lib {:suffix "copy"}) + tokens (get token-set-copy :tokens)] + + (t/is (some? token-set-copy)) + (t/is (= (:name token-set-copy) "test-token-set-copy")) + (t/is (= (count (:tokens token-set-copy)) 0)) + (t/is (= (count tokens) 0)))) + +(t/deftest duplicate-not-existing-token-set + (let [tokens-lib (ctob/make-tokens-lib) + + token-set-copy (ctob/duplicate-set "test-token-set" tokens-lib {:suffix "copy"})] + + (t/is (nil? token-set-copy)))) + (t/deftest active-themes-set-names (let [tokens-lib (-> (ctob/make-tokens-lib) (ctob/add-set (ctob/make-token-set :name "test-token-set"))) @@ -918,7 +968,6 @@ (t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set))) (t/is (dt/is-after? (:modified-at token') (:modified-at token))))) - (t/deftest update-token-in-sets-rename (let [tokens-lib (-> (ctob/make-tokens-lib) (ctob/add-set (ctob/make-token-set :name "test-token-set")) diff --git a/frontend/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs index 7c234838f..5b56b57d7 100644 --- a/frontend/src/app/main/data/tokens.cljs +++ b/frontend/src/app/main/data/tokens.cljs @@ -21,7 +21,6 @@ [app.main.ui.workspace.tokens.update :as wtu] [app.util.i18n :refer [tr]] [beicon.v2.core :as rx] - [cuerdas.core :as str] [potok.v2.core :as ptk])) (declare set-selected-token-set-name) @@ -192,6 +191,23 @@ (rx/of (set-selected-token-set-name name) (dch/commit-changes changes)))))))) +(defn duplicate-token-set + [id is-group] + (ptk/reify ::duplicate-token-set + ptk/WatchEvent + (watch [it state _] + (let [data (dsh/lookup-file-data state) + name (ctob/normalize-set-name id) + tokens-lib (get data :tokens-lib) + suffix (tr "workspace.token.duplicate-suffix")] + + (when-let [set (ctob/duplicate-set name tokens-lib {:suffix suffix})] + (let [changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/set-token-set (:name set) is-group set))] + (rx/of (set-selected-token-set-name name) + (dch/commit-changes changes)))))))) + (defn toggle-token-set [name] (assert (string? name) "expected a string for `name`") @@ -385,17 +401,8 @@ (when-let [token (ctob/get-token token-set token-name)] (let [tokens (ctob/get-tokens token-set) unames (map :name tokens) - - suffix-fn - (fn [copy-count] - (let [suffix (tr "workspace.token.duplicate-suffix")] - (str/concat "-" - suffix - (when (> copy-count 1) - (str "-" copy-count))))) - - copy-name - (cfh/generate-unique-name token-name unames :suffix-fn suffix-fn)] + suffix (tr "workspace.token.duplicate-suffix") + copy-name (cfh/generate-unique-name token-name unames :suffix suffix)] (rx/of (create-token (assoc token :name copy-name))))))))) diff --git a/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs index 029244de5..9da6a4e56 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs @@ -44,6 +44,12 @@ (fn [] (st/emit! (dt/start-token-set-edition edition-id)))) + on-duplicate + (mf/use-fn + (mf/deps is-group id) + (fn [] + (st/emit! (dt/duplicate-token-set id is-group)))) + on-delete (mf/use-fn (mf/deps is-group path) @@ -53,6 +59,8 @@ (when is-group [:> menu-entry* {:title (tr "workspace.token.add-set-to-group") :on-click create-set-at-path}]) [:> menu-entry* {:title (tr "labels.rename") :on-click on-edit}] + (when-not is-group + [:> menu-entry* {:title (tr "labels.duplicate") :on-click on-duplicate}]) [:> menu-entry* {:title (tr "labels.delete") :on-click on-delete}]])) (mf/defc token-set-context-menu* diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index bca0112e1..49936ee76 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -10,6 +10,7 @@ [frontend-tests.logic.groups-test] [frontend-tests.plugins.context-shapes-test] [frontend-tests.tokens.logic.token-actions-test] + [frontend-tests.tokens.logic.token-data-test] [frontend-tests.tokens.style-dictionary-test] [frontend-tests.tokens.token-form-test] [frontend-tests.tokens.token-test] @@ -39,6 +40,7 @@ 'frontend-tests.util-simple-math-test 'frontend-tests.basic-shapes-test 'frontend-tests.tokens.logic.token-actions-test + 'frontend-tests.tokens.logic.token-data-test 'frontend-tests.tokens.style-dictionary-test 'frontend-tests.tokens.token-test 'frontend-tests.tokens.token-form-test)) diff --git a/frontend/test/frontend_tests/tokens/helpers/tokens.cljs b/frontend/test/frontend_tests/tokens/helpers/tokens.cljs index eb6c4b7cd..fa621df96 100644 --- a/frontend/test/frontend_tests/tokens/helpers/tokens.cljs +++ b/frontend/test/frontend_tests/tokens/helpers/tokens.cljs @@ -20,3 +20,6 @@ :objects shape-id :applied-tokens] merge applied-attributes))) + +(defn get-tokens-lib [file] + (get-in file [:data :tokens-lib])) \ No newline at end of file diff --git a/frontend/test/frontend_tests/tokens/logic/token_data_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_data_test.cljs new file mode 100644 index 000000000..b36f3e144 --- /dev/null +++ b/frontend/test/frontend_tests/tokens/logic/token_data_test.cljs @@ -0,0 +1,61 @@ +(ns frontend-tests.tokens.logic.token-data-test + (:require + [app.common.test-helpers.files :as cthf] + [app.common.types.tokens-lib :as ctob] + [app.main.data.tokens :as dt] + [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-file [] + (cthf/sample-file :file-1 :page-label :page-1)) + +(defn setup-file-with-token-lib + [] + (-> (setup-file) + (assoc-in [:data :tokens-lib] + (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "Set A")))))) + +(t/deftest duplicate-set + (t/async + done + (let [file (setup-file-with-token-lib) + store (ths/setup-store file) + events [(dt/duplicate-token-set "Set A" false)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + token-lib (toht/get-tokens-lib file') + sets (ctob/get-sets token-lib) + set (ctob/get-set token-lib "Set A")] + + (t/testing "Token lib contains two sets" + (t/is (= (count sets) 2)) + (t/is (some? set))))))))) + +(t/deftest duplicate-non-exist-set + (t/async + done + (let [file (setup-file-with-token-lib) + store (ths/setup-store file) + events [(dt/duplicate-token-set "Set B" false)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + token-lib (toht/get-tokens-lib file') + sets (ctob/get-sets token-lib) + set (ctob/get-set token-lib "Set B")] + + (t/testing "Token lib contains one set" + (t/is (= (count sets) 1)) + (t/is (nil? set))))))))) \ No newline at end of file diff --git a/frontend/translations/en.po b/frontend/translations/en.po index d15ec525c..d9b840c91 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1367,6 +1367,10 @@ msgstr "Owner can't leave team, you must reassign the owner role." msgid "errors.token-set-already-exists" msgstr "A set with the same name already exists" +#: src/app/main/data/tokens.cljs: +msgid "errors.token-set-doesnt-exists" +msgstr "Can't duplicate an unkown set" + #: src/app/main/data/tokens.cljs:245 msgid "errors.token-set-exists-on-drop" msgstr "Cannot complete drop, a set with same name already exists at path." @@ -1896,6 +1900,10 @@ msgstr "Dashboard" msgid "labels.delete" msgstr "Delete" +#: src/app/main/ui/workspace/tokens/sets_context_menu.cljs +msgid "labels.duplicate" +msgstr "Duplicate" + #: src/app/main/ui/comments.cljs:976 msgid "labels.delete-comment" msgstr "Delete comment" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 2c317fcde..c319c06f2 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1385,6 +1385,10 @@ msgstr "" msgid "errors.token-set-already-exists" msgstr "Ya existe un set con el mismo nombre" +#: src/app/main/data/tokens.cljs: +msgid "errors.token-set-doesnt-exists" +msgstr "No se puede duplicar un set que no existe." + #: src/app/main/data/tokens.cljs:245 msgid "errors.token-set-exists-on-drop" msgstr "" @@ -1919,6 +1923,10 @@ msgstr "Panel" msgid "labels.delete" msgstr "Borrar" +#: src/app/main/ui/workspace/tokens/sets_context_menu.cljs +msgid "labels.duplicate" +msgstr "Duplicar" + #: src/app/main/ui/comments.cljs:976 msgid "labels.delete-comment" msgstr "Eliminar comentario" @@ -6713,6 +6721,10 @@ msgstr "Borrar theme" msgid "workspace.token.duplicate" msgstr "Duplicar token" +#: src/app/main/data/tokens.cljs:386 +msgid "workspace.token.duplicate-suffix" +msgstr "copiar" + #: src/app/main/ui/workspace/tokens/context_menu.cljs:262 msgid "workspace.token.edit" msgstr "Editar token"