diff --git a/CHANGES.md b/CHANGES.md index 0e7b48fa22..cff6c16817 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ ### :sparkles: New features & Enhancements +- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202) + ### :bug: Bugs fixed diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 5ee3661a91..df9e6f7b0a 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -47,6 +47,18 @@ self-reference? (get token-references token-name)] self-reference?)) +(defn references-token? + "Recursively check if a value references the token name. Handles strings, maps, and sequences." + [value token-name] + (cond + (string? value) + (boolean (some #(= % token-name) (find-token-value-references value))) + (map? value) + (some true? (map #(references-token? % token-name) (vals value))) + (sequential? value) + (some true? (map #(references-token? % token-name) value)) + :else false)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SCHEMA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -558,3 +570,18 @@ "Predicate if a shadow composite token is a reference value - a string pointing to another reference token." [token-value] (string? token-value)) + +(defn update-token-value-references + "Recursively update token references within a token value, supporting complex token values (maps, sequences, strings)." + [value old-name new-name] + (cond + (string? value) + (str/replace value + (re-pattern (str "\\{" (str/replace old-name "." "\\.") "\\}")) + (str "{" new-name "}")) + (map? value) + (d/update-vals value #(update-token-value-references % old-name new-name)) + (sequential? value) + (mapv #(update-token-value-references % old-name new-name) value) + :else + value)) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 5e278588a7..89d9eb2032 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -909,7 +909,8 @@ Will return a value that matches this schema: `:all` All of the nested sets are active `:partial` Mixed active state of nested sets") (get-tokens-in-active-sets [_] "set of set names that are active in the the active themes") - (get-all-tokens [_] "all tokens in the lib") + (get-all-tokens [_] "all tokens in the lib, as a sequence") + (get-all-tokens-map [_] "all tokens in the lib, as a map name -> token") (get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name")) (declare parse-multi-set-dtcg-json) @@ -1306,6 +1307,10 @@ Will return a value that matches this schema: tokens)) (get-all-tokens [this] + (mapcat #(vals (get-tokens- %)) + (get-sets this))) + + (get-all-tokens-map [this] (reduce (fn [tokens' set] (into tokens' (map (fn [x] [(:name x) x]) (vals (get-tokens- set))))) diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index 3ad2dd2b67..90cbefd5a7 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -2740,3 +2740,639 @@ test.describe("Tokens: Apply token", () => { }); }); }); + +test.describe("Tokens: Remapping Feature", () => { + test.describe("Box Shadow Token Remapping", () => { + test("User renames box shadow token with alias references", async ({ + page, + }) => { + const { + tokensUpdateCreateModal, + tokensSidebar, + tokenContextMenuForToken, + } = await setupTokensFile(page, { flags: ["enable-token-shadow"] }); + + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + // Create base shadow token + await tokensTabPanel + .getByRole("button", { name: "Add Token: Shadow" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + let nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("base-shadow"); + + const colorField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Color", + }); + await colorField.fill("#000000"); + + let submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Create derived shadow token that references base-shadow + await tokensTabPanel + .getByRole("button", { name: "Add Token: Shadow" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("derived-shadow"); + + const referenceToggle = + tokensUpdateCreateModal.getByTestId("reference-opt"); + await referenceToggle.click(); + + const referenceField = tokensUpdateCreateModal.getByLabel("Reference"); + await referenceField.fill("{base-shadow}"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Rename base-shadow token + const baseToken = tokensSidebar.getByRole("button", { + name: "base-shadow", + }); + await baseToken.click({ button: "right" }); + await tokenContextMenuForToken.getByText("Edit token").click(); + + await expect(tokensUpdateCreateModal).toBeVisible(); + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("foundation-shadow"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + + // Check for remapping modal + const remappingModal = page.getByTestId("token-remapping-modal"); + await expect(remappingModal).toBeVisible({ timeout: 5000 }); + await expect(remappingModal).toContainText("1"); + + const confirmButton = remappingModal.getByRole("button", { + name: /remap/i, + }); + await confirmButton.click(); + + // Verify token was renamed + await expect( + tokensSidebar.getByRole("button", { name: "foundation-shadow" }), + ).toBeVisible(); + await expect( + tokensSidebar.getByRole("button", { name: "derived-shadow" }), + ).toBeVisible(); + }); + + test("User renames and updates shadow token - referenced token and applied shapes update", async ({ + page, + }) => { + const { + tokensUpdateCreateModal, + tokensSidebar, + tokenContextMenuForToken, + workspacePage, + } = await setupTokensFile(page, { flags: ["enable-token-shadow"] }); + + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + // Create base shadow token + await tokensTabPanel + .getByRole("button", { name: "Add Token: Shadow" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + let nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("primary-shadow"); + + let colorField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Color", + }); + await colorField.fill("#000000"); + + let submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Create derived shadow token that references base + await tokensTabPanel + .getByRole("button", { name: "Add Token: Shadow" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("card-shadow"); + + const referenceToggle = + tokensUpdateCreateModal.getByTestId("reference-opt"); + await referenceToggle.click(); + + const referenceField = tokensUpdateCreateModal.getByLabel("Reference"); + await referenceField.fill("{primary-shadow}"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Apply the referenced token to a shape + await page.getByRole("tab", { name: "Layers" }).click(); + await workspacePage.layers + .getByTestId("layer-row") + .filter({ hasText: "Button" }) + .click(); + + await page.getByRole("tab", { name: "Tokens" }).click(); + const cardShadowToken = tokensSidebar.getByRole("button", { + name: "card-shadow", + }); + await cardShadowToken.click(); + + // Rename and update value of base token + const primaryToken = tokensSidebar.getByRole("button", { + name: "primary-shadow", + }); + await primaryToken.click({ button: "right" }); + await tokenContextMenuForToken.getByText("Edit token").click(); + + await expect(tokensUpdateCreateModal).toBeVisible(); + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("main-shadow"); + + // Update the color value + colorField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Color", + }); + await colorField.fill("#FF0000"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + + // Confirm remapping + const remappingModal = page.getByTestId("token-remapping-modal"); + await expect(remappingModal).toBeVisible({ timeout: 5000 }); + + const confirmButton = remappingModal.getByRole("button", { + name: /remap/i, + }); + await confirmButton.click(); + + // Verify base token was renamed + await expect( + tokensSidebar.getByRole("button", { name: "main-shadow" }), + ).toBeVisible(); + + // Verify referenced token still exists + await expect( + tokensSidebar.getByRole("button", { name: "card-shadow" }), + ).toBeVisible(); + + // Verify the shape still has the token applied with the NEW name + await page.getByRole("tab", { name: "Layers" }).click(); + await workspacePage.layers + .getByTestId("layer-row") + .filter({ hasText: "Button" }) + .click(); + + // Verify the shape still has the shadow applied with the UPDATED color value + // Expand the shadow section to access the color field + const shadowSection = workspacePage.rightSidebar.getByText("Drop shadow"); + await expect(shadowSection).toBeVisible(); + + // Click to expand the shadow options (the menu button) + const shadowMenuButton = workspacePage.rightSidebar + .getByRole("button", { name: "open more options" }) + .first(); + await shadowMenuButton.click(); + // Wait for the advanced options to appear + await page.waitForTimeout(500); + + // Verify the color value has updated from #000000 to #FF0000 + // Find the color input - it should be a textbox with a 6-character hex value + // We look for all textboxes and find the one with a hex color pattern + const allInputs = await workspacePage.rightSidebar + .locator('input[type="text"]') + .all(); + + let colorInput = null; + for (const input of allInputs) { + const value = await input.inputValue().catch(() => ''); + if (/^[A-Fa-f0-9]{6}$/.test(value)) { + colorInput = input; + break; + } + } + + expect(colorInput).not.toBeNull(); + const colorValue = await colorInput.inputValue(); + expect(colorValue.toUpperCase()).toBe("FF0000"); + }); + }); + + test.describe("Typography Token Remapping", () => { + test("User renames typography token with alias references", async ({ + page, + }) => { + const { + tokensUpdateCreateModal, + tokensSidebar, + tokenContextMenuForToken, + } = await setupTypographyTokensFile(page); + + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + // Create base typography token + await tokensTabPanel + .getByRole("button", { name: "Add Token: Typography" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + let nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("base-text"); + + const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Font size", + }); + await fontSizeField.fill("16"); + + let submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Create derived typography token + await tokensTabPanel + .getByRole("button", { name: "Add Token: Typography" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("body-text"); + + const referenceToggle = + tokensUpdateCreateModal.getByTestId("reference-opt"); + await referenceToggle.click(); + + const referenceField = tokensUpdateCreateModal.getByLabel("Reference"); + await referenceField.fill("{base-text}"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Rename base token + const baseToken = tokensSidebar.getByRole("button", { + name: "base-text", + }); + await baseToken.click({ button: "right" }); + await tokenContextMenuForToken.getByText("Edit token").click(); + + await expect(tokensUpdateCreateModal).toBeVisible(); + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("default-text"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + + // Check for remapping modal + const remappingModal = page.getByTestId("token-remapping-modal"); + await expect(remappingModal).toBeVisible({ timeout: 5000 }); + + const confirmButton = remappingModal.getByRole("button", { + name: /remap/i, + }); + await confirmButton.click(); + + // Verify token was renamed + await expect( + tokensSidebar.getByRole("button", { name: "default-text" }), + ).toBeVisible(); + await expect( + tokensSidebar.getByRole("button", { name: "body-text" }), + ).toBeVisible(); + }); + + test("User renames and updates typography token - referenced token and applied shapes update", async ({ + page, + }) => { + const { + tokensUpdateCreateModal, + tokensSidebar, + tokenContextMenuForToken, + workspacePage, + } = await setupTypographyTokensFile(page); + + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + // Create base typography token + await tokensTabPanel + .getByRole("button", { name: "Add Token: Typography" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + let nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("body-style"); + + let fontSizeField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Font size", + }); + await fontSizeField.fill("16"); + + let submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Create derived typography token + await tokensTabPanel + .getByRole("button", { name: "Add Token: Typography" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("paragraph-style"); + + const referenceToggle = + tokensUpdateCreateModal.getByTestId("reference-opt"); + await referenceToggle.click(); + + const referenceField = tokensUpdateCreateModal.getByLabel("Reference"); + await referenceField.fill("{body-style}"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Apply the referenced token to a text shape + await page.getByRole("tab", { name: "Layers" }).click(); + await workspacePage.layers + .getByTestId("layer-row") + .filter({ hasText: "Some Text" }) + .click(); + + await page.getByRole("tab", { name: "Tokens" }).click(); + const paragraphToken = tokensSidebar.getByRole("button", { + name: "paragraph-style", + }); + await paragraphToken.click(); + + // Rename and update value of base token + const bodyToken = tokensSidebar.getByRole("button", { + name: "body-style", + }); + await bodyToken.click({ button: "right" }); + await tokenContextMenuForToken.getByText("Edit token").click(); + + await expect(tokensUpdateCreateModal).toBeVisible(); + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("text-base"); + + // Update the font size value + fontSizeField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Font size", + }); + await fontSizeField.fill("18"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + + // Confirm remapping + const remappingModal = page.getByTestId("token-remapping-modal"); + await expect(remappingModal).toBeVisible({ timeout: 5000 }); + + const confirmButton = remappingModal.getByRole("button", { + name: /remap/i, + }); + await confirmButton.click(); + + // Verify base token was renamed + await expect( + tokensSidebar.getByRole("button", { name: "text-base" }), + ).toBeVisible(); + + // Verify referenced token still exists + await expect( + tokensSidebar.getByRole("button", { name: "paragraph-style" }), + ).toBeVisible(); + + // Verify the text shape still has the token applied with NEW name and value + await page.getByRole("tab", { name: "Layers" }).click(); + await workspacePage.layers + .getByTestId("layer-row") + .filter({ hasText: "Some Text" }) + .click(); + + // Verify the shape shows the updated font size value (18) + // This proves the remapping worked and the value update propagated through the reference + const fontSizeInput = workspacePage.rightSidebar.getByRole("textbox", { + name: "Font Size", + }); + await expect(fontSizeInput).toBeVisible(); + await expect(fontSizeInput).toHaveValue("18"); + }); + }); + + test.describe("Border Radius Token Remapping", () => { + test("User renames border radius token with alias references", async ({ + page, + }) => { + const { + tokensUpdateCreateModal, + tokensSidebar, + tokenContextMenuForToken, + } = await setupTokensFile(page); + + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + // Create base border radius token + await tokensTabPanel + .getByRole("button", { name: "Add Token: Border Radius" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + let nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("base-radius"); + + const valueField = tokensUpdateCreateModal.getByLabel("Value"); + await valueField.fill("4"); + + let submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Create derived border radius token + await tokensTabPanel + .getByRole("button", { name: "Add Token: Border Radius" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("card-radius"); + + const valueField2 = tokensUpdateCreateModal.getByLabel("Value"); + await valueField2.fill("{base-radius}"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Rename base token + const baseToken = tokensSidebar.getByRole("button", { + name: "base-radius", + }); + await baseToken.click({ button: "right" }); + await tokenContextMenuForToken.getByText("Edit token").click(); + + await expect(tokensUpdateCreateModal).toBeVisible(); + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("primary-radius"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + + // Check for remapping modal + const remappingModal = page.getByTestId("token-remapping-modal"); + await expect(remappingModal).toBeVisible({ timeout: 5000 }); + + const confirmButton = remappingModal.getByRole("button", { + name: /remap/i, + }); + await confirmButton.click(); + + // Verify token was renamed + await expect( + tokensSidebar.getByRole("button", { name: "primary-radius" }), + ).toBeVisible(); + await expect( + tokensSidebar.getByRole("button", { name: "card-radius" }), + ).toBeVisible(); + }); + + test("User renames and updates border radius token - referenced token updates", async ({ + page, + }) => { + const { + tokensUpdateCreateModal, + tokensSidebar, + tokenContextMenuForToken, + } = await setupTokensFile(page); + + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + // Create base border radius token + await tokensTabPanel + .getByRole("button", { name: "Add Token: Border Radius" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + let nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("radius-sm"); + + let valueField = tokensUpdateCreateModal.getByLabel("Value"); + await valueField.fill("4"); + + let submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Create derived border radius token + await tokensTabPanel + .getByRole("button", { name: "Add Token: Border Radius" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("button-radius"); + + const valueField2 = tokensUpdateCreateModal.getByLabel("Value"); + await valueField2.fill("{radius-sm}"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Rename and update value of base token + const radiusToken = tokensSidebar.getByRole("button", { + name: "radius-sm", + }); + await radiusToken.click({ button: "right" }); + await tokenContextMenuForToken.getByText("Edit token").click(); + + await expect(tokensUpdateCreateModal).toBeVisible(); + nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill("radius-base"); + + // Update the value + valueField = tokensUpdateCreateModal.getByLabel("Value"); + await valueField.fill("8"); + + submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + + // Confirm remapping + const remappingModal = page.getByTestId("token-remapping-modal"); + await expect(remappingModal).toBeVisible({ timeout: 5000 }); + + const confirmButton = remappingModal.getByRole("button", { + name: /remap/i, + }); + await confirmButton.click(); + + // Verify base token was renamed + await expect( + tokensSidebar.getByRole("button", { name: "radius-base" }), + ).toBeVisible(); + + // Verify referenced token still exists + await expect( + tokensSidebar.getByRole("button", { name: "button-radius" }), + ).toBeVisible(); + + // Verify the referenced token now points to the renamed token + // by opening it and checking the reference + const buttonRadiusToken = tokensSidebar.getByRole("button", { + name: "button-radius", + }); + await buttonRadiusToken.click({ button: "right" }); + await tokenContextMenuForToken.getByText("Edit token").click(); + + await expect(tokensUpdateCreateModal).toBeVisible(); + const currentValue = tokensUpdateCreateModal.getByLabel("Value"); + await expect(currentValue).toHaveValue("{radius-base}"); + }); + }); +}); diff --git a/frontend/src/app/main/data/workspace/tokens/import_export.cljs b/frontend/src/app/main/data/workspace/tokens/import_export.cljs index e7f872243d..7370da7921 100644 --- a/frontend/src/app/main/data/workspace/tokens/import_export.cljs +++ b/frontend/src/app/main/data/workspace/tokens/import_export.cljs @@ -74,7 +74,7 @@ (when unknown-tokens (st/emit! (show-unknown-types-warning unknown-tokens))) (try - (->> (ctob/get-all-tokens tokens-lib) + (->> (ctob/get-all-tokens-map tokens-lib) (sd/resolve-tokens-with-verbose-errors) (rx/map (fn [_] tokens-lib)) diff --git a/frontend/src/app/main/data/workspace/tokens/remapping.cljs b/frontend/src/app/main/data/workspace/tokens/remapping.cljs new file mode 100644 index 0000000000..be9a1290c9 --- /dev/null +++ b/frontend/src/app/main/data/workspace/tokens/remapping.cljs @@ -0,0 +1,177 @@ +;; 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 + +(ns app.main.data.workspace.tokens.remapping + "Core logic for token remapping functionality" + (:require + [app.common.files.changes-builder :as pcb] + [app.common.files.tokens :as cft] + [app.common.logging :as log] + [app.common.types.container :refer [shapes-seq]] + [app.common.types.file :refer [object-containers-seq]] + [app.common.types.token :as cto] + [app.common.types.tokens-lib :as ctob] + [app.main.data.changes :as dch] + [app.main.data.helpers :as dh] + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [potok.v2.core :as ptk])) + +;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default +(log/set-level! :warn) + +;; Token Reference Scanning +;; ======================== + +(defn scan-shape-applied-tokens + "Scan a shape for applied token references to a specific token name" + [shape token-name container] + (when-let [applied-tokens (:applied-tokens shape)] + (for [[attribute applied-token-name] applied-tokens + :when (= applied-token-name token-name)] + {:type :applied-token + :shape-id (:id shape) + :attribute attribute + :token-name applied-token-name + :container container}))) + +(defn scan-token-value-references + "Scan a token value for references to a specific token name (alias), supporting complex token values." + [token token-name] + (letfn [(find-all-token-value-references [token-value] + (cond + (string? token-value) + (filter #(= % token-name) (cto/find-token-value-references token-value)) + + (map? token-value) + (mapcat find-all-token-value-references (vals token-value)) + + (sequential? token-value) + (mapcat find-all-token-value-references token-value) + + :else + []))] + (when-let [value (:value token)] + (for [referenced-token-name (find-all-token-value-references value)] + {:type :token-alias + :source-token-name (:name token) + :referenced-token-name referenced-token-name})))) + +(defn scan-workspace-token-references + "Scan entire workspace for all token references to a specific token" + [file-data old-token-name] + (let [tokens-lib (:tokens-lib file-data) + containers (object-containers-seq file-data) + + ;; Scan all shapes for applied token references to the specific token + matching-applied (mapcat (fn [container] + (let [shapes (shapes-seq container)] + (mapcat #(scan-shape-applied-tokens % old-token-name container) shapes))) + containers) + + ;; Scan tokens library for alias references to the specific token + matching-aliases (if tokens-lib + (let [all-tokens (ctob/get-all-tokens tokens-lib)] + (mapcat #(scan-token-value-references % old-token-name) all-tokens)) + [])] + (log/info :hint "token-scan-details" + :token-name old-token-name + :containers-count (count containers) + :total-applied-refs (count matching-applied) + :matching-applied (count matching-applied) + :total-alias-refs (count matching-aliases) + :matching-aliases (count matching-aliases)) + + {:applied-tokens matching-applied + :token-aliases matching-aliases + :total-references (+ (count matching-applied) (count matching-aliases))})) + +;; Token Remapping Core Logic +;; ========================== + +(defn remap-tokens + "Main function to remap all token references when a token name changes" + [old-token-name new-token-name] + (ptk/reify ::remap-tokens + ptk/WatchEvent + (watch [_ state _] + (let [file-data (dh/lookup-file-data state) + scan-results (scan-workspace-token-references file-data old-token-name) + tokens-lib (:tokens-lib file-data) + sets (ctob/get-sets tokens-lib) + tokens-with-sets (mapcat (fn [set] + (map (fn [token] + {:token token :set set}) + (vals (ctob/get-tokens tokens-lib (ctob/get-id set))))) + sets) + + ;; Group applied token references by container + refs-by-container (group-by :container (:applied-tokens scan-results)) + + ;; Use apply-token logic to update shapes for both direct and alias references + shape-changes (reduce-kv + (fn [changes container refs] + (let [shape-ids (map :shape-id refs) + ;; Find the correct token to apply (new or alias) + token (or (some #(when (= (:name (:token %)) new-token-name) %) tokens-with-sets) + (some #(when (= (:name (:token %)) old-token-name) %) tokens-with-sets)) + attributes (set (map :attribute refs))] + (if token + (-> (pcb/with-container changes container) + (pcb/update-shapes shape-ids + (fn [shape] + (update shape :applied-tokens + #(merge % (cft/attributes-map attributes (:token token))))))) + changes))) + (-> (pcb/empty-changes) + (pcb/with-file-data file-data) + (pcb/with-library-data file-data)) + refs-by-container) + + ;; Create changes for updating token alias references + token-changes (reduce + (fn [changes ref] + (let [source-token-name (:source-token-name ref)] + (when-let [{:keys [token set]} (some #(when (= (:name (:token %)) source-token-name) %) tokens-with-sets)] + (let [old-value (:value token) + new-value (cto/update-token-value-references old-value old-token-name new-token-name)] + (pcb/set-token changes (ctob/get-id set) (:id token) + (assoc token :value new-value)))))) + shape-changes + (:token-aliases scan-results))] + + (log/info :hint "token-remapping" + :old-name old-token-name + :new-name new-token-name + :references-count (:total-references scan-results)) + + (rx/of (dch/commit-changes token-changes)))))) + +(defn validate-token-remapping + "Validate that a token remapping operation is safe to perform" + [old-name new-name] + (cond + (str/blank? new-name) + {:valid? false + :error :invalid-name + :message "Token name cannot be empty"} + (= old-name new-name) + {:valid? false + :error :no-change + :message "New name is the same as current name"} + :else + {:valid? true})) + +(defn count-token-references + "Count the number of references to a token in the workspace" + [file-data token-name] + (let [scan-results (scan-workspace-token-references file-data token-name)] + (log/info :hint "token-reference-scan" + :token-name token-name + :applied-refs (count (:applied-tokens scan-results)) + :alias-refs (count (:token-aliases scan-results)) + :total (:total-references scan-results)) + (:total-references scan-results))) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 0b44af4511..06519c5d75 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -36,6 +36,7 @@ [app.main.ui.workspace.tokens.import] [app.main.ui.workspace.tokens.import.modal] [app.main.ui.workspace.tokens.management.forms.modals] + [app.main.ui.workspace.tokens.remapping-modal] [app.main.ui.workspace.tokens.settings] [app.main.ui.workspace.tokens.themes.create-modal] [app.main.ui.workspace.viewport :refer [viewport*]] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs index 70797979c6..0d968beab0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs @@ -37,6 +37,7 @@ props (mf/spread-props props {:token-type token-type :tokens-tree-in-selected-set tokens-tree-in-selected-set + :tokens-in-selected-set tokens-in-selected-set :token token}) text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")}) text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")}) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index cb1f3a1902..55b2933a77 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -12,19 +12,21 @@ [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob] [app.main.constants :refer [max-input-length]] + [app.main.data.helpers :as dh] [app.main.data.modal :as modal] [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.data.workspace.tokens.propagation :as dwtp] + [app.main.data.workspace.tokens.remapping :as remap] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.foundations.typography.heading :refer [heading*]] - [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] [app.main.ui.forms :as fc] [app.main.ui.workspace.tokens.management.forms.controls :as token.controls] [app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]] + [app.main.ui.workspace.tokens.remapping-modal :as remapping-modal] [app.util.dom :as dom] [app.util.forms :as fm] [app.util.i18n :refer [tr]] @@ -92,6 +94,7 @@ initial type value-subfield + tokens-in-selected-set input-value-placeholder] :as props}] (let [make-schema (or make-schema default-make-schema) @@ -121,11 +124,11 @@ (mf/deref refs/workspace-active-theme-sets-tokens) tokens - (mf/with-memo [tokens token] + (mf/with-memo [tokens tokens-in-selected-set token] ;; Ensure that the resolved value uses the currently editing token ;; even if the name has been overriden by a token with the same name ;; in another set below. - (cond-> tokens + (cond-> (merge tokens tokens-in-selected-set) (and (:name token) (:value token)) (assoc (:name token) token))) @@ -144,10 +147,6 @@ (fm/use-form :schema schema :initial initial) - warning-name-change? - (not= (get-in @form [:data :name]) - (:name initial)) - on-cancel (mf/use-fn (fn [e] @@ -191,19 +190,38 @@ :tokens tokens}) (rx/subs! (fn [valid-token] - (st/emit! - (if is-create - (dwtl/create-token (ctob/make-token {:name name - :type token-type - :value (:value valid-token) - :description description})) - - (dwtl/update-token (:id token) - {:name name - :value (:value valid-token) - :description description})) - (dwtp/propagate-workspace-tokens) - (modal/hide))))))))] + (let [state @st/state + file-data (dh/lookup-file-data state) + old-name (:name token) + is-rename (and (= action "edit") (not= name old-name)) + references-count (remap/count-token-references file-data old-name)] + (if (and is-rename (> references-count 0)) + (remapping-modal/show-remapping-modal + {:old-token-name old-name + :new-token-name name + :references-count references-count + :on-confirm (fn [] + (st/emit! + (dwtl/update-token (:id token) + {:name name + :value (:value valid-token) + :description description}) + (remap/remap-tokens old-name name) + (dwtp/propagate-workspace-tokens) + (modal/hide!))) + :on-cancel #(modal/hide!)}) + (st/emit! + (if is-create + (dwtl/create-token (ctob/make-token {:name name + :type token-type + :value (:value valid-token) + :description description})) + (dwtl/update-token (:id token) + {:name name + :value (:value valid-token) + :description description})) + (dwtp/propagate-workspace-tokens) + (modal/hide!))))))))))] [:> fc/form* {:class (stl/css :form-wrapper) :form form @@ -222,12 +240,7 @@ :placeholder (tr "workspace.tokens.enter-token-name" token-title) :max-length max-input-length :variant "comfortable" - :auto-focus true}] - - (when (and warning-name-change? (= action "edit")) - [:div {:class (stl/css :warning-name-change-notification-wrapper)} - [:> context-notification* - {:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])] + :auto-focus true}]] [:div {:class (stl/css :input-row)} (case type diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss index 38ce932694..00eb38a2f2 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss @@ -50,10 +50,6 @@ grid-template-columns: 1fr auto auto; } -.warning-name-change-notification-wrapper { - margin-block-start: var(--sp-l); -} - .delete-btn { justify-self: start; } diff --git a/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs new file mode 100644 index 0000000000..58766ebd29 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs @@ -0,0 +1,106 @@ +;; 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 + + (ns app.main.ui.workspace.tokens.remapping-modal + "Token remapping confirmation modal" + (:require-macros [app.main.style :as stl]) + (:require + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [rumext.v2 :as mf])) + +(defn show-remapping-modal + "Show the token remapping confirmation modal" + [{:keys [old-token-name new-token-name references-count on-confirm on-cancel]}] + (let [props {:old-token-name old-token-name + :new-token-name new-token-name + :references-count references-count + :on-confirm on-confirm + :on-cancel on-cancel}] + (st/emit! (modal/show :tokens/remapping-confirmation props)))) + +(defn hide-remapping-modal + "Hide the token remapping confirmation modal" + [] + (st/emit! (modal/hide))) + +;; Remapping Modal Component +(mf/defc token-remapping-modal + {::mf/wrap-props false + ::mf/register modal/components + ::mf/register-as :tokens/remapping-confirmation} + [{:keys [old-token-name new-token-name references-count on-confirm on-cancel]}] + (let [remapping-in-progress* (mf/use-state false) + remapping-in-progress? (deref remapping-in-progress*) + + ;; Remap logic on confirm + on-confirm-remap + (mf/use-fn + (mf/deps on-confirm remapping-in-progress*) + (fn [e] + (dom/prevent-default e) + (dom/stop-propagation e) + (reset! remapping-in-progress* true) + ;; Call shared remapping logic + (let [state @st/state + remap-modal (:remap-modal state) + old-token-name (:old-token-name remap-modal) + new-token-name (:new-token-name remap-modal)] + (st/emit! [:tokens/remap-tokens old-token-name new-token-name])) + (when (fn? on-confirm) + (on-confirm)))) + + on-cancel-remap + (mf/use-fn + (mf/deps on-cancel) + (fn [e] + (dom/prevent-default e) + (dom/stop-propagation e) + (modal/hide!) + (when (fn? on-cancel) + (on-cancel))))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog) + :data-testid "token-remapping-modal"} + [:div {:class (stl/css :modal-header)} + [:> heading* {:level 2 + :typography "headline-medium" + :class (stl/css :modal-title)} + (tr "workspace.tokens.remap-token-references")]] + [:div {:class (stl/css :modal-content)} + [:> heading* {:level 3 + :typography "title-medium" + :class (stl/css :modal-msg)} + (tr "workspace.tokens.renaming-token-from-to" old-token-name new-token-name)] + [:div {:class (stl/css :modal-scd-msg)} + (if (> references-count 0) + (tr "workspace.tokens.references-found" references-count) + (tr "workspace.tokens.no-references-found"))] + (when remapping-in-progress? + [:> context-notification* + {:level :info + :appearance :ghost} + (tr "workspace.tokens.remapping-in-progress")])] + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:> button* {:on-click on-cancel-remap + :type "button" + :variant "secondary" + :disabled remapping-in-progress?} + (tr "labels.cancel")] + [:> button* {:on-click on-confirm-remap + :type "button" + :variant "primary" + :disabled remapping-in-progress?} + (if (> references-count 0) + (tr "workspace.tokens.remap-and-rename") + (tr "workspace.tokens.rename-only"))]]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss new file mode 100644 index 0000000000..c13fab2dce --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss @@ -0,0 +1,113 @@ +// 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/_sizes.scss" as *; +@use "ds/typography.scss" as t; + +@use "refactor/common-refactor.scss" as deprecated; + +.modal-overlay { + @extend .modal-overlay-base; + display: flex; + justify-content: center; + align-items: center; + position: fixed; + inset-inline-start: 0; + inset-block-start: 0; + height: 100%; + width: 100%; + background-color: var(--overlay-color); +} + +.modal-dialog { + @extend .modal-container-base; + width: 100%; + max-width: 32rem; + max-height: unset; + user-select: none; + position: relative; +} + +.modal-header { + margin-block-end: var(--sp-xxl); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-title { + @include t.use-typography("headline-medium"); + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; + position: absolute; + inset-block-start: var(--sp-s); + inset-inline-end: var(--sp-xs); +} + +.modal-content { + @include t.use-typography("body-large"); + margin-block-end: var(--sp-xxl); + padding: var(--sp-xxl) 0; + display: flex; + flex-direction: column; + gap: var(--sp-l); +} + +.modal-footer { + margin-block-start: var(--sp-xxl); + gap: var(--sp-s); +} + +.action-buttons { + @extend .modal-action-btns; + gap: var(--sp-s); +} + +.cancel-button { + @extend .modal-cancel-btn; +} + +.accept-btn { + @extend .modal-accept-btn; +} + +.modal-scd-msg, +.modal-msg { + @include t.use-typography("body-large"); + color: var(--modal-text-foreground-color); +} + +.remap-explanation { + margin: var(--spacing-sm) 0 0 0; + color: var(--color-foreground-secondary); + font-size: var(--fs-12); + line-height: var(--lh-1-4); +} + +.no-references-info { + margin-block-end: var(--spacing-md); +} + +.no-remap-explanation { + margin: var(--spacing-sm) 0 0 0; + color: var(--color-foreground-secondary); + font-size: var(--fs-12); + line-height: var(--lh-1-4); +} + +.progress-info { + margin-block-end: var(--spacing-md); + padding: var(--spacing-sm); + background: var(--color-background-secondary); + border-radius: var(--radius-sm); +} + +.modal-actions { + @extend .modal-action-btns; +} diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index ff67188e9b..dd9dbbe1d0 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -16,6 +16,7 @@ [frontend-tests.tokens.logic.token-actions-test] [frontend-tests.tokens.logic.token-data-test] [frontend-tests.tokens.style-dictionary-test] + [frontend-tests.tokens.workspace-tokens-remap-test] [frontend-tests.util-object-test] [frontend-tests.util-range-tree-test] [frontend-tests.util-simple-math-test] @@ -49,4 +50,5 @@ 'frontend-tests.util-object-test 'frontend-tests.util-range-tree-test 'frontend-tests.util-simple-math-test + 'frontend-tests.tokens.workspace-tokens-remap-test 'frontend-tests.worker-snap-test)) diff --git a/frontend/test/frontend_tests/test_helpers_shapes.cljs b/frontend/test/frontend_tests/test_helpers_shapes.cljs index ce66082283..d6b8e782d9 100644 --- a/frontend/test/frontend_tests/test_helpers_shapes.cljs +++ b/frontend/test/frontend_tests/test_helpers_shapes.cljs @@ -4,13 +4,13 @@ [app.common.geom.point :as gpt] [app.common.types.color :as clr] [app.main.data.workspace.libraries :as dwl] - [app.test-helpers.events :as the] - [app.test-helpers.libraries :as thl] - [app.test-helpers.pages :as thp] [beicon.v2.core :as rx] [cljs.pprint :refer [pprint]] [cljs.test :as t :include-macros true] [clojure.stacktrace :as stk] + [frontend-tests.helpers.events :as the] + [frontend-tests.helpers.libraries :as thl] + [frontend-tests.helpers.pages :as thp] [linked.core :as lks] [potok.v2.core :as ptk])) diff --git a/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs new file mode 100644 index 0000000000..6bfb7720f8 --- /dev/null +++ b/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs @@ -0,0 +1,127 @@ +;; 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 + +(ns frontend-tests.tokens.logic.token-remapping-test + (:require + [app.common.types.tokens-lib :as ctob] + [app.main.data.workspace.tokens.remapping :as dwtr] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.files :as thf] + [frontend-tests.helpers.ids :as thi] + [frontend-tests.helpers.objects :as tho] + [frontend-tests.helpers.state :as ths] + [frontend-tests.helpers.tokens :as tht])) + +(defn setup-file-with-tokens + "Setup a test file with tokens and shapes that use those tokens" + [] + (let [color-token {:name "color.primary" + :value "#FF0000" + :type :color} + alias-token {:name "color.secondary" + :value "{color.primary}" + :type :color}] + (-> (thf/sample-file :file-1 :page-label :page-1) + (tho/add-rect :rect-1) + (tho/add-rect :rect-2) + (assoc-in [:data :tokens-lib] + (-> (ctob/make-tokens-lib) + (ctob/add-theme (ctob/make-token-theme :name "Theme A" :sets #{"Set A"})) + (ctob/set-active-themes #{"/Theme A"}) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) + :name "Set A")) + (ctob/add-token (thi/id :set-a) + (ctob/make-token color-token)) + (ctob/add-token (thi/id :set-a) + (ctob/make-token alias-token)))) + ;; Apply the token to rect-1 + (tht/apply-token-to-shape :fill "color.primary" :rect-1)))) + +(t/deftest test-scan-workspace-token-references + (t/testing "should find applied token references" + (let [file (setup-file-with-tokens) + file-data (:data file) + scan-results (dwtr/scan-workspace-token-references file-data "color.primary")] + + (t/is (= 1 (count (:applied-tokens scan-results)))) + (t/is (= 1 (count (:token-aliases scan-results)))) + (t/is (= 2 (:total-references scan-results))) + + ;; Check applied token reference + (let [applied-ref (first (:applied-tokens scan-results))] + (t/is (= :applied-token (:type applied-ref))) + (t/is (= "color.primary" (:token-name applied-ref))) + (t/is (= :fill (:attribute applied-ref)))) + + ;; Check alias reference + (let [alias-ref (first (:token-aliases scan-results))] + (t/is (= :token-alias (:type alias-ref))) + (t/is (= "color.secondary" (:source-token-name alias-ref))) + (t/is (= "color.primary" (:referenced-token-name alias-ref))))))) + +(t/deftest test-scan-token-value-references + (t/testing "should extract token references from alias values" + (let [token {:name "color.secondary" + :value "{color.primary}" + :type :color} + references (dwtr/scan-token-value-references token)] + + (t/is (= 1 (count references))) + (let [ref (first references)] + (t/is (= :token-alias (:type ref))) + (t/is (= "color.secondary" (:source-token-name ref))) + (t/is (= "color.primary" (:referenced-token-name ref)))))) + + (t/testing "should handle multiple references in one value" + (let [token {:name "spacing.complex" + :value "calc({spacing.base} + {spacing.small})" + :type :spacing} + references (dwtr/scan-token-value-references token)] + + (t/is (= 2 (count references))) + (t/is (some #(= "spacing.base" (:referenced-token-name %)) references)) + (t/is (some #(= "spacing.small" (:referenced-token-name %)) references))))) + +(t/deftest test-update-token-value-references + (t/testing "should update token references in alias values" + (let [old-value "{color.primary}" + new-value (dwtr/update-token-value-references old-value "color.primary" "brand.primary")] + (t/is (= "{brand.primary}" new-value)))) + + (t/testing "should update multiple references" + (let [old-value "calc({spacing.base} + {spacing.base})" + new-value (dwtr/update-token-value-references old-value "spacing.base" "spacing.foundation")] + (t/is (= "calc({spacing.foundation} + {spacing.foundation})" new-value)))) + + (t/testing "should not update partial matches" + (let [old-value "{color.primary.light}" + new-value (dwtr/update-token-value-references old-value "color.primary" "brand.primary")] + (t/is (= "{color.primary.light}" new-value))))) + +(t/deftest test-count-token-references + (t/testing "should count total references to a token" + (let [file (setup-file-with-tokens) + file-data (:data file) + count (dwtr/count-token-references file-data "color.primary")] + (t/is (= 2 count))))) + +(t/deftest test-validate-token-remapping + (t/testing "should validate remapping parameters" + (let [file-data (:data (setup-file-with-tokens))] + + (t/testing "empty name should be invalid" + (let [result (dwtr/validate-token-remapping file-data "color.primary" "")] + (t/is (false? (:valid? result))) + (t/is (= :invalid-name (:error result))))) + + (t/testing "same name should be invalid" + (let [result (dwtr/validate-token-remapping file-data "color.primary" "color.primary")] + (t/is (false? (:valid? result))) + (t/is (= :no-change (:error result))))) + + (t/testing "valid new name should be valid" + (let [result (dwtr/validate-token-remapping file-data "color.primary" "brand.primary")] + (t/is (true? (:valid? result)))))))) diff --git a/frontend/test/frontend_tests/tokens/style_dictionary_test.cljs b/frontend/test/frontend_tests/tokens/style_dictionary_test.cljs index 2341d7baf6..bf8aad0c35 100644 --- a/frontend/test/frontend_tests/tokens/style_dictionary_test.cljs +++ b/frontend/test/frontend_tests/tokens/style_dictionary_test.cljs @@ -39,7 +39,7 @@ (ctob/make-token {:name "borderRadius.largeFn" :value "{borderRadius.sm} * 200000000" :type :border-radius})) - (ctob/get-all-tokens))] + (ctob/get-all-tokens-map))] (-> (sd/resolve-tokens tokens) (rx/sub! (fn [resolved-tokens] diff --git a/frontend/test/frontend_tests/tokens/workspace_tokens_remap_test.cljs b/frontend/test/frontend_tests/tokens/workspace_tokens_remap_test.cljs new file mode 100644 index 0000000000..3a514da20e --- /dev/null +++ b/frontend/test/frontend_tests/tokens/workspace_tokens_remap_test.cljs @@ -0,0 +1,372 @@ +;; 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 + +(ns frontend-tests.tokens.workspace-tokens-remap-test + (:require + [app.common.test-helpers.compositions :as ctho] + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.ids-map :as cthi] + [app.common.test-helpers.shapes :as cths] + [app.common.types.text :as txt] + [app.common.types.tokens-lib :as ctob] + [app.main.data.workspace.tokens.remapping :as remap] + [cljs.test :as t :include-macros true] + [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 cthi/reset-idmap!}) + +(def token-set-name "remap-test-set") +(def token-theme-name "remap-test-theme") + +(defn- make-base-file [] + (-> (cthf/sample-file :file-1 :page-label :page-1) + (ctho/add-rect :rect-shape) + (ctho/add-text :text-shape "Sample text"))) + +(defn- attach-token-set [file tokens] + (let [set-id (cthi/new-id! :token-set) + tokens-lib (reduce + (fn [lib token] + (ctob/add-token lib set-id (ctob/make-token token))) + (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-id + :name token-set-name)) + (ctob/add-theme (ctob/make-token-theme :name token-theme-name + :sets #{token-set-name})) + (ctob/set-active-themes #{(str "/" token-theme-name)})) + tokens)] + (assoc-in file [:data :tokens-lib] tokens-lib))) + +(defn- tokens-lib [file] + (get-in file [:data :tokens-lib])) + +(defn- find-token-entry [tokens-lib token-name] + (when tokens-lib + (some (fn [set] + (let [set-id (ctob/get-id set) + tokens (ctob/get-tokens tokens-lib set-id)] + (some (fn [[_ token]] + (when (= (:name token) token-name) + {:set-id set-id + :token token})) + tokens))) + (ctob/get-sets tokens-lib)))) + +(defn- rename-token-in-file [file old-name new-name] + (let [tokens-lib (tokens-lib file)] + (if-let [{:keys [set-id token]} (find-token-entry tokens-lib old-name)] + (let [tokens-lib' (ctob/update-token tokens-lib set-id (:id token) #(assoc % :name new-name))] + (assoc-in file [:data :tokens-lib] tokens-lib')) + file))) + +(defn- alias-reference [token-name] + (str "{" token-name "}")) + +(defn- alias-token-for-case [{:keys [token]}] + {:name (str (:name token) "-alias") + :type (:type token) + :value (alias-reference (:name token))}) + +(defn- file-for-case [{:keys [token attribute shape]}] + (let [file (-> (make-base-file) + (attach-token-set [token]))] + (toht/apply-token-to-shape file shape (:name token) #{attribute}))) + +(def token-remap-cases + [{:case :boolean + :token {:name "boolean-token" :type :boolean :value true} + :attribute :visible + :shape :rect-shape} + + {:case :border-radius + :token {:name "border-radius-token" :type :border-radius :value "12"} + :attribute :r1 + :shape :rect-shape} + + {:case :shadow + :token {:name "shadow-token" + :type :shadow + :value [{:offset-x 0 + :offset-y 1 + :blur 2 + :spread 0 + :color "rgba(0,0,0,0.5)" + :inset false}]} + :attribute :shadow + :shape :rect-shape} + + {:case :color + :token {:name "color-token" :type :color :value "#ff0000"} + :attribute :fill + :shape :rect-shape} + + {:case :dimensions + :token {:name "dimensions-token" :type :dimensions :value "100px"} + :attribute :width + :shape :rect-shape} + + {:case :font-family + :token {:name "font-family-token" + :type :font-family + :value ["Arial" "Helvetica"]} + :attribute :font-family + :shape :text-shape} + + {:case :font-size + :token {:name "font-size-token" :type :font-size :value "16px"} + :attribute :font-size + :shape :text-shape} + + {:case :letter-spacing + :token {:name "letter-spacing-token" :type :letter-spacing :value "1"} + :attribute :letter-spacing + :shape :text-shape} + + {:case :number + :token {:name "number-token" :type :number :value "42"} + :attribute :line-height + :shape :text-shape} + + {:case :opacity + :token {:name "opacity-token" :type :opacity :value 0.5} + :attribute :opacity + :shape :rect-shape} + + {:case :other + :token {:name "other-token" :type :other :value "misc"} + :attribute :custom-data + :shape :rect-shape} + + {:case :rotation + :token {:name "rotation-token" :type :rotation :value 45} + :attribute :rotation + :shape :rect-shape} + + {:case :sizing + :token {:name "sizing-token" :type :sizing :value "200px"} + :attribute :height + :shape :rect-shape} + + {:case :spacing + :token {:name "spacing-token" :type :spacing :value "8"} + :attribute :spacing + :shape :rect-shape} + + {:case :string + :token {:name "string-token" :type :string :value "hello"} + :attribute :string-content + :shape :text-shape} + + {:case :stroke-width + :token {:name "stroke-width-token" :type :stroke-width :value "2"} + :attribute :stroke-width + :shape :rect-shape} + + {:case :text-case + :token {:name "text-case-token" :type :text-case :value "uppercase"} + :attribute :text-transform + :shape :text-shape} + + {:case :text-decoration + :token {:name "text-decoration-token" :type :text-decoration :value "underline"} + :attribute :text-decoration + :shape :text-shape} + + {:case :font-weight + :token {:name "font-weight-token" :type :font-weight :value "bold"} + :attribute :font-weight + :shape :text-shape} + + {:case :typography + :token {:name "typography-token" + :type :typography + :value {:font-size "18px" + :font-family [(:font-id txt/default-text-attrs) "Arial"] + :font-weight "600" + :line-height "20px" + :letter-spacing "1" + :text-case "uppercase" + :text-decoration "underline"}} + :attribute :typography + :shape :text-shape}]) + +(def token-case-by-type + (into {} (map (juxt :case identity) token-remap-cases))) + +(defn- fetch-token-case [case-key] + (or (get token-case-by-type case-key) + (throw (ex-info "Unknown token case" {:case case-key})))) + +(defn- run-token-remap-test! [token-case done] + (let [{:keys [token attribute shape]} token-case + old-name (:name token) + new-name (str old-name "-renamed") + file (-> (file-for-case token-case) + (rename-token-in-file old-name new-name)) + store (ths/setup-store file) + events [(remap/remap-tokens old-name new-name)]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + shape' (cths/get-shape file' shape) + applied-name (get-in shape' [:applied-tokens attribute])] + (t/is (= new-name applied-name) + (str "attribute " attribute " now references renamed token")) + (t/is (not-any? #(= old-name %) (vals (or (:applied-tokens shape') {}))) + "old token name removed from applied tokens")))))) + +(defn- run-alias-remap-test! [token-case done] + (let [{:keys [token]} token-case + alias-token (alias-token-for-case token-case) + alias-name (:name alias-token) + old-name (:name token) + new-name (str old-name "-renamed") + file (-> (make-base-file) + (attach-token-set [token alias-token])) + tokens-lib-before (tokens-lib file) + set-id (some-> (ctob/get-set-by-name tokens-lib-before token-set-name) ctob/get-id) + alias-before (ctob/get-token-by-name tokens-lib-before token-set-name alias-name) + alias-id (:id alias-before) + file' (rename-token-in-file file old-name new-name) + store (ths/setup-store file')] + (tohs/run-store-async + store done [(remap/remap-tokens old-name new-name)] + (fn [new-state] + (let [file'' (ths/get-file-from-state new-state) + tokens-lib' (tokens-lib file'') + updated-alias (ctob/get-token tokens-lib' set-id alias-id)] + (t/is (= (alias-reference new-name) (:value updated-alias)) + (str "alias for " alias-name " updated to new token name"))))))) + +(defn- define-remap-test [case-key test-fn] + (t/async done + (test-fn (fetch-token-case case-key) done))) + +;; Direct remap tests +(t/deftest remap-boolean-token + (define-remap-test :boolean run-token-remap-test!)) + +(t/deftest remap-border-radius-token + (define-remap-test :border-radius run-token-remap-test!)) + +(t/deftest remap-shadow-token + (define-remap-test :shadow run-token-remap-test!)) + +(t/deftest remap-color-token + (define-remap-test :color run-token-remap-test!)) + +(t/deftest remap-dimensions-token + (define-remap-test :dimensions run-token-remap-test!)) + +(t/deftest remap-font-family-token + (define-remap-test :font-family run-token-remap-test!)) + +(t/deftest remap-font-size-token + (define-remap-test :font-size run-token-remap-test!)) + +(t/deftest remap-letter-spacing-token + (define-remap-test :letter-spacing run-token-remap-test!)) + +(t/deftest remap-number-token + (define-remap-test :number run-token-remap-test!)) + +(t/deftest remap-opacity-token + (define-remap-test :opacity run-token-remap-test!)) + +(t/deftest remap-other-token + (define-remap-test :other run-token-remap-test!)) + +(t/deftest remap-rotation-token + (define-remap-test :rotation run-token-remap-test!)) + +(t/deftest remap-sizing-token + (define-remap-test :sizing run-token-remap-test!)) + +(t/deftest remap-spacing-token + (define-remap-test :spacing run-token-remap-test!)) + +(t/deftest remap-string-token + (define-remap-test :string run-token-remap-test!)) + +(t/deftest remap-stroke-width-token + (define-remap-test :stroke-width run-token-remap-test!)) + +(t/deftest remap-text-case-token + (define-remap-test :text-case run-token-remap-test!)) + +(t/deftest remap-text-decoration-token + (define-remap-test :text-decoration run-token-remap-test!)) + +(t/deftest remap-font-weight-token + (define-remap-test :font-weight run-token-remap-test!)) + +(t/deftest remap-typography-token + (define-remap-test :typography run-token-remap-test!)) + +;; Alias remap tests +(t/deftest remap-boolean-alias + (define-remap-test :boolean run-alias-remap-test!)) + +(t/deftest remap-border-radius-alias + (define-remap-test :border-radius run-alias-remap-test!)) + +(t/deftest remap-shadow-alias + (define-remap-test :shadow run-alias-remap-test!)) + +(t/deftest remap-color-alias + (define-remap-test :color run-alias-remap-test!)) + +(t/deftest remap-dimensions-alias + (define-remap-test :dimensions run-alias-remap-test!)) + +(t/deftest remap-font-family-alias + (define-remap-test :font-family run-alias-remap-test!)) + +(t/deftest remap-font-size-alias + (define-remap-test :font-size run-alias-remap-test!)) + +(t/deftest remap-letter-spacing-alias + (define-remap-test :letter-spacing run-alias-remap-test!)) + +(t/deftest remap-number-alias + (define-remap-test :number run-alias-remap-test!)) + +(t/deftest remap-opacity-alias + (define-remap-test :opacity run-alias-remap-test!)) + +(t/deftest remap-other-alias + (define-remap-test :other run-alias-remap-test!)) + +(t/deftest remap-rotation-alias + (define-remap-test :rotation run-alias-remap-test!)) + +(t/deftest remap-sizing-alias + (define-remap-test :sizing run-alias-remap-test!)) + +(t/deftest remap-spacing-alias + (define-remap-test :spacing run-alias-remap-test!)) + +(t/deftest remap-string-alias + (define-remap-test :string run-alias-remap-test!)) + +(t/deftest remap-stroke-width-alias + (define-remap-test :stroke-width run-alias-remap-test!)) + +(t/deftest remap-text-case-alias + (define-remap-test :text-case run-alias-remap-test!)) + +(t/deftest remap-text-decoration-alias + (define-remap-test :text-decoration run-alias-remap-test!)) + +(t/deftest remap-font-weight-alias + (define-remap-test :font-weight run-alias-remap-test!)) + +(t/deftest remap-typography-alias + (define-remap-test :typography run-alias-remap-test!)) diff --git a/frontend/translations/cs.po b/frontend/translations/cs.po index d1d9e51887..1d7905009c 100644 --- a/frontend/translations/cs.po +++ b/frontend/translations/cs.po @@ -6456,10 +6456,6 @@ msgstr "Nástroje" msgid "workspace.tokens.value-not-valid" msgstr "Hodnota není platná" -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "Přejmenováním tohoto tokenu se přeruší jakýkoli odkaz na jeho starý název." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Položky" diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 2da98333c1..aba044a401 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -7175,12 +7175,6 @@ msgstr "Der Wert ist nicht gültig" msgid "workspace.tokens.value-with-percent" msgstr "Ungültiger Wert: % ist nicht zulässig." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "" -"Die Umbenennung dieses Tokens macht jeden Verweis auf seinen alten Namen " -"kaputt." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Assets" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f3b19b98e1..b31a6deb29 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7543,6 +7543,42 @@ msgstr "Color" msgid "workspace.tokens.composite-line-height-needs-font-size" msgstr "Line Height depends on Font Size. Add a Font Size to get the resolved value." +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.remap-token-references" +msgstr "Remap Token References" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.renaming-token-from-to" +msgstr "Renaming token from '%s' to '%s'" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.references-found" +msgstr "%s references found in your design" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.remap-explanation" +msgstr "All references to this token will be automatically updated to use the new name." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.no-references-found" +msgstr "No references found" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.no-remap-needed" +msgstr "This token is not currently used in your design, so no remapping is needed." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.remapping-in-progress" +msgstr "Remapping token references..." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.remap-and-rename" +msgstr "Remap & Rename" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.rename-only" +msgstr "Rename" + #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:78 msgid "workspace.tokens.create-new-theme" msgstr "Create your first theme now." @@ -8076,7 +8112,7 @@ msgstr "Type '%s' is not supported (%s)\n" msgid "workspace.tokens.use-reference" msgstr "Use a reference" -#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:131 +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:133 msgid "workspace.tokens.value-not-valid" msgstr "The value is not valid" @@ -8088,10 +8124,6 @@ msgstr "Invalid value: % is not allowed." msgid "workspace.tokens.value-with-units" msgstr "Invalid value: Units are not allowed." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "Renaming this token will break any reference to its old name." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Assets" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ab2cd2d6ce..49d5be5df1 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -4420,6 +4420,42 @@ msgstr "Mostrar/ocultar recursos" msgid "shortcuts.toggle-colorpalette" msgstr "Mostrar/ocultar paleta de colores" +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.remap-token-references" +msgstr "Actualizar referencias de token" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.renaming-token-from-to" +msgstr "Renombrando el token de '%s' a '%s'" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.references-found" +msgstr "%s referencias encontradas en tu diseño" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.remap-explanation" +msgstr "Todas las referencias a este token se actualizarán automáticamente para usar el nuevo nombre." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.no-references-found" +msgstr "No se encontraron referencias" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.no-remap-needed" +msgstr "Este token no se utiliza actualmente en tu diseño, por lo que no es necesario actualizar referencias." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.remapping-in-progress" +msgstr "Actualizando referencias de token..." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.remap-and-rename" +msgstr "Actualizar referencias y renombrar" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +msgid "workspace.tokens.rename-only" +msgstr "Renombrar" + #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185 msgid "shortcuts.toggle-focus-mode" msgstr "Mostrar/ocultar focus mode" @@ -7958,10 +7994,6 @@ msgstr "El valor no es válido" msgid "workspace.tokens.value-with-units" msgstr "Valor no válido: No se permiten unidades." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "Al renombrar este token se romperán las referencias al nombre anterior" - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Recursos" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 1a5a108136..f928ce72b7 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -7893,10 +7893,6 @@ msgstr "Valeur non valide : % n'est pas autorisé." msgid "workspace.tokens.value-with-units" msgstr "Valeur non valide : les unités ne sont pas autorisées." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "Si vous renommez ce token, toute référence à son ancien nom sera incorrecte." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Ressources" diff --git a/frontend/translations/he.po b/frontend/translations/he.po index 393d8b64a6..1c234a6382 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -7810,10 +7810,6 @@ msgstr "ערך שגוי: אסור %." msgid "workspace.tokens.value-with-units" msgstr "ערך שגוי: אסור יחידות." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "שינוי שם האסימון הזה יפגע בכל הפניה לשם הישן שלו." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "משאבים" diff --git a/frontend/translations/hi.po b/frontend/translations/hi.po index 68bf52dc44..8a3979f1b2 100644 --- a/frontend/translations/hi.po +++ b/frontend/translations/hi.po @@ -7300,10 +7300,6 @@ msgstr "मान मान्य नहीं है" msgid "workspace.tokens.value-with-units" msgstr "अमान्य मान: इकाइयाँ अनुमति नहीं हैं।" -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "इस टोकन का नाम बदलने से इसके पुराने नाम के किसी भी संदर्भ टूट जाएंगे।" - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "एसेट्स" diff --git a/frontend/translations/hr.po b/frontend/translations/hr.po index f390276ba2..71fa3c2870 100644 --- a/frontend/translations/hr.po +++ b/frontend/translations/hr.po @@ -6477,10 +6477,6 @@ msgstr "Alati" msgid "workspace.tokens.value-not-valid" msgstr "Vrijednost nije važeća" -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "Preimenovanje ovog tokena prekinut će sve reference na njegov stari naziv." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Stavke" diff --git a/frontend/translations/id.po b/frontend/translations/id.po index eb7898b170..2b744f2c66 100644 --- a/frontend/translations/id.po +++ b/frontend/translations/id.po @@ -6855,10 +6855,6 @@ msgstr "Peralatan" msgid "workspace.tokens.value-not-valid" msgstr "Nilai tidak valid" -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "Mengubah nama token ini akan merusak referensi nama lamanya." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Aset" diff --git a/frontend/translations/it.po b/frontend/translations/it.po index 682fc2289f..bd777f649f 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -7940,12 +7940,6 @@ msgstr "Valore non valido: % non è consentito." msgid "workspace.tokens.value-with-units" msgstr "Valore non valido: le unità non sono consentite." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "" -"Rinominare questo token interromperà qualsiasi riferimento al suo vecchio " -"nome." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Risorse" diff --git a/frontend/translations/lv.po b/frontend/translations/lv.po index 11845374fc..6871e970a1 100644 --- a/frontend/translations/lv.po +++ b/frontend/translations/lv.po @@ -7582,12 +7582,6 @@ msgstr "Vērtība nav derīga" msgid "workspace.tokens.value-with-units" msgstr "Nederīga vērtība: mērvienības nav atļautas." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "" -"Šīs tekstvienības pārdēvēšana salauzīs visas atsauces uz tās iepriekšējo " -"nosaukumu." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Līdzekļi" diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po index cac5b57683..589b55960d 100644 --- a/frontend/translations/nl.po +++ b/frontend/translations/nl.po @@ -7968,12 +7968,6 @@ msgstr "Ongeldige waarde: % is niet toegestaan." msgid "workspace.tokens.value-with-units" msgstr "Ongeldige waarde: Eenheden zijn niet toegestaan." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "" -"Met het wijzigen van de naam van dit token, worden alle verwijzingen naar " -"de oude naam verbroken." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Assets" diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po index d6a58ff605..bdd3ebe919 100644 --- a/frontend/translations/pt_BR.po +++ b/frontend/translations/pt_BR.po @@ -5661,10 +5661,6 @@ msgstr "Ferramentas" msgid "workspace.tokens.value-not-valid" msgstr "O valor não é válido" -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "Renomear este token quebrará quaisquer referência para o nome antigo." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Ativos" diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index 0cfb2f50ec..5916c280a8 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -7980,10 +7980,6 @@ msgstr "Valoare invalidă: % nu este permis." msgid "workspace.tokens.value-with-units" msgstr "Valoare invalidă: Unitățile nu sunt permise." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "Redenumirea acestui token va distruge orice referință la numele său vechi." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Obiecte" diff --git a/frontend/translations/sv.po b/frontend/translations/sv.po index 7edf2b433c..c2832d5a26 100644 --- a/frontend/translations/sv.po +++ b/frontend/translations/sv.po @@ -7928,10 +7928,6 @@ msgstr "Ogiltigt värde: % är inte tillåtet." msgid "workspace.tokens.value-with-units" msgstr "Ogiltigt värde: Enheter är ej tillåtna." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "Om du byter namn på denna token bryts alla referenser till dess gamla namn." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Tillgångar" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 42e7500b9b..312a86950a 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -7935,12 +7935,6 @@ msgstr "Geçersiz değer: % izin verilmiyor." msgid "workspace.tokens.value-with-units" msgstr "Geçersiz değer: Birimlere izin verilmiyor." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "" -"Bu tokenin adını değiştirmek, eski adına yapılan tüm referansları " -"bozacaktır." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Varlıklar" diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po index a84d623218..60368f688e 100644 --- a/frontend/translations/ukr_UA.po +++ b/frontend/translations/ukr_UA.po @@ -7437,10 +7437,6 @@ msgstr "Значення не є дійсним" msgid "workspace.tokens.value-with-units" msgstr "Помилкове значення: Одиниці не дозволені." -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "Якщо перейменувати токен, посилання на старе імʼя буде розірвано." - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "Ресурси" diff --git a/frontend/translations/zh_Hant.po b/frontend/translations/zh_Hant.po index c283e30b4c..08de8ea114 100644 --- a/frontend/translations/zh_Hant.po +++ b/frontend/translations/zh_Hant.po @@ -6295,10 +6295,6 @@ msgstr "工具" msgid "workspace.tokens.value-not-valid" msgstr "該值無效" -#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602 -msgid "workspace.tokens.warning-name-change" -msgstr "重新命名此權杖(token)將會中斷對其舊名稱的任何參照。" - #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146 msgid "workspace.toolbar.assets" msgstr "資源"