From d49b4923026db3a771c1bd392f82aa7c360d345c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Schr=C3=B6dl?= Date: Tue, 4 Feb 2025 10:59:28 +0100 Subject: [PATCH] :sparkles: Create sets inside folders --- common/src/app/common/logic/tokens.cljc | 2 +- common/src/app/common/types/tokens_lib.cljc | 31 +++-- frontend/playwright/ui/pages/WorkspacePage.js | 3 + frontend/playwright/ui/specs/tokens.spec.js | 64 ++++++--- frontend/src/app/main/data/tokens.cljs | 8 +- .../app/main/ui/workspace/tokens/sets.cljs | 131 ++++++++++-------- .../ui/workspace/tokens/sets_context.cljs | 11 +- .../workspace/tokens/sets_context_menu.cljs | 33 +++-- .../app/main/ui/workspace/tokens/sidebar.cljs | 10 +- frontend/translations/en.po | 4 + 10 files changed, 176 insertions(+), 121 deletions(-) diff --git a/common/src/app/common/logic/tokens.cljc b/common/src/app/common/logic/tokens.cljc index 62b2405183..705e352b23 100644 --- a/common/src/app/common/logic/tokens.cljc +++ b/common/src/app/common/logic/tokens.cljc @@ -52,7 +52,7 @@ [tokens-lib {:keys [from-index to-index position collapsed-paths] :or {collapsed-paths #{}}}] (let [tree (-> (ctob/get-set-tree tokens-lib) - (ctob/walk-sets-tree-seq :walk-children? #(contains? collapsed-paths %))) + (ctob/walk-sets-tree-seq :skip-children-pred #(contains? collapsed-paths %))) from (nth tree from-index) to (nth tree to-index) before (case position diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index e6f16e7141..7213dca372 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -671,18 +671,31 @@ used for managing active sets without a user created theme.") ;; === Import / Export from DTCG format (defn walk-sets-tree-seq - [nodes & {:keys [walk-children?] - :or {walk-children? (constantly true)}}] + "Walk sets tree as a flat list. + + Options: + `:skip-children-pred`: predicate to skip iterating over a set groups children by checking the path of the set group + `:new-editing-set-path`: append a an item with `:new?` at the given path" + [nodes & {:keys [skip-children-pred new-editing-set-path] + :or {skip-children-pred (constantly false)}}] (let [walk (fn walk [node {:keys [parent depth] :or {parent [] depth 0} :as opts}] (lazy-seq (if (d/ordered-map? node) - (mapcat #(walk % opts) node) + (let [root (cond-> node + (= [] new-editing-set-path) (assoc :new? true))] + (mapcat #(walk % opts) root)) (let [[k v] node] (cond - ;;; Set + ;; New set + (= :new? k) [{:new? true + :group? false + :parent-path parent + :depth depth}] + + ;; Set (and v (instance? TokenSet v)) [{:group? false :path (split-token-set-path (:name v)) @@ -698,12 +711,12 @@ used for managing active sets without a user created theme.") :path path :parent-path parent :depth depth}] - (if (walk-children? path) + (if (skip-children-pred path) [item] - (cons - item - (mapcat #(walk % (assoc opts :parent path :depth (inc depth))) v)))))))))] - (walk nodes nil))) + (let [v' (cond-> v + (= path new-editing-set-path) (assoc :new? true))] + (cons item (mapcat #(walk % (assoc opts :parent path :depth (inc depth))) v'))))))))))] + (walk (or nodes (d/ordered-map)) nil))) (defn flatten-nested-tokens-json "Recursively flatten the dtcg token structure, joining keys with '.'." diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 87a2eedab2..06641a90cc 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -95,6 +95,9 @@ export class WorkspacePage extends BaseWebSocketPage { this.tokenContextMenuForToken = page.getByTestId( "tokens-context-menu-for-token", ); + this.tokenContextMenuForSet = page.getByTestId( + "tokens-context-menu-for-set", + ); } async goToWorkspace({ diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index 58e622135c..21ee5ead82 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -26,6 +26,7 @@ const setupEmptyTokensFile = async (page) => { tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar, tokenSetItems: workspacePage.tokenSetItems, tokenSetGroupItems: workspacePage.tokenSetGroupItems, + tokenContextMenuForSet: workspacePage.tokenContextMenuForSet, }; }; @@ -58,6 +59,7 @@ const setupTokensFile = async (page) => { tokenSetGroupItems: workspacePage.tokenSetGroupItems, tokensSidebar: workspacePage.tokensSidebar, tokenContextMenuForToken: workspacePage.tokenContextMenuForToken, + tokenContextMenuForSet: workspacePage.tokenContextMenuForSet, }; }; @@ -223,27 +225,43 @@ test.describe("Tokens: Sets Tab", () => { await setInput.press(finalKey); }; - // test("User creates sets tree structure by entering a set path", async ({ - // page, - // }) => { - // const { - // workspacePage, - // tokenThemesSetsSidebar, - // tokenSetItems, - // tokenSetGroupItems, - // } = await setupEmptyTokensFile(page); - // - // const tokensTabButton = tokenThemesSetsSidebar - // .getByRole("button", { name: "Add set" }) - // .click(); - // - // await createSet(tokenThemesSetsSidebar, "core/colors/light"); - // await createSet(tokenThemesSetsSidebar, "core/colors/dark"); - // - // // User cancels during editing - // await createSet(tokenThemesSetsSidebar, "core/colors/dark", "Escape"); - // - // await expect(tokenSetItems).toHaveCount(2); - // await expect(tokenSetGroupItems).toHaveCount(2); - // }); + test("User creates sets tree structure by entering a set path", async ({ + page, + }) => { + const { + workspacePage, + tokenThemesSetsSidebar, + tokenSetItems, + tokenSetGroupItems, + tokenContextMenuForSet, + } = await setupEmptyTokensFile(page); + + const tokensTabButton = tokenThemesSetsSidebar + .getByRole("button", { name: "Add set" }) + .click(); + + await createSet(tokenThemesSetsSidebar, "core/colors/light"); + await createSet(tokenThemesSetsSidebar, "core/colors/dark"); + + // User cancels during editing + await createSet(tokenThemesSetsSidebar, "core/colors/dark", "Escape"); + + await expect(tokenSetItems).toHaveCount(2); + await expect(tokenSetGroupItems).toHaveCount(2); + + // Create set in group + await tokenThemesSetsSidebar + .getByRole("button", { name: "Collapse core" }) + .click({ button: "right" }); + await expect(tokenContextMenuForSet).toBeVisible(); + await tokenContextMenuForSet.getByText("Add set to this group").click(); + + const setInput = tokenThemesSetsSidebar.locator("input:focus"); + await expect(setInput).toBeVisible(); + await setInput.fill("sizes/small"); + await setInput.press("Enter"); + + await expect(tokenSetItems).toHaveCount(3); + await expect(tokenSetGroupItems).toHaveCount(3); + }); }); diff --git a/frontend/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs index 92db299b0c..83b1ac6097 100644 --- a/frontend/src/app/main/data/tokens.cljs +++ b/frontend/src/app/main/data/tokens.cljs @@ -108,11 +108,9 @@ (dch/commit-changes changes) (wtu/update-workspace-tokens)))))) -(defn create-token-set [token-set] - (let [new-token-set (merge - {:name "Token Set" - :tokens []} - token-set)] +(defn create-token-set [set-name token-set] + (let [new-token-set (-> token-set + (update :name #(if (empty? %) set-name (ctob/join-set-path [% set-name]))))] (ptk/reify ::create-token-set ptk/WatchEvent (watch [it _ _] diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index 59b09ae939..92708d6033 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -37,14 +37,14 @@ (st/emit! (dwts/set-selected-token-set-name set-name))) (defn on-update-token-set [set-name token-set] - (st/emit! (wdt/update-token-set set-name token-set))) + (st/emit! (wdt/update-token-set set-name (ctob/update-name token-set set-name)))) (defn on-update-token-set-group [set-group-path set-group-fname] (st/emit! (wdt/rename-token-set-group set-group-path set-group-fname))) -(defn on-create-token-set [_ token-set] +(defn on-create-token-set [set-name token-set] (st/emit! (ptk/event ::ev/event {::ev/name "create-tokens-set"})) - (st/emit! (wdt/create-token-set token-set))) + (st/emit! (wdt/create-token-set set-name token-set))) (mf/defc editing-label [{:keys [default-value on-cancel on-submit]}] @@ -243,7 +243,7 @@ on-edit-submit' (mf/use-fn (mf/deps set on-edit-submit) - #(on-edit-submit set-name (ctob/update-name set %))) + #(on-edit-submit % set)) on-drag (mf/use-fn @@ -319,7 +319,7 @@ on-toggle-set-group set-node] :as props}] - (let [{:keys [on-edit] :as ctx} (sets-context/use-context) + (let [{:keys [on-edit new-path] :as ctx} (sets-context/use-context) collapsed-paths (mf/use-state #{}) collapsed? (mf/use-fn @@ -331,29 +331,11 @@ (swap! collapsed-paths #(if (contains? % path) (disj % path) (conj % path)))))] - (for [[index {:keys [group? path parent-path depth] :as node}] - (d/enumerate (ctob/walk-sets-tree-seq set-node :walk-children? #(contains? @collapsed-paths %)))] - (if (not group?) - (let [editing-id (sets-context/set-path->id path)] - [:& sets-tree-set - {:key editing-id - :set (:set node) - :label (last path) - :active? active? - :selected? (selected? (get-in node [:set :name])) - :draggable? draggable? - :on-select on-select - :tree-path path - :tree-depth depth - :tree-index index - :tree-parent-path parent-path - :on-toggle on-toggle-set - :editing-id editing-id - :editing? editing? - :on-edit on-edit - :on-edit-reset on-edit-reset - :on-edit-submit on-edit-submit-set - :collapsed-paths collapsed-paths}]) + (for [[index {:keys [new? group? path parent-path depth] :as node}] + (d/enumerate (ctob/walk-sets-tree-seq set-node {:skip-children-pred #(contains? @collapsed-paths %) + :new-editing-set-path new-path}))] + (cond + group? (let [editing-id (sets-context/set-group-path->id path)] [:& sets-tree-set-group {:key editing-id @@ -374,6 +356,49 @@ :collapsed? (collapsed? path) :on-toggle-collapse on-toggle-collapse :on-toggle on-toggle-set-group + :collapsed-paths collapsed-paths}]) + + new? + (let [editing-id (sets-context/set-path->id path)] + [:& sets-tree-set + {:key editing-id + :set (ctob/make-token-set :name (if (empty? parent-path) + "" + (ctob/join-set-path parent-path))) + :label "" + :active? (constantly true) + :selected? (constantly true) + :on-select (constantly nil) + :tree-path path + :tree-depth depth + :tree-index index + :tree-parent-path parent-path + :on-toggle (constantly nil) + :editing-id editing-id + :editing? (constantly true) + :on-edit-reset on-edit-reset + :on-edit-submit on-create-token-set}]) + + :else + (let [editing-id (sets-context/set-path->id path)] + [:& sets-tree-set + {:key editing-id + :set (:set node) + :label (last path) + :active? active? + :selected? (selected? (get-in node [:set :name])) + :draggable? draggable? + :on-select on-select + :tree-path path + :tree-depth depth + :tree-index index + :tree-parent-path parent-path + :on-toggle on-toggle-set + :editing-id editing-id + :editing? editing? + :on-edit on-edit + :on-edit-reset on-edit-reset + :on-edit-submit on-edit-submit-set :collapsed-paths collapsed-paths}]))))) (mf/defc controlled-sets-list @@ -390,45 +415,31 @@ on-select context] :as _props}] - (let [{:keys [editing? new? on-edit on-reset] :as ctx} (or context (sets-context/use-context)) + (let [{:keys [editing? on-edit on-reset new-path] :as ctx} (or context (sets-context/use-context)) theme-modal? (= origin "theme-modal") can-edit? (:can-edit (deref refs/permissions)) draggable? (and (not theme-modal?) can-edit?)] [:fieldset {:class (stl/css :sets-list)} (if (and theme-modal? - (empty? token-sets)) + (empty? token-sets) + (not new-path)) [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message-sets)} (tr "workspace.token.no-sets-create")] - (if (and theme-modal? - (empty? token-sets)) - [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message-sets)} - (tr "workspace.token.no-sets-create")] - [:* - [:& sets-tree - {:draggable? draggable? - :set-node token-sets - :selected? token-set-selected? - :on-select on-select - :active? token-set-active? - :group-active? token-set-group-active? - :on-toggle-set on-toggle-token-set - :on-toggle-set-group on-toggle-token-set-group - :editing? editing? - :on-edit on-edit - :on-edit-reset on-reset - :on-edit-submit-set on-update-token-set - :on-edit-submit-group on-update-token-set-group}] - (when new? - [:& sets-tree-set - {:set (ctob/make-token-set :name "") - :label "" - :selected? (constantly true) - :active? (constantly true) - :editing? (constantly true) - :on-select (constantly nil) - :on-edit (constantly nil) - :on-edit-reset on-reset - :on-edit-submit on-create-token-set}])]))])) + [:& sets-tree + {:draggable? draggable? + :set-node token-sets + :selected? token-set-selected? + :on-select on-select + :active? token-set-active? + :group-active? token-set-group-active? + :on-toggle-set on-toggle-token-set + :on-toggle-set-group on-toggle-token-set-group + :editing? editing? + :on-create-token-set on-create-token-set + :on-edit on-edit + :on-edit-reset on-reset + :on-edit-submit-set on-update-token-set + :on-edit-submit-group on-update-token-set-group}])])) (mf/defc sets-list diff --git a/frontend/src/app/main/ui/workspace/tokens/sets_context.cljs b/frontend/src/app/main/ui/workspace/tokens/sets_context.cljs index 9f01ae24a2..0bb6c67560 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets_context.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets_context.cljs @@ -15,14 +15,12 @@ (defn set-path->id [set-path] (dm/str "set-" set-path)) -(def initial {:editing-id nil - :new? false}) +(def initial {}) (def context (mf/create-context initial)) (def static-context {:editing? (constantly false) - :new? false :on-edit (constantly nil) :on-create (constantly nil) :on-reset (constantly nil)}) @@ -37,7 +35,7 @@ (defn use-context [] (let [ctx (mf/use-ctx context) - {:keys [editing-id new?]} @ctx + {:keys [editing-id new-path]} @ctx editing? (mf/use-callback (mf/deps editing-id) #(= editing-id %)) @@ -45,11 +43,12 @@ (fn [editing-id] (reset! ctx (assoc @ctx :editing-id editing-id)))) on-create (mf/use-fn - #(swap! ctx assoc :editing-id (random-uuid) :new? true)) + (fn [path] + (swap! ctx assoc :editing-id (random-uuid) :new-path path))) on-reset (mf/use-fn #(reset! ctx initial))] {:editing? editing? - :new? new? + :new-path new-path :on-edit on-edit :on-create on-create :on-reset on-reset})) 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 d0bd660bdd..b95da308fd 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 @@ -35,19 +35,27 @@ (mf/defc menu [{:keys [group? path]}] - (let [{:keys [on-edit]} (sets-context/use-context) - edit-name (mf/use-fn - (mf/deps group?) - (fn [] - (let [path (if group? - (sets-context/set-group-path->id path) - (sets-context/set-path->id path))] - (on-edit path)))) - delete-set (mf/use-fn #(st/emit! (wdt/delete-token-set-path group? path)))] + (let [{:keys [on-create on-edit]} (sets-context/use-context) + create-set-at-path + (mf/use-fn + (mf/deps path) + #(on-create path)) + + edit-name + (mf/use-fn + (mf/deps group?) + (fn [] + (let [path (if group? + (sets-context/set-group-path->id path) + (sets-context/set-path->id path))] + (on-edit path)))) + + delete-set + (mf/use-fn + #(st/emit! (wdt/delete-token-set-path group? path)))] [:ul {:class (stl/css :context-list)} - ;; TODO Implement - ;; (when (ctob/prefixed-set-path-final-group? prefixed-set-path) - ;; [:& menu-entry {:title "Add set to this group" :on-click js/console.log}]) + (when 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 edit-name}] [:& menu-entry {:title (tr "labels.delete") :on-click delete-set}]])) @@ -66,6 +74,7 @@ [:& dropdown {:show (boolean mdata) :on-close #(st/emit! wdt/hide-token-set-context-menu)} [:div {:class (stl/css :token-set-context-menu) + :data-testid "tokens-context-menu-for-set" :ref dropdown-ref :style {:top top :left left} :on-context-menu prevent-default} diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 369c389f33..6d41eb28ee 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -201,13 +201,13 @@ (mf/defc add-set-button [{:keys [on-open style]}] - (let [{:keys [on-create new?]} (sets-context/use-context) + (let [{:keys [on-create new-path]} (sets-context/use-context) on-click #(do (on-open) - (on-create)) + (on-create [])) can-edit? (:can-edit (deref refs/permissions))] (if (= style "inline") - (when-not new? + (when-not new-path (if can-edit? [:div {:class (stl/css :empty-sets-wrapper)} [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} @@ -227,9 +227,9 @@ (mf/defc theme-sets-list [{:keys [on-open]}] (let [token-sets (mf/deref refs/workspace-ordered-token-sets) - {:keys [new?] :as ctx} (sets-context/use-context)] + {:keys [new-path] :as ctx} (sets-context/use-context)] (if (and (empty? token-sets) - (not new?)) + (not new-path)) [:& add-set-button {:on-open on-open :style "inline"}] [:& h/sortable-container {} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index d1a01a0060..4d3b52e31e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6539,6 +6539,10 @@ msgstr "Enter token value or alias" msgid "workspace.token.grouping-set-alert" msgstr "Token Set grouping is not supported yet." +#: src/app/main/ui/workspace/tokens/sets_context_menu.cljs +msgid "workspace.token.add-set-to-group" +msgstr "Add set to this group" + #: src/app/main/ui/workspace/tokens/modals/themes.cljs:179 msgid "workspace.token.label.group" msgstr "Group"