Add duplicate sets feature (#6240)

*  Add duplicate sets feature

*  Add test to each module

* 🎉 Fix comments

* 🎉 Remove duplicate from groups

* 🎉 Remove create theme from test

* 🎉 Remove ' from names
This commit is contained in:
Eva Marco 2025-04-14 16:22:40 +02:00 committed by GitHub
parent f5c699ab7a
commit eee5cf5fb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 190 additions and 23 deletions

View file

@ -427,11 +427,6 @@
(map #(str/concat base-name (suffix-fn %)) (map #(str/concat base-name (suffix-fn %))
(iterate inc 1)))) (iterate inc 1))))
(defn ^:private get-suffix
"Default suffix impelemtation"
[copy-count]
(str/concat " " copy-count))
(defn generate-unique-name (defn generate-unique-name
"Generates a unique name by selecting the first available name from a generated sequence. "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`. The sequence consists of `base-name` and its variants, avoiding conflicts with `existing-names`.
@ -445,8 +440,7 @@
Returns: Returns:
- A unique name not present in `existing-names`." - A unique name not present in `existing-names`."
[base-name existing-names & {:keys [suffix-fn immediate-suffix?] [base-name existing-names & {:keys [suffix-fn immediate-suffix? suffix]}]
:or {suffix-fn get-suffix}}]
(dm/assert! (dm/assert!
"expected a set of strings" "expected a set of strings"
(coll? existing-names)) (coll? existing-names))
@ -454,9 +448,21 @@
(dm/assert! (dm/assert!
"expected a string for `basename`." "expected a string for `basename`."
(string? base-name)) (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)) immediate-suffix? (conj base-name))
names (name-seq base-name suffix-fn)] names (name-seq base-name suffix-fn)]
(->> names (->> names
(remove #(contains? existing-name-set %)) (remove #(contains? existing-name-set %))
first))) first)))

View file

@ -9,6 +9,7 @@
#?(:clj [app.common.fressian :as fres]) #?(:clj [app.common.fressian :as fres])
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as dt] [app.common.time :as dt]
[app.common.transit :as t] [app.common.transit :as t]
@ -321,6 +322,7 @@
(assoc-in [:ids temp-id] token)))) (assoc-in [:ids temp-id] token))))
{:tokens-tree {} :ids {}} tokens)) {:tokens-tree {} :ids {}} tokens))
(defprotocol ITokenSet (defprotocol ITokenSet
(update-name [_ set-name] "change a token set name while keeping the path") (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") (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))) this)))
(delete-set [_ set-name] (delete-set [_ set-name]
(let [prefixed-path (set-name->prefixed-full-path set-name)] (let [prefixed-path (set-name->prefixed-full-path set-name)]
(TokensLib. (d/dissoc-in sets prefixed-path) (TokensLib. (d/dissoc-in sets prefixed-path)
@ -1468,6 +1471,14 @@ Will return a value that matches this schema:
{:encode/json encode-dtcg {:encode/json encode-dtcg
:decode/json decode-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) (sm/register! type:tokens-lib)
;; === Serialization handlers for RPC API (transit) and database (fressian) ;; === Serialization handlers for RPC API (transit) and database (fressian)

View file

@ -120,7 +120,6 @@
(t/is (= ["Foo/Foo" "Foo/Baz" "Foo/Bar"] (move ["Foo"] ["Foo" "Foo"] ["Foo" "Baz"] false))) (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/is (= ["Foo/Baz" "Foo/Bar" "Foo/Foo"] (move ["Foo"] ["Foo" "Foo"] nil false))))))
(t/deftest move-token-set-nested-2 (t/deftest move-token-set-nested-2
(let [tokens-lib (-> (ctob/make-tokens-lib) (let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "a/b")) (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" (t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception) #"expected valid params for token-theme"
(ctob/make-token-theme params))))) (ctob/make-token-theme params)))))
(t/deftest make-tokens-lib (t/deftest make-tokens-lib
(let [tokens-lib (ctob/make-tokens-lib)] (let [tokens-lib (ctob/make-tokens-lib)]
(t/is (= (ctob/set-count tokens-lib) 0)))) (t/is (= (ctob/set-count tokens-lib) 0))))
@ -315,6 +313,58 @@
(t/is (= (:sets token-theme') #{})) (t/is (= (:sets token-theme') #{}))
(t/is (nil? token-set')))) (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 (t/deftest active-themes-set-names
(let [tokens-lib (-> (ctob/make-tokens-lib) (let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "test-token-set"))) (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-set') (:modified-at token-set)))
(t/is (dt/is-after? (:modified-at token') (:modified-at token))))) (t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
(t/deftest update-token-in-sets-rename (t/deftest update-token-in-sets-rename
(let [tokens-lib (-> (ctob/make-tokens-lib) (let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "test-token-set")) (ctob/add-set (ctob/make-token-set :name "test-token-set"))

View file

@ -21,7 +21,6 @@
[app.main.ui.workspace.tokens.update :as wtu] [app.main.ui.workspace.tokens.update :as wtu]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
(declare set-selected-token-set-name) (declare set-selected-token-set-name)
@ -192,6 +191,23 @@
(rx/of (set-selected-token-set-name name) (rx/of (set-selected-token-set-name name)
(dch/commit-changes changes)))))))) (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 (defn toggle-token-set
[name] [name]
(assert (string? name) "expected a string for `name`") (assert (string? name) "expected a string for `name`")
@ -385,17 +401,8 @@
(when-let [token (ctob/get-token token-set token-name)] (when-let [token (ctob/get-token token-set token-name)]
(let [tokens (ctob/get-tokens token-set) (let [tokens (ctob/get-tokens token-set)
unames (map :name tokens) unames (map :name tokens)
suffix (tr "workspace.token.duplicate-suffix")
suffix-fn copy-name (cfh/generate-unique-name token-name unames :suffix suffix)]
(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)]
(rx/of (create-token (assoc token :name copy-name))))))))) (rx/of (create-token (assoc token :name copy-name)))))))))

View file

@ -44,6 +44,12 @@
(fn [] (fn []
(st/emit! (dt/start-token-set-edition edition-id)))) (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 on-delete
(mf/use-fn (mf/use-fn
(mf/deps is-group path) (mf/deps is-group path)
@ -53,6 +59,8 @@
(when is-group (when is-group
[:> menu-entry* {:title (tr "workspace.token.add-set-to-group") :on-click create-set-at-path}]) [:> 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}] [:> 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}]])) [:> menu-entry* {:title (tr "labels.delete") :on-click on-delete}]]))
(mf/defc token-set-context-menu* (mf/defc token-set-context-menu*

View file

@ -10,6 +10,7 @@
[frontend-tests.logic.groups-test] [frontend-tests.logic.groups-test]
[frontend-tests.plugins.context-shapes-test] [frontend-tests.plugins.context-shapes-test]
[frontend-tests.tokens.logic.token-actions-test] [frontend-tests.tokens.logic.token-actions-test]
[frontend-tests.tokens.logic.token-data-test]
[frontend-tests.tokens.style-dictionary-test] [frontend-tests.tokens.style-dictionary-test]
[frontend-tests.tokens.token-form-test] [frontend-tests.tokens.token-form-test]
[frontend-tests.tokens.token-test] [frontend-tests.tokens.token-test]
@ -39,6 +40,7 @@
'frontend-tests.util-simple-math-test 'frontend-tests.util-simple-math-test
'frontend-tests.basic-shapes-test 'frontend-tests.basic-shapes-test
'frontend-tests.tokens.logic.token-actions-test 'frontend-tests.tokens.logic.token-actions-test
'frontend-tests.tokens.logic.token-data-test
'frontend-tests.tokens.style-dictionary-test 'frontend-tests.tokens.style-dictionary-test
'frontend-tests.tokens.token-test 'frontend-tests.tokens.token-test
'frontend-tests.tokens.token-form-test)) 'frontend-tests.tokens.token-form-test))

View file

@ -20,3 +20,6 @@
:objects shape-id :objects shape-id
:applied-tokens] :applied-tokens]
merge applied-attributes))) merge applied-attributes)))
(defn get-tokens-lib [file]
(get-in file [:data :tokens-lib]))

View file

@ -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)))))))))

View file

@ -1367,6 +1367,10 @@ msgstr "Owner can't leave team, you must reassign the owner role."
msgid "errors.token-set-already-exists" msgid "errors.token-set-already-exists"
msgstr "A set with the same name 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 #: src/app/main/data/tokens.cljs:245
msgid "errors.token-set-exists-on-drop" msgid "errors.token-set-exists-on-drop"
msgstr "Cannot complete drop, a set with same name already exists at path." msgstr "Cannot complete drop, a set with same name already exists at path."
@ -1896,6 +1900,10 @@ msgstr "Dashboard"
msgid "labels.delete" msgid "labels.delete"
msgstr "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 #: src/app/main/ui/comments.cljs:976
msgid "labels.delete-comment" msgid "labels.delete-comment"
msgstr "Delete comment" msgstr "Delete comment"

View file

@ -1385,6 +1385,10 @@ msgstr ""
msgid "errors.token-set-already-exists" msgid "errors.token-set-already-exists"
msgstr "Ya existe un set con el mismo nombre" 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 #: src/app/main/data/tokens.cljs:245
msgid "errors.token-set-exists-on-drop" msgid "errors.token-set-exists-on-drop"
msgstr "" msgstr ""
@ -1919,6 +1923,10 @@ msgstr "Panel"
msgid "labels.delete" msgid "labels.delete"
msgstr "Borrar" msgstr "Borrar"
#: src/app/main/ui/workspace/tokens/sets_context_menu.cljs
msgid "labels.duplicate"
msgstr "Duplicar"
#: src/app/main/ui/comments.cljs:976 #: src/app/main/ui/comments.cljs:976
msgid "labels.delete-comment" msgid "labels.delete-comment"
msgstr "Eliminar comentario" msgstr "Eliminar comentario"
@ -6713,6 +6721,10 @@ msgstr "Borrar theme"
msgid "workspace.token.duplicate" msgid "workspace.token.duplicate"
msgstr "Duplicar token" 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 #: src/app/main/ui/workspace/tokens/context_menu.cljs:262
msgid "workspace.token.edit" msgid "workspace.token.edit"
msgstr "Editar token" msgstr "Editar token"