diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index d5c10a90e3..a6c7153e72 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -14,7 +14,7 @@ test.beforeEach(async ({ page }) => { await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json"); }); -test.describe("Tokens - CRUD", () => { +test.describe("Tokens - creation", () => { test("User creates border radius token", async ({ page }) => { await testTokenCreationFlow(page, { tokenLabel: "Border Radius", @@ -1256,6 +1256,91 @@ test.describe("Tokens - CRUD", () => { ).toBeEnabled(); }); + test("User creates grouped color token", async ({ page }) => { + const { workspacePage, tokensUpdateCreateModal, tokensSidebar } = + await setupEmptyTokensFile(page); + + await tokensSidebar + .getByRole("button", { name: "Add Token: Color" }) + .click(); + + // Create grouped color token with mouse + + await expect(tokensUpdateCreateModal).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const valueField = tokensUpdateCreateModal.getByLabel("Value"); + + await nameField.click(); + await nameField.fill("dark.primary"); + + await valueField.click(); + await valueField.fill("red"); + + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + + await unfoldTokenTree(tokensSidebar, "color", "dark.primary"); + + await expect(tokensSidebar.getByLabel("primary")).toBeEnabled(); + }); + + test("User cant create regular token with value missing", async ({ + page, + }) => { + const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page); + + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + await tokensTabPanel + .getByRole("button", { name: "Add Token: Color" }) + .click(); + + await expect(tokensUpdateCreateModal).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // Initially submit button should be disabled + await expect(submitButton).toBeDisabled(); + + // Fill in name but leave value empty + await nameField.click(); + await nameField.fill("primary"); + + // Submit button should remain disabled when value is empty + await expect(submitButton).toBeDisabled(); + }); + + test("User duplicate color token", async ({ page }) => { + const { tokensSidebar, tokenContextMenuForToken } = + await setupTokensFile(page); + + await expect(tokensSidebar).toBeVisible(); + + unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); + + const colorToken = tokensSidebar.getByRole("button", { + name: "100", + }); + + await colorToken.click({ button: "right" }); + await expect(tokenContextMenuForToken).toBeVisible(); + + await tokenContextMenuForToken.getByText("Duplicate token").click(); + await expect(tokenContextMenuForToken).not.toBeVisible(); + + await expect( + tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }), + ).toBeVisible(); + }); +}); + +test.describe("Tokens tab - edition", () => { test("User edits typography token and all fields are valid", async ({ page, }) => { @@ -1388,67 +1473,7 @@ test.describe("Tokens - CRUD", () => { await expect(colorTokenChanged).toBeVisible(); }); - test("User creates grouped color token", async ({ page }) => { - const { workspacePage, tokensUpdateCreateModal, tokensSidebar } = - await setupEmptyTokensFile(page); - - await tokensSidebar - .getByRole("button", { name: "Add Token: Color" }) - .click(); - - // Create grouped color token with mouse - - await expect(tokensUpdateCreateModal).toBeVisible(); - - const nameField = tokensUpdateCreateModal.getByLabel("Name"); - const valueField = tokensUpdateCreateModal.getByLabel("Value"); - - await nameField.click(); - await nameField.fill("dark.primary"); - - await valueField.click(); - await valueField.fill("red"); - - const submitButton = tokensUpdateCreateModal.getByRole("button", { - name: "Save", - }); - await expect(submitButton).toBeEnabled(); - await submitButton.click(); - - await unfoldTokenTree(tokensSidebar, "color", "dark.primary"); - - await expect(tokensSidebar.getByLabel("primary")).toBeEnabled(); - }); - - test("User cant create regular token with value missing", async ({ - page, - }) => { - const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page); - - const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); - await tokensTabPanel - .getByRole("button", { name: "Add Token: Color" }) - .click(); - - await expect(tokensUpdateCreateModal).toBeVisible(); - - const nameField = tokensUpdateCreateModal.getByLabel("Name"); - const submitButton = tokensUpdateCreateModal.getByRole("button", { - name: "Save", - }); - - // Initially submit button should be disabled - await expect(submitButton).toBeDisabled(); - - // Fill in name but leave value empty - await nameField.click(); - await nameField.fill("primary"); - - // Submit button should remain disabled when value is empty - await expect(submitButton).toBeDisabled(); - }); - - test("User changes color token color while keeping custom color space", async ({ + test("User edits color token color while keeping custom color space", async ({ page, }) => { const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } = @@ -1502,30 +1527,9 @@ test.describe("Tokens - CRUD", () => { await valueSaturationSelector.click({ position: { x: 0, y: 0 } }); await expect(valueField).toHaveValue(/^rgba(.*)$/); }); +}); - test("User duplicate color token", async ({ page }) => { - const { tokensSidebar, tokenContextMenuForToken } = - await setupTokensFile(page); - - await expect(tokensSidebar).toBeVisible(); - - unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); - - const colorToken = tokensSidebar.getByRole("button", { - name: "100", - }); - - await colorToken.click({ button: "right" }); - await expect(tokenContextMenuForToken).toBeVisible(); - - await tokenContextMenuForToken.getByText("Duplicate token").click(); - await expect(tokenContextMenuForToken).not.toBeVisible(); - - await expect( - tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }), - ).toBeVisible(); - }); - +test.describe("Tokens tab - delete", () => { test("User delete color token", async ({ page }) => { const { tokensSidebar, tokenContextMenuForToken } = await setupTokensFile(page); @@ -1546,4 +1550,40 @@ test.describe("Tokens - CRUD", () => { await expect(tokenContextMenuForToken).not.toBeVisible(); await expect(colorToken).not.toBeVisible(); }); + + test("User removes node and all child tokens", async ({ page }) => { + const { tokensSidebar, workspacePage } = await setupTokensFile(page); + + await expect(tokensSidebar).toBeVisible(); + + // Expand color tokens + unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); + + // Verify that the node and child token are visible before deletion + const colorNode = tokensSidebar.getByRole("button", { + name: "blue", + exact: true, + }); + const colorNodeToken = tokensSidebar.getByRole("button", { + name: "100", + }); + + // Select a node and right click on it to open context menu + await expect(colorNode).toBeVisible(); + await expect(colorNodeToken).toBeVisible(); + await colorNode.click({ button: "right" }); + + // select "Delete" from the context menu + const deleteNodeButton = page.getByRole("button", { + name: "Delete", + exact: true, + }); + await expect(deleteNodeButton).toBeVisible(); + await deleteNodeButton.click(); + + // Verify that the node is removed + await expect(colorNode).not.toBeVisible(); + // Verify that child token is also removed + await expect(colorNodeToken).not.toBeVisible(); + }); }); diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs index 25fc451cf8..6be5314d31 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -433,11 +433,22 @@ ptk/WatchEvent (watch [it state _] (let [data (dsh/lookup-file-data state) + changes (-> (pcb/empty-changes it) (pcb/with-library-data data) (pcb/set-token set-id token-id nil))] (rx/of (dch/commit-changes changes)))))) +(defn bulk-delete-tokens + [set-id token-ids] + (dm/assert! (uuid? set-id)) + (dm/assert! (every? uuid? token-ids)) + (ptk/reify ::bulk-delete-tokens + ptk/WatchEvent + (watch [_ _ _] + (apply rx/of + (map #(delete-token set-id %) token-ids))))) + (defn duplicate-token [token-id] (dm/assert! (uuid? token-id)) @@ -505,6 +516,19 @@ (update state :workspace-tokens assoc :token-context-menu params) (update state :workspace-tokens dissoc :token-context-menu))))) +(defn assign-token-node-context-menu + [{:keys [position] :as params}] + + (when params + (assert (gpt/point? position) "expected a point instance for `position` param")) + + (ptk/reify ::show-token-node-context-menu + ptk/UpdateEvent + (update [_ state] + (if params + (update state :workspace-tokens assoc :token-node-context-menu params) + (update state :workspace-tokens dissoc :token-node-context-menu))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TOKEN-SET UI OPS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/ui/ds/layers/layer_button.cljs b/frontend/src/app/main/ui/ds/layers/layer_button.cljs index 759952c30a..315ad56e88 100644 --- a/frontend/src/app/main/ui/ds/layers/layer_button.cljs +++ b/frontend/src/app/main/ui/ds/layers/layer_button.cljs @@ -19,17 +19,19 @@ [:expandable {:optional true} :boolean] [:expanded {:optional true} :boolean] [:icon {:optional true} :string] - [:on-toggle-expand fn?]]) + [:on-toggle-expand {:optional true} fn?] + [:on-context-menu {:optional true} fn?]]) (mf/defc layer-button* {::mf/schema schema:layer-button} - [{:keys [label description class is-expandable expanded icon on-toggle-expand children] :rest props}] + [{:keys [label description class is-expandable expanded icon on-toggle-expand on-context-menu children] :rest props}] (let [button-props (mf/spread-props props {:class [class (stl/css-case :layer-button true :layer-button--expandable is-expandable :layer-button--expanded expanded)] :type "button" - :on-click on-toggle-expand})] + :on-click on-toggle-expand + :on-context-menu on-context-menu})] [:div {:class (stl/css :layer-button-wrapper)} [:> "button" button-props [:div {:class (stl/css :layer-button-content)} diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs index 57028b1bd1..4ac8623f46 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs @@ -14,8 +14,10 @@ [app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]] [app.main.ui.workspace.tokens.management.group :refer [token-group*]] + [app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]] [app.util.array :as array] [app.util.i18n :refer [tr]] + [cuerdas.core :as str] [rumext.v2 :as mf])) (defn- get-sorted-token-groups @@ -120,7 +122,27 @@ [empty-group filled-group] (mf/with-memo [tokens-by-type] - (get-sorted-token-groups tokens-by-type))] + (get-sorted-token-groups tokens-by-type)) + + ;; Filter tokens by their path and return their ids + filter-tokens-by-path-ids + (mf/use-fn + (mf/deps tokens) + (fn [type path] + (->> tokens + (filter (fn [token] + (let [[_ token-value] token] + (and (= (:type token-value) type) (str/starts-with? (:name token-value) path))))) + (mapv (fn [token] + (let [[_ token-value] token] + (:id token-value))))))) + + delete-node + (mf/with-memo [tokens selected-token-set-id] + (fn [node type] + (let [path (:path node) + tokens-in-path-ids (filter-tokens-by-path-ids type path)] + (st/emit! (dwtl/bulk-delete-tokens selected-token-set-id tokens-in-path-ids)))))] (mf/with-effect [tokens-lib selected-token-set-id] (when (and tokens-lib @@ -134,6 +156,7 @@ [:* [:& token-context-menu] + [:> token-node-context-menu* {:on-delete-node delete-node}] [:& selected-set-info* {:tokens-lib tokens-lib :selected-token-set-id selected-token-set-id}] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index b3450e8665..58912a2101 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -88,7 +88,7 @@ expandable? (d/nilv (seq tokens) false) - on-context-menu + on-pill-context-menu (mf/use-fn (fn [event token] (dom/prevent-default event) @@ -98,6 +98,15 @@ :errors (:errors token) :token-id (:id token)})))) + on-node-context-menu + (mf/use-fn + (fn [event node] + (dom/prevent-default event) + (st/emit! (dwtl/assign-token-node-context-menu + {:node node + :type type + :position (dom/get-client-position event)})))) + on-toggle-open-click (mf/use-fn (mf/deps type expandable?) @@ -159,4 +168,5 @@ :selected-token-set-id selected-token-set-id :is-selected-inside-layout is-selected-inside-layout :on-token-pill-click on-token-pill-click - :on-context-menu on-context-menu}])])) + :on-pill-context-menu on-pill-context-menu + :on-node-context-menu on-node-context-menu}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs new file mode 100644 index 0000000000..4e272f7bdd --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs @@ -0,0 +1,83 @@ +(ns app.main.ui.workspace.tokens.management.node-context-menu + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data.macros :as dm] + [app.main.data.workspace.tokens.library-edit :as dwtl] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [okulary.core :as l] + [rumext.v2 :as mf])) + +(def ^:private schema:token-node-context-menu + [:map + [:on-delete-node fn?]]) + +(def ^:private tokens-node-menu-ref + (l/derived :token-node-context-menu refs/workspace-tokens)) + +(defn- prevent-default + [event] + (dom/prevent-default event) + (dom/stop-propagation event)) + +(mf/defc token-node-context-menu* + {::mf/schema schema:token-node-context-menu} + [{:keys [on-delete-node]}] + (let [mdata (mf/deref tokens-node-menu-ref) + is-open? (boolean mdata) + dropdown-ref (mf/use-ref) + dropdown-action (mf/use-ref) + dropdown-direction* (mf/use-state "down") + dropdown-direction (deref dropdown-direction*) + dropdown-direction-change* (mf/use-ref 0) + top (+ (get-in mdata [:position :y]) 5) + left (+ (get-in mdata [:position :x]) 5) + + delete-node (mf/use-fn + (mf/deps mdata) + (fn [] + (let [node (get mdata :node) + type (get mdata :type)] + (when node + (on-delete-node node type)))))] + + (mf/with-effect [is-open?] + (when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?)) + (reset! dropdown-direction* "down") + (mf/set-ref-val! dropdown-direction-change* 0))) + + (mf/with-effect [is-open? dropdown-ref dropdown-action] + (let [dropdown-element (mf/ref-val dropdown-ref)] + (when (and (= 0 (mf/ref-val dropdown-direction-change*)) dropdown-element) + (let [is-outside? (dom/is-element-outside? dropdown-element)] + (reset! dropdown-direction* (if is-outside? "up" "down")) + (mf/set-ref-val! dropdown-direction-change* (inc (mf/ref-val dropdown-direction-change*))))))) + + ;; FIXME: perf optimization + + (when is-open? + (mf/portal + (mf/html + [:& dropdown {:show is-open? + :on-close #(st/emit! (dwtl/assign-token-node-context-menu nil))} + [:div {:class (stl/css :token-node-context-menu) + :data-testid "tokens-context-menu-for-token-node" + :ref dropdown-ref + :data-direction dropdown-direction + :style {:--bottom (if (= dropdown-direction "up") + "40px" + "unset") + :--top (dm/str top "px") + :left (dm/str left "px")} + :on-context-menu prevent-default} + (when mdata + [:ul {:class (stl/css :token-node-context-menu-list)} + [:li {:class (stl/css :token-node-context-menu-listitem)} + [:button {:class (stl/css :token-node-context-menu-action) + :type "button" + :on-click delete-node} + (tr "labels.delete")]]])]]) + (dom/get-body))))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss new file mode 100644 index 0000000000..7e84dfa6d8 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss @@ -0,0 +1,70 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "ds/_utils.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/typography.scss" as t; +@use "ds/spacing.scss" as *; +@use "ds/mixins.scss" as *; + +.token-node-context-menu { + --menu-inline-size: #{px2rem(240)}; + + position: absolute; + z-index: var(--z-index-dropdown); +} + +.token-node-context-menu[data-direction="up"] { + bottom: var(--bottom); +} + +.token-node-context-menu[data-direction="down"] { + top: var(--top); +} + +.token-node-context-menu-list { + inline-size: var(--menu-inline-size); + padding: var(--sp-xs); + border-radius: $br-8; + border: $b-2 solid var(--color-background-quaternary); + background-color: var(--color-background-tertiary); + max-block-size: 100vh; + overflow-y: auto; + box-shadow: 0px 0px $sz-12 0px var(--menu-shadow-color); +} + +.token-node-context-menu-action { + --context-menu-item-bg-color: none; + --context-menu-item-fg-color: var(--color-foreground-primary); + --context-menu-item-border-color: none; + + @include t.use-typography("body-small"); + appearance: none; + background: var(--context-menu-item-bg-color); + border: $b-1 solid var(--context-menu-item-border-color); + color: var(--context-menu-item-fg-color); + border-radius: $br-8; + cursor: pointer; + block-size: px2rem(32); + inline-size: 100%; + display: flex; + align-items: center; + padding: var(--sp-xs); + + &:hover { + --context-menu-item-bg-color: var(--color-background-quaternary); + } + + &:focus { + --context-menu-item-bg-color: var(--menu-background-color-focus); + --context-menu-item-border-color: var(--color-background-tertiary); + } + + &[aria-selected="true"] { + --context-menu-item-bg-color: var(--color-background-quaternary); + } +} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs index e7da275b39..40c1938eee 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs @@ -10,6 +10,7 @@ [app.common.path-names :as cpn] [app.common.types.tokens-lib :as ctob] [app.main.data.workspace.tokens.library-edit :as dwtl] + [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.ds.layers.layer-button :refer [layer-button*]] [app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]] @@ -26,7 +27,8 @@ [:selected-token-set-id {:optional true} :any] [:tokens-lib {:optional true} :any] [:on-token-pill-click {:optional true} fn?] - [:on-context-menu {:optional true} fn?]]) + [:on-pill-context-menu {:optional true} fn?] + [:on-node-context-menu {:optional true} fn?]]) (mf/defc folder-node* {::mf/schema schema:folder-node} @@ -39,22 +41,29 @@ selected-token-set-id tokens-lib on-token-pill-click - on-context-menu]}] + on-pill-context-menu + on-node-context-menu]}] (let [full-path (str (name type) "." (:path node)) is-folder-expanded (contains? (set (or unfolded-token-paths [])) full-path) - swap-folder-expanded (mf/use-fn (mf/deps (:path node) type) (fn [] (let [path (str (name type) "." (:path node))] - (st/emit! (dwtl/toggle-token-path path)))))] + (st/emit! (dwtl/toggle-token-path path))))) + + node-context-menu-prep (mf/use-fn + (mf/deps on-node-context-menu node) + (fn [event] + (when on-node-context-menu + (on-node-context-menu event node))))] [:li {:class (stl/css :folder-node)} [:> layer-button* {:label (:name node) :expanded is-folder-expanded :aria-expanded is-folder-expanded :aria-controls (str "folder-children-" (:path node)) :is-expandable (not (:leaf node)) - :on-toggle-expand swap-folder-expanded}] + :on-toggle-expand swap-folder-expanded + :on-context-menu node-context-menu-prep}] (when is-folder-expanded (let [children-fn (:children-fn node)] [:div {:class (stl/css :folder-children-wrapper) @@ -63,16 +72,17 @@ (let [children (children-fn)] (for [child children] (if (not (:leaf child)) - [:ul {:class (stl/css :node-parent)} - [:> folder-node* {:key (:path child) - :type type + [:ul {:class (stl/css :node-parent) + :key (:path child)} + [:> folder-node* {:type type :node child :unfolded-token-paths unfolded-token-paths :selected-shapes selected-shapes :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens :on-token-pill-click on-token-pill-click - :on-context-menu on-context-menu + :on-pill-context-menu on-pill-context-menu + :on-node-context-menu on-node-context-menu :tokens-lib tokens-lib :selected-token-set-id selected-token-set-id}]] (let [id (:id (:leaf child)) @@ -84,7 +94,7 @@ :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens :on-click on-token-pill-click - :on-context-menu on-context-menu}])))))]))])) + :on-context-menu on-pill-context-menu}])))))]))])) (def ^:private schema:token-tree [:map @@ -97,7 +107,8 @@ [:selected-token-set-id {:optional true} :any] [:tokens-lib {:optional true} :any] [:on-token-pill-click {:optional true} fn?] - [:on-context-menu {:optional true} fn?]]) + [:on-pill-context-menu {:optional true} fn?] + [:on-node-context-menu {:optional true} fn?]]) (mf/defc token-tree* {::mf/schema schema:token-tree} @@ -110,12 +121,19 @@ tokens-lib selected-token-set-id on-token-pill-click - on-context-menu]}] + on-pill-context-menu + on-node-context-menu]}] (let [separator "." tree (mf/use-memo (mf/deps tokens) (fn [] - (cpn/build-tree-root tokens separator)))] + (cpn/build-tree-root tokens separator))) + can-edit? (:can-edit (deref refs/permissions)) + on-node-context-menu (mf/use-fn + (mf/deps can-edit? on-node-context-menu) + (fn [event node] + (when can-edit? + (on-node-context-menu event node))))] [:div {:class (stl/css :token-tree-wrapper)} (for [node tree] (if (:leaf node) @@ -127,7 +145,7 @@ :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens :on-click on-token-pill-click - :on-context-menu on-context-menu}]) + :on-context-menu on-pill-context-menu}]) ;; Render segment folder [:ul {:class (stl/css :node-parent) :key (:path node)} @@ -138,6 +156,7 @@ :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens :on-token-pill-click on-token-pill-click - :on-context-menu on-context-menu + :on-node-context-menu on-node-context-menu + :on-pill-context-menu on-pill-context-menu :tokens-lib tokens-lib :selected-token-set-id selected-token-set-id}]]))]))