From dda3377596ee949125098f03786a3375dba4cd3b Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 5 Feb 2026 11:28:47 +0100 Subject: [PATCH] :bug: Allow detach broken token from input (#8242) * :bug: Allow detach broken token from input * :bug: Fix multiselection on multiple token applied * :recycle: Remove detach-token new fn --- common/src/app/common/files/tokens.cljc | 6 +- .../playwright/ui/specs/tokens/apply.spec.js | 93 ++++++++++++++++++- .../data/workspace/tokens/application.cljs | 7 +- .../main/ui/ds/controls/numeric_input.cljs | 23 +++-- .../sidebar/options/menus/border_radius.cljs | 4 +- .../options/menus/color_selection.cljs | 8 +- .../workspace/sidebar/options/menus/fill.cljs | 10 +- .../sidebar/options/menus/layer.cljs | 4 +- .../options/menus/layout_container.cljs | 8 +- .../sidebar/options/menus/layout_item.cljs | 12 +-- .../sidebar/options/menus/measures.cljs | 4 +- .../sidebar/options/menus/stroke.cljs | 6 +- .../sidebar/options/rows/color_row.cljs | 9 +- .../tokens/management/context_menu.cljs | 31 +++++-- .../logic/components_and_tokens.cljs | 4 +- 15 files changed, 165 insertions(+), 64 deletions(-) diff --git a/common/src/app/common/files/tokens.cljc b/common/src/app/common/files/tokens.cljc index 811eb598b5..6d86bca9ab 100644 --- a/common/src/app/common/files/tokens.cljc +++ b/common/src/app/common/files/tokens.cljc @@ -39,12 +39,12 @@ (into {}))) (defn remove-attributes-for-token - "Removes applied tokens with `token-id` for the given `attributes` set from `applied-tokens`." - [attributes token applied-tokens] + "Removes applied tokens with `token-name` for the given `attributes` set from `applied-tokens`." + [attributes token-name applied-tokens] (let [attr? (set attributes)] (->> (remove (fn [[k v]] (and (attr? k) - (= v (or (token-identifier token) token)))) + (= v token-name))) applied-tokens) (into {})))) diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index 0b9b75c798..2952a23c87 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -831,15 +831,102 @@ test.describe("Tokens: Apply token", () => { }); await detachButton.click(); await expect(marginPillXL).not.toBeVisible(); - const horizontalMarginInput = layoutItemSectionSidebar.getByText('Horizontal marginOpen token'); + const horizontalMarginInput = layoutItemSectionSidebar.getByText( + "Horizontal marginOpen token", + ); await expect(horizontalMarginInput).toBeVisible(); - const tokenDropdown = horizontalMarginInput.getByRole('button', { name: 'Open token list' }); + const tokenDropdown = horizontalMarginInput.getByRole("button", { + name: "Open token list", + }); await tokenDropdown.click(); await expect(dimensionTokenOptionXl).toBeVisible(); await dimensionTokenOptionXl.click(); - + await expect(marginPillXL).toBeVisible(); }); }); + +test.describe("Tokens: Detach token", () => { + test("User applies border-radius token to a shape from sidebar", async ({ + page, + }) => { + const { workspacePage, tokensSidebar, tokenContextMenuForToken } = + await setupTokensFile(page); + + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + + // Open tokens sections on left sidebar + const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); + await tokensTabButton.click(); + + // Unfold border radius tokens + await page.getByRole("button", { name: "Border Radius 3" }).click(); + await expect( + tokensSidebar.getByRole("button", { name: "borderRadius" }), + ).toBeVisible(); + await tokensSidebar.getByRole("button", { name: "borderRadius" }).click(); + await expect( + tokensSidebar.getByRole("button", { name: "borderRadius.sm" }), + ).toBeVisible(); + + // Apply border radius token from token panels + await tokensSidebar + .getByRole("button", { name: "borderRadius.sm" }) + .click(); + + // Check if border radius sections is visible on right sidebar + const borderRadiusSection = page.getByRole("region", { + name: "border-radius-section", + }); + await expect(borderRadiusSection).toBeVisible(); + + // Check if token pill is visible on design tab on right sidebar + const brTokenPillSM = borderRadiusSection.getByRole("button", { + name: "borderRadius.sm", + }); + await expect(brTokenPillSM).toBeVisible(); + await brTokenPillSM.click(); + + // Rename token + await tokensSidebar + .getByRole("button", { name: "borderRadius.sm" }) + .click({ button: "right" }); + await expect(page.getByText("Edit token")).toBeVisible(); + await page.getByText("Edit token").click(); + const editModal = page.getByTestId("token-update-create-modal"); + await expect(editModal).toBeVisible(); + await expect( + editModal.getByRole("textbox", { name: "Name" }), + ).toBeVisible(); + await editModal + .getByRole("textbox", { name: "Name" }) + .fill("BorderRadius.smBis"); + const submitButton = editModal.getByRole("button", { name: "Save" }); + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + await expect(page.getByText("Don't remap")).toBeVisible(); + await page.getByText("Don't remap").click(); + const brokenPill = borderRadiusSection.getByRole("button", { + name: "This token is not in any", + }); + await expect(brokenPill).toBeVisible(); + + // Detach broken token + const detachButton = borderRadiusSection.getByRole("button", { + name: "Detach token", + }); + await detachButton.click(); + await expect(brokenPill).not.toBeVisible(); + + //De-select and select shape again to double check token is detached + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(0).click(); + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + await expect(brokenPill).not.toBeVisible(); + }); +}); diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 012e2aca05..31ff1747d8 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -706,17 +706,18 @@ "Removes `attributes` that match `token` for `shape-ids`. Doesn't update shape attributes." - [{:keys [attributes token shape-ids] :as _props}] + [{:keys [attributes token-name shape-ids] :as _props}] (ptk/reify ::unapply-token ptk/WatchEvent (watch [_ _ _] (rx/of - (let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))] + (let [remove-token #(when % (cft/remove-attributes-for-token attributes token-name %))] (dwsh/update-shapes shape-ids (fn [shape] (update shape :applied-tokens remove-token)))))))) + (defn toggle-token [{:keys [token attrs shape-ids expand-with-children]}] (ptk/reify ::on-toggle-token @@ -746,7 +747,7 @@ (if unapply-tokens? (rx/of (unapply-token {:attributes (or attrs all-attributes attributes) - :token token + :token-name (:name token) :shape-ids shape-ids})) (rx/of (cond diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs index 1f4948995e..e22841ac7d 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -564,18 +564,17 @@ (mf/use-fn (mf/deps on-detach tokens disabled token-applied) (fn [event] - (let [token (get-token-op tokens token-applied)] - (when-not disabled - (dom/prevent-default event) - (dom/stop-propagation event) - (reset! token-applied* nil) - (reset! selected-id* nil) - (reset! focused-id* nil) - (when on-detach - (on-detach token)) - (ts/schedule-on-idle - (fn [] - (dom/focus! (mf/ref-val ref)))))))) + (when-not disabled + (dom/prevent-default event) + (dom/stop-propagation event) + (reset! token-applied* nil) + (reset! selected-id* nil) + (reset! focused-id* nil) + (when on-detach + (on-detach token-applied)) + (ts/schedule-on-idle + (fn [] + (dom/focus! (mf/ref-val ref))))))) on-token-key-down (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs index df12f90a41..7daff9923a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs @@ -60,8 +60,8 @@ on-detach-token (mf/use-fn (mf/deps ids) - (fn [token attr] - (st/emit! (dwta/unapply-token {:token (first token) + (fn [token-name attr] + (st/emit! (dwta/unapply-token {:token-name token-name :attributes #{attr} :shape-ids ids})))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index 652de6ca23..6a0862379a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -152,9 +152,9 @@ on-detach-token (mf/use-fn (mf/deps token-colors groups) - (fn [token] + (fn [token-name] (let [prev-colors (mf/ref-val prev-colors-ref) - token-color (some #(when (= (:token-name %) (:name token)) %) token-colors) + token-color (some #(when (= (:token-name %) token-name) %) token-colors) [color-operations _] (retrieve-color-operations groups token-color prev-colors)] (doseq [op color-operations] @@ -166,8 +166,8 @@ (d/without-nils))] (mf/set-ref-val! prev-colors-ref (conj prev-colors color)) - (st/emit! (dwta/unapply-token {:attributes attr - :token token + (st/emit! (dwta/unapply-token {:token-name token-name + :attributes attr :shape-ids [(:shape-id op)]}))))))) select-only diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index c521868519..190c53645f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -74,7 +74,6 @@ render-wasm? (feat/use-feature "render-wasm/v1") - ^boolean multiple? (= :multiple fills) @@ -183,9 +182,9 @@ on-detach-token (mf/use-fn (mf/deps ids) - (fn [token] - (st/emit! (dwta/unapply-token {:attributes #{:fill} - :token token + (fn [token-name] + (st/emit! (dwta/unapply-token {:token-name token-name + :attributes #{:fill} :shape-ids ids}))))] (mf/with-layout-effect [hide-on-export] @@ -215,7 +214,8 @@ (when open? [:div {:class (stl/css :fill-content)} (cond - (= :multiple fills) + (or (= :multiple fills) + (= :multiple fill-token-applied)) [:div {:class (stl/css :fill-multiple)} [:div {:class (stl/css :fill-multiple-label)} (tr "settings.multiple")] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs index 486fe1bb26..0372a861df 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs @@ -72,8 +72,8 @@ on-detach-token (mf/use-fn (mf/deps ids) - (fn [token attr] - (st/emit! (dwta/unapply-token {:token (first token) + (fn [token-name attr] + (st/emit! (dwta/unapply-token {:token-name token-name :attributes #{attr} :shape-ids ids})))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 53875959c6..ed6187e73f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -339,8 +339,8 @@ on-detach-token (mf/use-fn (mf/deps ids) - (fn [token attr] - (st/emit! (dwta/unapply-token {:token (first token) + (fn [token-name attr] + (st/emit! (dwta/unapply-token {:token-name token-name :attributes #{attr} :shape-ids ids})))) @@ -475,7 +475,7 @@ (mf/use-fn (mf/deps ids) (fn [token attr] - (st/emit! (dwta/unapply-token {:token (first token) + (st/emit! (dwta/unapply-token {:token-name token :attributes #{attr} :shape-ids ids})))) @@ -722,7 +722,7 @@ (mf/use-fn (mf/deps ids) (fn [token attr] - (st/emit! (dwta/unapply-token {:token (first token) + (st/emit! (dwta/unapply-token {:token-name token :attributes #{attr} :shape-ids ids})))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs index af250cc856..c7c2bd9193 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -97,8 +97,8 @@ on-detach-token (mf/use-fn (mf/deps ids) - (fn [token attr] - (st/emit! (dwta/unapply-token {:token (first token) + (fn [token-name attr] + (st/emit! (dwta/unapply-token {:token-name token-name :attributes #{attr} :shape-ids ids})))) @@ -220,8 +220,8 @@ on-detach-token (mf/use-fn (mf/deps ids) - (fn [token attr] - (st/emit! (dwta/unapply-token {:token (first token) + (fn [token-name attr] + (st/emit! (dwta/unapply-token {:token-name token-name :attributes #{attr} :shape-ids ids})))) @@ -550,8 +550,8 @@ on-detach-token (mf/use-fn (mf/deps ids) - (fn [token attr] - (st/emit! (dwta/unapply-token {:token (first token) + (fn [token-name attr] + (st/emit! (dwta/unapply-token {:token-name token-name :attributes #{attr} :shape-ids ids})))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index e6aaa91a37..723a5b5b17 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -319,8 +319,8 @@ on-detach-token (mf/use-fn (mf/deps ids) - (fn [token attr] - (st/emit! (dwta/unapply-token {:token (first token) + (fn [token-name attr] + (st/emit! (dwta/unapply-token {:token-name token-name :attributes #{attr} :shape-ids ids})))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 46befbc2d4..c35bac471e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -171,9 +171,9 @@ on-detach-token (mf/use-fn (mf/deps ids) - (fn [token attrs] - (st/emit! (dwta/unapply-token {:attributes attrs - :token token + (fn [token-name attrs] + (st/emit! (dwta/unapply-token {:token-name token-name + :attributes attrs :shape-ids ids}))))] [:section {:class (stl/css :stroke-section) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 026a5b8ebd..afe72d64fe 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -85,14 +85,14 @@ (mf/use-fn (mf/deps detach-token token applied-token-name) (fn [] - (let [token (or token applied-token-name)] - (detach-token token)))) + (let [token-name (or (:name token) applied-token-name)] + (detach-token token-name)))) has-errors (some? (:errors token)) token-name (:name token) resolved (:resolved-value token) - not-active (and (empty? active-tokens) - (nil? token)) + not-active (or (empty? active-tokens) + (nil? token)) id (dm/str (:id token) "-name") swatch-tooltip-content (cond not-active @@ -344,7 +344,6 @@ (mf/with-effect [color prev-color disable-picker] (when (and (not disable-picker) (not= prev-color color)) (modal/update-props! :colorpicker {:data (parse-color color)}))) - [:div {:class [class row-class]} ;; Drag handler (when (some? on-reorder) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index ef1ccb0128..d4470f63f0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -64,14 +64,17 @@ (let [selected? (selected-pred attribute) props {:attributes #{attribute} :token token - :shape-ids shape-ids}] + :shape-ids shape-ids} + unnaply-props {:token-name (:name token) + :attributes #{attribute} + :shape-ids shape-ids}] {:title title :hint hint :selected? selected? :action (fn [] (if selected? - (st/emit! (dwta/unapply-token props)) + (st/emit! (dwta/unapply-token unnaply-props)) (st/emit! (dwta/apply-token (assoc props :on-update-shape on-update-shape-fn)))))})) allowed-attributes))) @@ -82,12 +85,15 @@ {:keys [all-selected? selected-pred shape-ids]} (attribute-actions token selected-shapes attributes) all-action (let [props {:attributes attributes :token token - :shape-ids shape-ids}] + :shape-ids shape-ids} + unnaply-props {:token-name (:name token) + :attributes attributes + :shape-ids shape-ids}] {:title (tr "labels.all") :selected? all-selected? :hint hint :action #(if all-selected? - (st/emit! (dwta/unapply-token props)) + (st/emit! (dwta/unapply-token unnaply-props)) (st/emit! (dwta/apply-token (assoc props :on-update-shape (or on-update-shape-all on-update-shape)))))}) single-actions (map (fn [[attr title]] (let [selected? (selected-pred attr)] @@ -96,10 +102,13 @@ :action #(let [props {:attributes #{attr} :token token :shape-ids shape-ids} + unnaply-props {:token-name (:name token) + :attributes #{attr} + :shape-ids shape-ids} event (cond all-selected? (-> (assoc props :attributes-to-remove attributes) (dwta/apply-token)) - selected? (dwta/unapply-token props) + selected? (dwta/unapply-token unnaply-props) :else (-> (assoc props :on-update-shape on-update-shape) (dwta/apply-token)))] (st/emit! event))})) @@ -123,9 +132,12 @@ :action (fn [] (let [props {:attributes attrs :token token - :shape-ids shape-ids}] + :shape-ids shape-ids} + unnaply-props {:token-name (:name token) + :attributes attrs + :shape-ids shape-ids}] (if all-selected? - (st/emit! (dwta/unapply-token props)) + (st/emit! (dwta/unapply-token unnaply-props)) (st/emit! (dwta/apply-token (assoc props :on-update-shape on-update-shape))))))} {:title "Horizontal" :selected? horizontal-selected? @@ -165,10 +177,13 @@ :action #(let [props {:attributes #{attr} :token token :shape-ids shape-ids} + unnaply-props {:token-name (:name token) + :attributes #{attr} + :shape-ids shape-ids} event (cond all-selected? (-> (assoc props :attributes-to-remove attrs) (dwta/apply-token)) - selected? (dwta/unapply-token props) + selected? (dwta/unapply-token unnaply-props) :else (-> (assoc props :on-update-shape on-update-shape) (dwta/apply-token)))] (st/emit! event))})) diff --git a/frontend/test/frontend_tests/logic/components_and_tokens.cljs b/frontend/test/frontend_tests/logic/components_and_tokens.cljs index 9682403013..689ac76b63 100644 --- a/frontend/test/frontend_tests/logic/components_and_tokens.cljs +++ b/frontend/test/frontend_tests/logic/components_and_tokens.cljs @@ -177,7 +177,7 @@ ;; ==== Action events [(dwta/unapply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} - :token (toht/get-token file "test-token-1")})] + :token-name "test-token-1"})] step2 (fn [_] (let [events2 [(dwl/sync-file (:id file) (:id file))]] @@ -289,7 +289,7 @@ ;; ==== Action events [(dwta/unapply-token {:shape-ids [(cthi/id :c-frame1)] :attributes #{:r1 :r2 :r3 :r4} - :token (toht/get-token file "test-token-1")}) + :token-name "test-token-1"}) (dwta/apply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-3")