mirror of
https://github.com/penpot/penpot.git
synced 2026-05-28 03:13:40 +00:00
parent
8e7e6ffc2f
commit
f8913c755d
@ -5,6 +5,7 @@
|
|||||||
### :boom: Breaking changes & Deprecations
|
### :boom: Breaking changes & Deprecations
|
||||||
|
|
||||||
### :rocket: Epics and highlights
|
### :rocket: Epics and highlights
|
||||||
|
|
||||||
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112)
|
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112)
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
### :sparkles: New features & Enhancements
|
||||||
@ -15,11 +16,10 @@
|
|||||||
- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
|
- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
|
||||||
- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313)
|
- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313)
|
||||||
- Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474)
|
- Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474)
|
||||||
|
- Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137)
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
|
||||||
## 2.15.0 (Unreleased)
|
## 2.15.0 (Unreleased)
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
### :boom: Breaking changes & Deprecations
|
||||||
|
|||||||
@ -147,6 +147,27 @@
|
|||||||
#(and (some? tokens-tree)
|
#(and (some? tokens-tree)
|
||||||
(not (ctob/token-name-path-exists? % tokens-tree)))]])
|
(not (ctob/token-name-path-exists? % tokens-tree)))]])
|
||||||
|
|
||||||
|
(defn make-node-token-name-schema
|
||||||
|
"Dynamically generates a schema to check a token node name, adding translated error messages
|
||||||
|
and two additional validations:
|
||||||
|
- Min and max length.
|
||||||
|
- Checks if other token with a path derived from the name already exists at `tokens-tree`.
|
||||||
|
e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists."
|
||||||
|
[active-tokens tokens-tree node]
|
||||||
|
[:and
|
||||||
|
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||||
|
(-> cto/schema:token-node-name
|
||||||
|
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
|
||||||
|
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||||
|
(fn [name]
|
||||||
|
(let [current-path (:path node)
|
||||||
|
current-name (:name node)
|
||||||
|
new-tokens (ctob/update-tokens-group active-tokens current-path current-name name)]
|
||||||
|
(and (some? new-tokens)
|
||||||
|
(some (fn [[token-name _]]
|
||||||
|
(not (ctob/token-name-path-exists? token-name tokens-tree)))
|
||||||
|
new-tokens))))]])
|
||||||
|
|
||||||
(def schema:token-description
|
(def schema:token-description
|
||||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
||||||
|
|
||||||
@ -165,6 +186,11 @@
|
|||||||
(when (and name value)
|
(when (and name value)
|
||||||
(not (cto/token-value-self-reference? name value))))]])
|
(not (cto/token-value-self-reference? name value))))]])
|
||||||
|
|
||||||
|
(defn make-node-token-schema
|
||||||
|
[active-tokens tokens-tree node]
|
||||||
|
[:map
|
||||||
|
[:name (make-node-token-name-schema active-tokens tokens-tree node)]])
|
||||||
|
|
||||||
(defn convert-dtcg-token
|
(defn convert-dtcg-token
|
||||||
"Convert token attributes as they come from a decoded json, with DTCG types, to internal types.
|
"Convert token attributes as they come from a decoded json, with DTCG types, to internal types.
|
||||||
Eg. From this:
|
Eg. From this:
|
||||||
|
|||||||
@ -136,6 +136,9 @@
|
|||||||
(def token-name-validation-regex
|
(def token-name-validation-regex
|
||||||
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
||||||
|
|
||||||
|
(def token-node-name-validation-regex
|
||||||
|
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
||||||
|
|
||||||
(def schema:token-name
|
(def schema:token-name
|
||||||
"A token name can contains letters, numbers, underscores the character $ and dots, but
|
"A token name can contains letters, numbers, underscores the character $ and dots, but
|
||||||
not start with $ or end with a dot. The $ character does not have any special meaning,
|
not start with $ or end with a dot. The $ character does not have any special meaning,
|
||||||
@ -153,6 +156,14 @@
|
|||||||
:gen/gen sg/text}
|
:gen/gen sg/text}
|
||||||
token-ref-validation-regex])
|
token-ref-validation-regex])
|
||||||
|
|
||||||
|
(def schema:token-node-name
|
||||||
|
"A token node name can contains letters, numbers, underscores and the character $, but
|
||||||
|
not start with $ or a dot, or end with a dot. The $ character does not have any special meaning,
|
||||||
|
but dots separate token groups (e.g. color.primary.background)."
|
||||||
|
[:re {:title "TokenNodeName"
|
||||||
|
:gen/gen sg/text}
|
||||||
|
token-node-name-validation-regex])
|
||||||
|
|
||||||
(def schema:token-type
|
(def schema:token-type
|
||||||
[::sm/one-of {:decode/json (fn [type]
|
[::sm/one-of {:decode/json (fn [type]
|
||||||
(if (string? type)
|
(if (string? type)
|
||||||
|
|||||||
@ -153,6 +153,18 @@
|
|||||||
tokens)]
|
tokens)]
|
||||||
(group-by :type tokens')))
|
(group-by :type tokens')))
|
||||||
|
|
||||||
|
(defn rename-path
|
||||||
|
"Renames a node or token path segment with a new name.
|
||||||
|
If token is provided, it renames a token path, otherwise it renames a node path."
|
||||||
|
([node new-name]
|
||||||
|
(rename-path node nil new-name))
|
||||||
|
([node token new-name]
|
||||||
|
(let [element (if token (:name token) (:path node))
|
||||||
|
split-path (cpn/split-path element :separator ".")
|
||||||
|
updated-split-element-name (assoc split-path (:depth node) new-name)
|
||||||
|
new-element-path (cpn/join-path updated-split-element-name :separator "." :with-spaces? false)]
|
||||||
|
new-element-path)))
|
||||||
|
|
||||||
;; === Token Set
|
;; === Token Set
|
||||||
|
|
||||||
(defprotocol ITokenSet
|
(defprotocol ITokenSet
|
||||||
@ -1490,6 +1502,30 @@ Will return a value that matches this schema:
|
|||||||
(seq)
|
(seq)
|
||||||
(boolean)))))
|
(boolean)))))
|
||||||
|
|
||||||
|
(defn update-tokens-group
|
||||||
|
"Updates the active tokens path when renaming a group node.
|
||||||
|
- Filters tokens whose path matches the current path prefix
|
||||||
|
- Replaces the token name with the new name
|
||||||
|
- Updates the :path value in the token object
|
||||||
|
|
||||||
|
active-tokens: map of token-name to token-object for all active tokens in the set
|
||||||
|
current-path: the path of the group being renamed, e.g. \"foo.bar\"
|
||||||
|
current-name: the current name of the group being renamed, e.g. \"bar\"
|
||||||
|
new-name: the new name for the group being renamed, e.g. \"baz\""
|
||||||
|
|
||||||
|
[active-tokens current-path current-name new-name]
|
||||||
|
(let [path-prefix (str/replace current-path current-name "")]
|
||||||
|
(mapv (fn [[token-path token-obj]]
|
||||||
|
(if (str/starts-with? token-path path-prefix)
|
||||||
|
(let [new-token-path (str/replace token-path current-name new-name)
|
||||||
|
new-token-obj (-> token-obj
|
||||||
|
(assoc :name new-token-path)
|
||||||
|
(cond-> (:path token-obj)
|
||||||
|
(assoc :path (str/replace (:path token-obj) current-name new-name))))]
|
||||||
|
[new-token-path new-token-obj])
|
||||||
|
[token-path token-obj]))
|
||||||
|
active-tokens)))
|
||||||
|
|
||||||
;; === Import / Export from JSON format
|
;; === Import / Export from JSON format
|
||||||
|
|
||||||
;; Supported formats:
|
;; Supported formats:
|
||||||
|
|||||||
@ -108,7 +108,9 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async waitForIdle() {
|
async waitForIdle() {
|
||||||
await this.page.evaluate(() => new Promise((resolve) => globalThis.requestIdleCallback(resolve)));
|
await this.page.evaluate(
|
||||||
|
() => new Promise((resolve) => globalThis.requestIdleCallback(resolve)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -190,6 +192,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
this.tokensUpdateCreateModal = page.getByTestId(
|
this.tokensUpdateCreateModal = page.getByTestId(
|
||||||
"token-update-create-modal",
|
"token-update-create-modal",
|
||||||
);
|
);
|
||||||
|
this.tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
|
||||||
this.tokenThemeUpdateCreateModal = page.getByTestId(
|
this.tokenThemeUpdateCreateModal = page.getByTestId(
|
||||||
"token-theme-update-create-modal",
|
"token-theme-update-create-modal",
|
||||||
);
|
);
|
||||||
@ -224,7 +227,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
|
|
||||||
async #waitForWebSocketReadiness() {
|
async #waitForWebSocketReadiness() {
|
||||||
// TODO: find a better event to settle whether the app is ready to receive notifications via ws
|
// TODO: find a better event to settle whether the app is ready to receive notifications via ws
|
||||||
await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 })
|
await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendPresenceMessage(fixture) {
|
async sendPresenceMessage(fixture) {
|
||||||
@ -309,7 +312,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
async clickWithDragViewportAt(x, y, width, height) {
|
async clickWithDragViewportAt(x, y, width, height) {
|
||||||
await this.page.waitForTimeout(100);
|
await this.page.waitForTimeout(100);
|
||||||
const box = await this.viewport.boundingBox();
|
const box = await this.viewport.boundingBox();
|
||||||
if (!box) throw new Error('Viewport not visible');
|
if (!box) throw new Error("Viewport not visible");
|
||||||
|
|
||||||
const startX = box.x + x;
|
const startX = box.x + x;
|
||||||
const startY = box.y + y;
|
const startY = box.y + y;
|
||||||
@ -362,7 +365,9 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
await this.page.keyboard.press("T");
|
await this.page.keyboard.press("T");
|
||||||
await this.page.waitForTimeout(timeToWait);
|
await this.page.waitForTimeout(timeToWait);
|
||||||
|
|
||||||
const layersCountBefore = await this.layers.getByTestId("layer-row").count();
|
const layersCountBefore = await this.layers
|
||||||
|
.getByTestId("layer-row")
|
||||||
|
.count();
|
||||||
await this.clickAndMove(x1, y1, x2, y2);
|
await this.clickAndMove(x1, y1, x2, y2);
|
||||||
|
|
||||||
if (initialText) {
|
if (initialText) {
|
||||||
@ -385,10 +390,13 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
await this.page.keyboard.press("ControlOrMeta+C");
|
await this.page.keyboard.press("ControlOrMeta+C");
|
||||||
}
|
}
|
||||||
// wait for the clipboard to be updated
|
// wait for the clipboard to be updated
|
||||||
await this.page.waitForFunction(async () => {
|
await this.page.waitForFunction(
|
||||||
const content = await navigator.clipboard.readText()
|
async () => {
|
||||||
return content !== "";
|
const content = await navigator.clipboard.readText();
|
||||||
}, { timeout: 1000 });
|
return content !== "";
|
||||||
|
},
|
||||||
|
{ timeout: 1000 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cut(kind = "keyboard", locator = undefined) {
|
async cut(kind = "keyboard", locator = undefined) {
|
||||||
@ -399,13 +407,15 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
await this.page.keyboard.press("ControlOrMeta+X");
|
await this.page.keyboard.press("ControlOrMeta+X");
|
||||||
}
|
}
|
||||||
// wait for the clipboard to be updated
|
// wait for the clipboard to be updated
|
||||||
await this.page.waitForFunction(async () => {
|
await this.page.waitForFunction(
|
||||||
const content = await navigator.clipboard.readText()
|
async () => {
|
||||||
return content !== "";
|
const content = await navigator.clipboard.readText();
|
||||||
}, { timeout: 1000 });
|
return content !== "";
|
||||||
|
},
|
||||||
|
{ timeout: 1000 },
|
||||||
|
);
|
||||||
|
|
||||||
await this.page.waitForTimeout(3000);
|
await this.page.waitForTimeout(3000);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -914,7 +914,9 @@ test.describe("Tokens - creation", () => {
|
|||||||
const emptyNameError = "Name should be at least 1 character";
|
const emptyNameError = "Name should be at least 1 character";
|
||||||
|
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||||
await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
await setupEmptyTokensFileRender(page, {
|
||||||
|
flags: ["enable-token-shadow"],
|
||||||
|
});
|
||||||
|
|
||||||
// Open modal
|
// Open modal
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
@ -1130,7 +1132,9 @@ test.describe("Tokens - creation", () => {
|
|||||||
const emptyNameError = "Name should be at least 1 character";
|
const emptyNameError = "Name should be at least 1 character";
|
||||||
|
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||||
await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
await setupEmptyTokensFileRender(page, {
|
||||||
|
flags: ["enable-token-shadow"],
|
||||||
|
});
|
||||||
|
|
||||||
// Open modal
|
// Open modal
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
@ -1576,7 +1580,8 @@ test.describe("Tokens - creation", () => {
|
|||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
await nameField.fill(newTokenTitle);
|
await nameField.fill(newTokenTitle);
|
||||||
|
|
||||||
const referenceTabButton = tokensUpdateCreateModal.getByTestId("reference-opt");
|
const referenceTabButton =
|
||||||
|
tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||||
await referenceTabButton.click();
|
await referenceTabButton.click();
|
||||||
|
|
||||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||||
|
|||||||
@ -161,6 +161,7 @@ const setupTokensFileRender = async (page, options = {}) => {
|
|||||||
workspacePage,
|
workspacePage,
|
||||||
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
||||||
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
|
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
|
||||||
|
tokensRenameNodeModal: workspacePage.tokensRenameNodeModal,
|
||||||
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
||||||
tokenSetItems: workspacePage.tokenSetItems,
|
tokenSetItems: workspacePage.tokenSetItems,
|
||||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||||
|
|||||||
@ -123,7 +123,7 @@ const createCompositeDerivedToken = async (page, type, name, reference) => {
|
|||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
test.describe("Remapping Tokens", () => {
|
test.describe("Remapping a single token", () => {
|
||||||
test.describe("Box Shadow Token Remapping", () => {
|
test.describe("Box Shadow Token Remapping", () => {
|
||||||
test("User renames box shadow token with alias references", async ({
|
test("User renames box shadow token with alias references", async ({
|
||||||
page,
|
page,
|
||||||
@ -634,3 +634,148 @@ test.describe("Remapping Tokens", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("Remapping group of tokens", () => {
|
||||||
|
test("User renames a group - no remap", async ({ page }) => {
|
||||||
|
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||||
|
|
||||||
|
// Create multiple tokens in a group
|
||||||
|
await createToken(page, "Color", "dark.primary", "Value", "#000000");
|
||||||
|
await createToken(page, "Color", "dark.secondary", "Value", "#111111");
|
||||||
|
|
||||||
|
// Verify that the node and child token are visible before deletion
|
||||||
|
const darkNode = tokensSidebar.getByRole("button", {
|
||||||
|
name: "dark",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
const darkNodeToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: "primary",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select a node and right click on it to open context menu
|
||||||
|
await expect(darkNode).toBeVisible();
|
||||||
|
await expect(darkNodeToken).toBeVisible();
|
||||||
|
await darkNode.click({ button: "right" });
|
||||||
|
|
||||||
|
// select "Rename" from the context menu
|
||||||
|
const renameNodeButton = page.getByRole("button", {
|
||||||
|
name: "Rename",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
await expect(renameNodeButton).toBeVisible();
|
||||||
|
await renameNodeButton.click();
|
||||||
|
|
||||||
|
// Expect the rename modal to be visible, fill in the new name and submit
|
||||||
|
const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
|
||||||
|
await expect(tokenRenameNodeModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokenRenameNodeModal.getByRole("textbox", {
|
||||||
|
name: "Name",
|
||||||
|
});
|
||||||
|
await nameField.fill("darker");
|
||||||
|
|
||||||
|
const submitButton = tokenRenameNodeModal.getByRole("button", {
|
||||||
|
name: "Rename",
|
||||||
|
});
|
||||||
|
await submitButton.click();
|
||||||
|
|
||||||
|
// Ensure that the remapping modal does not appear
|
||||||
|
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||||
|
await expect(remappingModal).not.toBeVisible();
|
||||||
|
|
||||||
|
// Verify that the node has been renamed and tokens are still visible
|
||||||
|
const darkerNode = tokensSidebar.getByRole("button", {
|
||||||
|
name: "darker",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(darkerNode).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("User renames a group - and remaps", async ({ page }) => {
|
||||||
|
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||||
|
const workspacePage = new WasmWorkspacePage(page);
|
||||||
|
const rightSidebar = workspacePage.rightSidebar;
|
||||||
|
|
||||||
|
// Create multiple tokens in a group
|
||||||
|
await createToken(page, "Color", "light.primary", "Value", "#FFFFFF");
|
||||||
|
await createToken(page, "Color", "light.secondary", "Value", "#EEEEEE");
|
||||||
|
|
||||||
|
// Verify that the node and child token are visible before deletion
|
||||||
|
const lightNode = tokensSidebar.getByRole("button", {
|
||||||
|
name: "light",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
const lightNodeToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: "primary",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select a node and right click on it to open context menu
|
||||||
|
await expect(lightNode).toBeVisible();
|
||||||
|
await expect(lightNodeToken).toBeVisible();
|
||||||
|
|
||||||
|
// Apply token to a shape to ensure remapping modal appears with applied token reference
|
||||||
|
await page.getByRole("tab", { name: "Layers" }).click();
|
||||||
|
await page
|
||||||
|
.getByTestId("layer-row")
|
||||||
|
.filter({ hasText: "Rectangle" })
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page.getByRole("tab", { name: "Tokens" }).click();
|
||||||
|
const lightPrimaryToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: "primary",
|
||||||
|
});
|
||||||
|
await lightPrimaryToken.click();
|
||||||
|
|
||||||
|
// Right click on the node to rename
|
||||||
|
|
||||||
|
await lightNode.click({ button: "right" });
|
||||||
|
const renameNodeButton = page.getByRole("button", {
|
||||||
|
name: "Rename",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
await expect(renameNodeButton).toBeVisible();
|
||||||
|
await renameNodeButton.click();
|
||||||
|
|
||||||
|
// Expect the rename modal to be visible, fill in the new name and submit
|
||||||
|
const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
|
||||||
|
await expect(tokenRenameNodeModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokenRenameNodeModal.getByRole("textbox", {
|
||||||
|
name: "Name",
|
||||||
|
});
|
||||||
|
await nameField.fill("lighter");
|
||||||
|
|
||||||
|
const submitButton = tokenRenameNodeModal.getByRole("button", {
|
||||||
|
name: "Rename",
|
||||||
|
});
|
||||||
|
await submitButton.click();
|
||||||
|
|
||||||
|
// Ensure that the remapping modal appears and confirm remap
|
||||||
|
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||||
|
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
|
const confirmButton = remappingModal.getByRole("button", {
|
||||||
|
name: "remap tokens",
|
||||||
|
});
|
||||||
|
await confirmButton.click();
|
||||||
|
|
||||||
|
// Verify that the node has been renamed and tokens are still visible
|
||||||
|
const lighterNode = tokensSidebar.getByRole("button", {
|
||||||
|
name: "lighter",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(lighterNode).toBeVisible();
|
||||||
|
|
||||||
|
// Verify that the applied token reference has been updated in the right sidebar for the selected shape
|
||||||
|
const fillSection = rightSidebar.getByTestId("fill-section");
|
||||||
|
await expect(fillSection).toBeVisible();
|
||||||
|
|
||||||
|
const tokenReference = fillSection.getByLabel("lighter.primary", {
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
await expect(tokenReference).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -456,6 +456,34 @@
|
|||||||
(rx/of (dch/commit-changes changes)
|
(rx/of (dch/commit-changes changes)
|
||||||
(ptk/data-event ::ev/event {::ev/name "edit-token" :type token-type})))))))
|
(ptk/data-event ::ev/event {::ev/name "edit-token" :type token-type})))))))
|
||||||
|
|
||||||
|
(defn bulk-update-tokens
|
||||||
|
[set-id token-ids type old-path new-path]
|
||||||
|
(dm/assert! (uuid? set-id))
|
||||||
|
(dm/assert! (every? uuid? token-ids))
|
||||||
|
(ptk/reify ::bulk-update-tokens
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [it state _]
|
||||||
|
(let [token-set (if set-id
|
||||||
|
(lookup-token-set state set-id)
|
||||||
|
(lookup-token-set state))
|
||||||
|
data (dsh/lookup-file-data state)
|
||||||
|
changes (reduce (fn [changes token-id]
|
||||||
|
(let [token (-> (get-tokens-lib state)
|
||||||
|
(ctob/get-token (ctob/get-id token-set) token-id))
|
||||||
|
new-name (str/replace (:name token) old-path new-path)
|
||||||
|
token' (->> (merge token {:name new-name})
|
||||||
|
(into {})
|
||||||
|
(ctob/make-token))]
|
||||||
|
(pcb/set-token changes (ctob/get-id token-set) token-id token')))
|
||||||
|
(-> (pcb/empty-changes it)
|
||||||
|
(pcb/with-library-data data))
|
||||||
|
|
||||||
|
token-ids)]
|
||||||
|
(toggle-token-path (str (name type) "." old-path))
|
||||||
|
(toggle-token-path (str (name type) "." new-path))
|
||||||
|
(rx/of (dch/commit-changes changes)
|
||||||
|
(ptk/data-event ::ev/event {::ev/name "bulk-update-tokens" :type type}))))))
|
||||||
|
|
||||||
(defn delete-token
|
(defn delete-token
|
||||||
[set-id token-id]
|
[set-id token-id]
|
||||||
(dm/assert! (uuid? set-id))
|
(dm/assert! (uuid? set-id))
|
||||||
|
|||||||
@ -150,6 +150,18 @@
|
|||||||
|
|
||||||
(rx/of (dch/commit-changes token-changes))))))
|
(rx/of (dch/commit-changes token-changes))))))
|
||||||
|
|
||||||
|
(defn bulk-remap-tokens
|
||||||
|
"Helper function to remap a batch of tokens, used for node renaming"
|
||||||
|
[tokens-in-path new-tokens]
|
||||||
|
(ptk/reify ::bulk-remap-tokens
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(rx/concat
|
||||||
|
(map (fn [old-token new-token]
|
||||||
|
(remap-tokens (:name old-token) (:name new-token)))
|
||||||
|
tokens-in-path
|
||||||
|
new-tokens)))))
|
||||||
|
|
||||||
(defn validate-token-remapping
|
(defn validate-token-remapping
|
||||||
"Validate that a token remapping operation is safe to perform"
|
"Validate that a token remapping operation is safe to perform"
|
||||||
[old-name new-name]
|
[old-name new-name]
|
||||||
|
|||||||
@ -67,24 +67,38 @@
|
|||||||
|
|
||||||
(mf/defc form-submit*
|
(mf/defc form-submit*
|
||||||
[{:keys [disabled on-submit] :rest props}]
|
[{:keys [disabled on-submit] :rest props}]
|
||||||
|
|
||||||
(let [form (mf/use-ctx context)
|
(let [form (mf/use-ctx context)
|
||||||
disabled? (or (and (some? form)
|
form-state (when form @form)
|
||||||
(or (not (:valid @form))
|
|
||||||
(seq (:async-errors @form))
|
disabled? (mf/use-memo
|
||||||
(seq (:extra-errors @form))))
|
(mf/deps form form-state disabled)
|
||||||
(true? disabled))
|
(fn []
|
||||||
|
(boolean
|
||||||
|
(or (nil? form)
|
||||||
|
(true? disabled)
|
||||||
|
(not (:valid form-state))
|
||||||
|
(seq (:async-errors form-state))
|
||||||
|
(seq (:extra-errors form-state))))))
|
||||||
|
|
||||||
handle-key-down-save
|
handle-key-down-save
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-submit form)
|
(mf/deps on-submit form disabled?)
|
||||||
(fn [e]
|
(fn [e]
|
||||||
(when (or (k/enter? e) (k/space? e))
|
(when (and (or (k/enter? e) (k/space? e)) (not disabled?))
|
||||||
(dom/prevent-default e)
|
(dom/prevent-default e)
|
||||||
(on-submit form e))))
|
(on-submit form e))))
|
||||||
|
|
||||||
props
|
props
|
||||||
(mf/spread-props props {:disabled disabled?
|
(mf/spread-props props {:on-key-down handle-key-down-save
|
||||||
:on-key-down handle-key-down-save
|
:type "submit"})
|
||||||
:type "submit"})]
|
|
||||||
|
props
|
||||||
|
(if disabled?
|
||||||
|
(mf/spread-props props {:disabled true
|
||||||
|
:on-key-down handle-key-down-save
|
||||||
|
:type "submit"})
|
||||||
|
props)]
|
||||||
|
|
||||||
[:> button* props]))
|
[:> button* props]))
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
[app.main.ui.workspace.tokens.import]
|
[app.main.ui.workspace.tokens.import]
|
||||||
[app.main.ui.workspace.tokens.import.modal]
|
[app.main.ui.workspace.tokens.import.modal]
|
||||||
[app.main.ui.workspace.tokens.management.forms.modals]
|
[app.main.ui.workspace.tokens.management.forms.modals]
|
||||||
|
[app.main.ui.workspace.tokens.management.forms.rename-node-modal]
|
||||||
[app.main.ui.workspace.tokens.remapping-modal]
|
[app.main.ui.workspace.tokens.remapping-modal]
|
||||||
[app.main.ui.workspace.tokens.settings]
|
[app.main.ui.workspace.tokens.settings]
|
||||||
[app.main.ui.workspace.tokens.themes.create-modal]
|
[app.main.ui.workspace.tokens.themes.create-modal]
|
||||||
|
|||||||
@ -195,7 +195,7 @@
|
|||||||
(dom/set-attribute! checkbox "indeterminate" true)
|
(dom/set-attribute! checkbox "indeterminate" true)
|
||||||
(dom/remove-attribute! checkbox "indeterminate"))))
|
(dom/remove-attribute! checkbox "indeterminate"))))
|
||||||
|
|
||||||
[:div {:class (stl/css :fill-section)}
|
[:div {:class (stl/css :fill-section) :data-testid "fill-section"}
|
||||||
[:div {:class (stl/css :fill-title)}
|
[:div {:class (stl/css :fill-title)}
|
||||||
[:> title-bar* {:collapsable has-fills?
|
[:> title-bar* {:collapsable has-fills?
|
||||||
:collapsed (not open?)
|
:collapsed (not open?)
|
||||||
|
|||||||
@ -2,12 +2,17 @@
|
|||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.path-names :as cpn]
|
||||||
[app.common.types.shape.layout :as ctsl]
|
[app.common.types.shape.layout :as ctsl]
|
||||||
[app.common.types.tokens-lib :as ctob]
|
[app.common.types.tokens-lib :as ctob]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
|
[app.main.data.helpers :as dh]
|
||||||
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.style-dictionary :as sd]
|
[app.main.data.style-dictionary :as sd]
|
||||||
[app.main.data.workspace.tokens.application :as dwta]
|
[app.main.data.workspace.tokens.application :as dwta]
|
||||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
[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.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||||
@ -17,6 +22,7 @@
|
|||||||
[app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]]
|
[app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]]
|
||||||
[app.util.array :as array]
|
[app.util.array :as array]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
|
[cljs.pprint :as pp]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
@ -124,6 +130,17 @@
|
|||||||
(mf/with-memo [tokens-by-type]
|
(mf/with-memo [tokens-by-type]
|
||||||
(get-sorted-token-groups tokens-by-type))
|
(get-sorted-token-groups tokens-by-type))
|
||||||
|
|
||||||
|
;; Filter tokens by their path and return the tokens
|
||||||
|
filter-tokens-by-path
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [tokens-filtered-by-type node]
|
||||||
|
(->> tokens-filtered-by-type
|
||||||
|
(filter (fn [token]
|
||||||
|
(let [token-path (cpn/split-path (:name token) :separator ".")
|
||||||
|
_ (pp/pprint {:token-path token-path :count (count token-path)})]
|
||||||
|
(and (> (count token-path) 0)
|
||||||
|
(str/starts-with? (:name token) (str (:path node) ".")))))))))
|
||||||
|
|
||||||
;; Filter tokens by their path and return their ids
|
;; Filter tokens by their path and return their ids
|
||||||
filter-tokens-by-path-ids
|
filter-tokens-by-path-ids
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -132,7 +149,7 @@
|
|||||||
(->> selected-token-set-tokens
|
(->> selected-token-set-tokens
|
||||||
(filter (fn [token]
|
(filter (fn [token]
|
||||||
(let [[_ token-value] token]
|
(let [[_ token-value] token]
|
||||||
(and (= (:type token-value) type) (str/starts-with? (:name token-value) path)))))
|
(and (= (:type token-value) type) (str/starts-with? (:name token-value) (str path "."))))))
|
||||||
(mapv (fn [token]
|
(mapv (fn [token]
|
||||||
(let [[_ token-value] token]
|
(let [[_ token-value] token]
|
||||||
(:id token-value)))))))
|
(:id token-value)))))))
|
||||||
@ -176,7 +193,88 @@
|
|||||||
;; Remove from unfolded tree path
|
;; Remove from unfolded tree path
|
||||||
(if remaining-tokens?
|
(if remaining-tokens?
|
||||||
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
|
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
|
||||||
(st/emit! (dwtl/toggle-token-path (name type)))))))]
|
(st/emit! (dwtl/toggle-token-path (name type)))))))
|
||||||
|
|
||||||
|
bulk-rename-tokens-in-path
|
||||||
|
;; Rename tokens in bulk affected by a node rename.
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps filter-tokens-by-path-ids selected-token-set-id)
|
||||||
|
(fn [node type new-node-name]
|
||||||
|
(let [old-path (:path node)
|
||||||
|
new-path (ctob/rename-path node new-node-name)
|
||||||
|
tokens-in-path-ids (filter-tokens-by-path-ids type old-path)]
|
||||||
|
(st/emit!
|
||||||
|
(modal/hide)
|
||||||
|
(dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-path)))))
|
||||||
|
|
||||||
|
bulk-remap-tokens-in-path
|
||||||
|
;; Remap tokens in bulk affected by a node rename.
|
||||||
|
;; It will update the token names and propagate the changes to the workspace.
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps filter-tokens-by-path filter-tokens-by-path-ids selected-token-set-tokens selected-token-set-id)
|
||||||
|
(fn [node type new-node-name]
|
||||||
|
(let [old-path (:path node)
|
||||||
|
;; Get tokens in path to remap their names after remapping the node
|
||||||
|
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
|
||||||
|
tokens-filtered-by-type (get tokens-by-type type)
|
||||||
|
tokens-in-path (filter-tokens-by-path tokens-filtered-by-type node)
|
||||||
|
tokens-in-path-ids (filter-tokens-by-path-ids type old-path)
|
||||||
|
new-node-path (ctob/rename-path node new-node-name)
|
||||||
|
new-tokens (map (fn [token]
|
||||||
|
(let [new-token-path (ctob/rename-path node token new-node-name)]
|
||||||
|
(assoc token :name new-token-path)))
|
||||||
|
tokens-in-path)]
|
||||||
|
(st/emit!
|
||||||
|
(dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-node-path)
|
||||||
|
(remap/bulk-remap-tokens tokens-in-path new-tokens)
|
||||||
|
(dwtp/propagate-workspace-tokens)
|
||||||
|
(modal/hide)))))
|
||||||
|
|
||||||
|
on-remap-node-warning
|
||||||
|
;; If there are tokens that will be affected by the node rename, we show the remap modal
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps bulk-remap-tokens-in-path bulk-rename-tokens-in-path)
|
||||||
|
(fn [node type new-node-name]
|
||||||
|
(let [remap-data {:new-name new-node-name
|
||||||
|
:old-name (:name node)
|
||||||
|
:type "node"}
|
||||||
|
remap-handler #(bulk-remap-tokens-in-path node type new-node-name)
|
||||||
|
rename-handler #(bulk-rename-tokens-in-path node type new-node-name)]
|
||||||
|
(st/emit!
|
||||||
|
(modal/hide)
|
||||||
|
(modal/show :tokens/remapping-confirmation {:remap-data remap-data
|
||||||
|
:on-remap remap-handler
|
||||||
|
:on-rename rename-handler})))))
|
||||||
|
|
||||||
|
on-rename-node
|
||||||
|
;; When user renames a node, we need to check if there are tokens that will be affected by this change.
|
||||||
|
;; If there are, we display the remap modal, otherwise, we rename the tokens directly.
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps selected-token-set-tokens filter-tokens-by-path on-remap-node-warning bulk-rename-tokens-in-path)
|
||||||
|
(fn [node type new-node-name]
|
||||||
|
(let [state @st/state
|
||||||
|
file-data (dh/lookup-file-data state)
|
||||||
|
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
|
||||||
|
tokens-filtered-by-type (get tokens-by-type type)
|
||||||
|
tokens-in-current-path (filter-tokens-by-path tokens-filtered-by-type node)
|
||||||
|
_ (pp/pprint {:tokens-in-current-path tokens-in-current-path})
|
||||||
|
token-references-count (reduce (fn [count token]
|
||||||
|
(+ count (remap/count-token-references file-data (:name token))))
|
||||||
|
0
|
||||||
|
tokens-in-current-path)]
|
||||||
|
(if (> token-references-count 0)
|
||||||
|
(on-remap-node-warning node type new-node-name)
|
||||||
|
(bulk-rename-tokens-in-path node type new-node-name)))))
|
||||||
|
|
||||||
|
open-rename-node-modal
|
||||||
|
;; When user renames a node, we display a form modal
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps selected-token-set-tokens on-rename-node)
|
||||||
|
(fn [node type]
|
||||||
|
(let [on-rename-node-handler #(on-rename-node node type %)]
|
||||||
|
(st/emit! (modal/show :tokens/rename-node {:node node
|
||||||
|
:tokens-in-active-set selected-token-set-tokens
|
||||||
|
:on-rename on-rename-node-handler})))))]
|
||||||
|
|
||||||
(mf/with-effect [tokens-lib selected-token-set-id]
|
(mf/with-effect [tokens-lib selected-token-set-id]
|
||||||
(when (and tokens-lib
|
(when (and tokens-lib
|
||||||
@ -190,7 +288,8 @@
|
|||||||
|
|
||||||
[:*
|
[:*
|
||||||
[:& token-context-menu {:on-delete-token delete-token}]
|
[:& token-context-menu {:on-delete-token delete-token}]
|
||||||
[:> token-node-context-menu* {:on-delete-node delete-node}]
|
[:> token-node-context-menu* {:on-rename-node open-rename-node-modal
|
||||||
|
:on-delete-node delete-node}]
|
||||||
|
|
||||||
[:> selected-set-info* {:tokens-lib tokens-lib
|
[:> selected-set-info* {:tokens-lib tokens-lib
|
||||||
:selected-token-set-id selected-token-set-id}]
|
:selected-token-set-id selected-token-set-id}]
|
||||||
|
|||||||
@ -160,13 +160,13 @@
|
|||||||
on-remap-token
|
on-remap-token
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps token)
|
(mf/deps token)
|
||||||
(fn [valid-token name old-name description]
|
(fn [valid-token new-name old-name description]
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(dwtl/update-token (:id token)
|
(dwtl/update-token (:id token)
|
||||||
{:name name
|
{:name new-name
|
||||||
:value (:value valid-token)
|
:value (:value valid-token)
|
||||||
:description description})
|
:description description})
|
||||||
(remap/remap-tokens old-name name)
|
(remap/remap-tokens old-name new-name)
|
||||||
(dwtp/propagate-workspace-tokens)
|
(dwtp/propagate-workspace-tokens)
|
||||||
(modal/hide!))))
|
(modal/hide!))))
|
||||||
|
|
||||||
@ -203,11 +203,12 @@
|
|||||||
is-rename (and (= action "edit") (not= name old-name))
|
is-rename (and (= action "edit") (not= name old-name))
|
||||||
references-count (remap/count-token-references file-data old-name)
|
references-count (remap/count-token-references file-data old-name)
|
||||||
on-remap #(on-remap-token valid-token name old-name description)
|
on-remap #(on-remap-token valid-token name old-name description)
|
||||||
on-rename #(on-rename-token valid-token name description)]
|
on-rename #(on-rename-token valid-token name description)
|
||||||
|
remap-data {:new-name name
|
||||||
|
:old-name old-name
|
||||||
|
:type "token"}]
|
||||||
(if (and is-rename (> references-count 0))
|
(if (and is-rename (> references-count 0))
|
||||||
(st/emit! (modal/show :tokens/remapping-confirmation {:old-token-name old-name
|
(st/emit! (modal/show :tokens/remapping-confirmation {:remap-data remap-data
|
||||||
:new-token-name name
|
|
||||||
:references-count references-count
|
|
||||||
:on-remap on-remap
|
:on-remap on-remap
|
||||||
:on-rename on-rename}))
|
:on-rename on-rename}))
|
||||||
(st/emit!
|
(st/emit!
|
||||||
|
|||||||
@ -0,0 +1,117 @@
|
|||||||
|
(ns app.main.ui.workspace.tokens.management.forms.rename-node-modal
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.files.tokens :as cfo]
|
||||||
|
[app.common.types.tokens-lib :as ctob]
|
||||||
|
[app.main.data.modal :as modal]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||||
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
|
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||||
|
[app.main.ui.forms :as fc]
|
||||||
|
[app.util.forms :as fm]
|
||||||
|
[app.util.i18n :refer [tr]]
|
||||||
|
[app.util.keyboard :as kbd]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(mf/defc rename-node-form*
|
||||||
|
[{:keys [node active-tokens tokens-tree on-close on-submit]}]
|
||||||
|
(let [make-schema #(cfo/make-node-token-schema active-tokens tokens-tree node)
|
||||||
|
|
||||||
|
schema
|
||||||
|
(mf/with-memo [active-tokens]
|
||||||
|
(make-schema))
|
||||||
|
|
||||||
|
initial (mf/with-memo [node]
|
||||||
|
{:name (:name node)})
|
||||||
|
|
||||||
|
form (fm/use-form :schema schema
|
||||||
|
:initial initial)
|
||||||
|
|
||||||
|
on-submit (mf/use-fn
|
||||||
|
(mf/deps form on-submit)
|
||||||
|
(fn []
|
||||||
|
(let [name (get-in @form [:clean-data :name])]
|
||||||
|
(when (and (get-in @form [:touched :name]) (not= name (:name node)))
|
||||||
|
(on-submit name)))))
|
||||||
|
|
||||||
|
is-disabled? (or (not (:valid @form))
|
||||||
|
(not (get-in @form [:touched :name]))
|
||||||
|
(= (get-in @form [:clean-data :name]) (:name node)))
|
||||||
|
|
||||||
|
new-path (mf/with-memo [@form node]
|
||||||
|
(let [new-name (get-in @form [:clean-data :name])
|
||||||
|
path (str (:path node))
|
||||||
|
new-path (str/replace path (:name node) new-name)]
|
||||||
|
new-path))]
|
||||||
|
|
||||||
|
[:*
|
||||||
|
[:> heading* {:level 2
|
||||||
|
:typography "headline-medium"
|
||||||
|
:class (stl/css :form-modal-title)}
|
||||||
|
(tr "workspace.tokens.rename-group")]
|
||||||
|
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||||
|
:form form
|
||||||
|
:on-submit on-submit}
|
||||||
|
[:> fc/form-input* {:id "rename-node"
|
||||||
|
:name :name
|
||||||
|
:label (tr "workspace.tokens.token-name")
|
||||||
|
:placeholder (tr "workspace.tokens.token-name")
|
||||||
|
:max-length 255
|
||||||
|
:variant "comfortable"
|
||||||
|
:hint-type "hint"
|
||||||
|
:hint-message (tr "workspace.tokens.rename-group-name-hint" new-path)
|
||||||
|
:auto-focus true}]
|
||||||
|
[:div {:class (stl/css :form-actions)}
|
||||||
|
[:> button* {:variant "secondary"
|
||||||
|
:name "cancel"
|
||||||
|
:on-click on-close} (tr "labels.cancel")]
|
||||||
|
[:> fc/form-submit* {:variant "primary"
|
||||||
|
:disabled is-disabled?
|
||||||
|
:name "rename"} (tr "labels.rename")]]]]))
|
||||||
|
|
||||||
|
(mf/defc rename-node-modal
|
||||||
|
{::mf/register modal/components
|
||||||
|
::mf/register-as :tokens/rename-node}
|
||||||
|
[{:keys [node tokens-in-active-set on-rename]}]
|
||||||
|
|
||||||
|
(let [tokens-tree-in-selected-set
|
||||||
|
(mf/with-memo [tokens-in-active-set node]
|
||||||
|
(-> (ctob/tokens-tree tokens-in-active-set)
|
||||||
|
(d/dissoc-in (:name node))))
|
||||||
|
|
||||||
|
close-modal
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(st/emit! (modal/hide))))
|
||||||
|
|
||||||
|
rename
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-rename)
|
||||||
|
(fn [new-name]
|
||||||
|
(on-rename new-name)))
|
||||||
|
|
||||||
|
on-key-down
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps [close-modal])
|
||||||
|
(fn [event]
|
||||||
|
(when (kbd/esc? event)
|
||||||
|
(close-modal))))]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-overlay)
|
||||||
|
:on-key-down on-key-down
|
||||||
|
:data-testid "token-rename-node-modal"}
|
||||||
|
[:div {:class (stl/css :modal-dialog)}
|
||||||
|
[:> icon-button* {:class (stl/css :close-btn)
|
||||||
|
:on-click close-modal
|
||||||
|
:aria-label (tr "labels.close")
|
||||||
|
:variant "ghost"
|
||||||
|
:icon i/close}]
|
||||||
|
[:> rename-node-form* {:node node
|
||||||
|
:active-tokens tokens-in-active-set
|
||||||
|
:tokens-tree tokens-tree-in-selected-set
|
||||||
|
:on-close close-modal
|
||||||
|
:on-submit rename}]]]))
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
// 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 {
|
||||||
|
--modal-title-foreground-color: var(--color-foreground-primary);
|
||||||
|
--modal-text-foreground-color: var(--color-foreground-secondary);
|
||||||
|
|
||||||
|
@extend .modal-overlay-base;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
inset-block-start: 0;
|
||||||
|
block-size: 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
background-color: var(--overlay-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: $sz-6;
|
||||||
|
inset-inline-end: $sz-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
@extend .modal-container-base;
|
||||||
|
inline-size: 100%;
|
||||||
|
max-inline-size: 32rem;
|
||||||
|
max-block-size: unset;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal-title {
|
||||||
|
@include t.use-typography("headline-medium");
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
(def ^:private schema:token-node-context-menu
|
(def ^:private schema:token-node-context-menu
|
||||||
[:map
|
[:map
|
||||||
|
[:on-rename-node fn?]
|
||||||
[:on-delete-node fn?]])
|
[:on-delete-node fn?]])
|
||||||
|
|
||||||
(def ^:private tokens-node-menu-ref
|
(def ^:private tokens-node-menu-ref
|
||||||
@ -25,7 +26,7 @@
|
|||||||
|
|
||||||
(mf/defc token-node-context-menu*
|
(mf/defc token-node-context-menu*
|
||||||
{::mf/schema schema:token-node-context-menu}
|
{::mf/schema schema:token-node-context-menu}
|
||||||
[{:keys [on-delete-node]}]
|
[{:keys [on-rename-node on-delete-node]}]
|
||||||
(let [mdata (mf/deref tokens-node-menu-ref)
|
(let [mdata (mf/deref tokens-node-menu-ref)
|
||||||
is-open? (boolean mdata)
|
is-open? (boolean mdata)
|
||||||
dropdown-ref (mf/use-ref)
|
dropdown-ref (mf/use-ref)
|
||||||
@ -35,7 +36,13 @@
|
|||||||
dropdown-direction-change* (mf/use-ref 0)
|
dropdown-direction-change* (mf/use-ref 0)
|
||||||
top (+ (get-in mdata [:position :y]) 5)
|
top (+ (get-in mdata [:position :y]) 5)
|
||||||
left (+ (get-in mdata [:position :x]) 5)
|
left (+ (get-in mdata [:position :x]) 5)
|
||||||
|
rename-node (mf/use-fn
|
||||||
|
(mf/deps mdata on-rename-node)
|
||||||
|
(fn []
|
||||||
|
(let [node (get mdata :node)
|
||||||
|
type (get mdata :type)]
|
||||||
|
(when node
|
||||||
|
(on-rename-node node type)))))
|
||||||
delete-node (mf/use-fn
|
delete-node (mf/use-fn
|
||||||
(mf/deps mdata)
|
(mf/deps mdata)
|
||||||
(fn []
|
(fn []
|
||||||
@ -75,6 +82,11 @@
|
|||||||
:on-context-menu prevent-default}
|
:on-context-menu prevent-default}
|
||||||
(when mdata
|
(when mdata
|
||||||
[:ul {:class (stl/css :token-node-context-menu-list)}
|
[:ul {:class (stl/css :token-node-context-menu-list)}
|
||||||
|
[:li {:class (stl/css :token-node-context-menu-listitem)}
|
||||||
|
[:button {:class (stl/css :token-node-context-menu-action)
|
||||||
|
:type "button"
|
||||||
|
:on-click rename-node}
|
||||||
|
(tr "labels.rename")]]
|
||||||
[:li {:class (stl/css :token-node-context-menu-listitem)}
|
[:li {:class (stl/css :token-node-context-menu-listitem)}
|
||||||
[:button {:class (stl/css :token-node-context-menu-action)
|
[:button {:class (stl/css :token-node-context-menu-action)
|
||||||
:type "button"
|
:type "button"
|
||||||
|
|||||||
@ -20,27 +20,41 @@
|
|||||||
[app.util.keyboard :as kbd]
|
[app.util.keyboard :as kbd]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(defn hide-remapping-modal
|
(defn- hide-remapping-modal
|
||||||
"Hide the token remapping confirmation modal"
|
"Hide the token remapping confirmation modal"
|
||||||
[]
|
[]
|
||||||
(st/emit! (modal/hide)))
|
(st/emit! (modal/hide)))
|
||||||
|
|
||||||
|
;; TODO: Uncomment when modal components support schema validation
|
||||||
|
|
||||||
|
;; (def ^:private schema:remap-data
|
||||||
|
;; [:map
|
||||||
|
;; [:old-name :string]
|
||||||
|
;; [:new-name :string]
|
||||||
|
;; [:type [:enum "token" "node"]]])
|
||||||
|
|
||||||
|
;; (def ^:private schema:token-remapping-modal
|
||||||
|
;; [:map
|
||||||
|
;; [:remap-data [:maybe schema:remap-data]]
|
||||||
|
;; [:on-remap {:optional true} [:maybe fn?]]
|
||||||
|
;; [:on-rename {:optional true} [:maybe fn?]]])
|
||||||
|
|
||||||
;; Remapping Modal Component
|
;; Remapping Modal Component
|
||||||
(mf/defc token-remapping-modal
|
(mf/defc token-remapping-modal
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
::mf/register-as :tokens/remapping-confirmation}
|
::mf/register-as :tokens/remapping-confirmation
|
||||||
[{:keys [old-token-name new-token-name on-remap on-rename]}]
|
;; TODO: Uncomment when modal components support schema validation
|
||||||
(let [remap-modal (get @st/state :remap-modal)
|
;; ::mf/schema schema:token-remapping-modal
|
||||||
|
}
|
||||||
|
[{:keys [remap-data on-remap on-rename]}]
|
||||||
|
(let [old-name (:old-name remap-data)
|
||||||
|
new-name (:new-name remap-data)
|
||||||
|
|
||||||
;; Remap logic on confirm
|
;; Remap logic on confirm
|
||||||
confirm-remap
|
confirm-remap
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-remap remap-modal)
|
(mf/deps on-remap old-name new-name)
|
||||||
(fn []
|
(fn []
|
||||||
;; Call shared remapping logic
|
|
||||||
(let [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-remap)
|
(when (fn? on-remap)
|
||||||
(on-remap))))
|
(on-remap))))
|
||||||
|
|
||||||
@ -83,9 +97,13 @@
|
|||||||
:id "modal-title"
|
:id "modal-title"
|
||||||
:typography "headline-large"
|
:typography "headline-large"
|
||||||
:class (stl/css :modal-title)}
|
:class (stl/css :modal-title)}
|
||||||
(tr "workspace.tokens.remap-token-references-title" old-token-name new-token-name)]]
|
(if (= (:type remap-data) "token")
|
||||||
|
(tr "workspace.tokens.remap-token-references-title" old-name new-name)
|
||||||
|
(tr "workspace.tokens.remap-node-references-title" old-name new-name))]]
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-effects")]
|
(if (= (:type remap-data) "token")
|
||||||
|
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-token-warning-effects")]
|
||||||
|
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-node-warning-effects")])
|
||||||
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-time")]]
|
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-time")]]
|
||||||
[:div {:class (stl/css :modal-footer)}
|
[:div {:class (stl/css :modal-footer)}
|
||||||
[:div {:class (stl/css :action-buttons)}
|
[:div {:class (stl/css :action-buttons)}
|
||||||
|
|||||||
@ -4272,6 +4272,26 @@ msgstr "Pàgines"
|
|||||||
msgid "workspace.sitemap"
|
msgid "workspace.sitemap"
|
||||||
msgstr "Mapa del lloc"
|
msgstr "Mapa del lloc"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
|
msgid "workspace.tokens.remap-node-references-title"
|
||||||
|
msgstr "Canviar el nom de `%s` a `%s` i remapejar tots els tokens d'aquest grup?"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
|
msgid "workspace.tokens.remap-token-warning-effects"
|
||||||
|
msgstr "Això canviarà totes les capes i referències que utilitzen el token antic."
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89
|
||||||
|
msgid "workspace.tokens.remap-warning-time"
|
||||||
|
msgstr "Aquest procés pot trigar una mica"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||||
|
msgid "workspace.tokens.rename-group"
|
||||||
|
msgstr "Canviar nom del grup de tokens"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||||
|
msgid "workspace.tokens.rename-group-name-hint"
|
||||||
|
msgstr "Els teus tokens es renomenaran automàticament a %s.(sufix).(token)"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
||||||
msgid "workspace.toolbar.assets"
|
msgid "workspace.toolbar.assets"
|
||||||
msgstr "Recursos"
|
msgstr "Recursos"
|
||||||
|
|||||||
@ -8301,11 +8301,19 @@ msgstr "Remap tokens"
|
|||||||
msgid "workspace.tokens.remap-token-references-title"
|
msgid "workspace.tokens.remap-token-references-title"
|
||||||
msgstr "Remap all tokens that use `%s` to `%s`?"
|
msgstr "Remap all tokens that use `%s` to `%s`?"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
msgid "workspace.tokens.remap-warning-effects"
|
msgid "workspace.tokens.remap-node-references-title"
|
||||||
|
msgstr "Rename `%s` to `%s` and remap all tokens in this group?"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
|
msgid "workspace.tokens.remap-token-warning-effects"
|
||||||
msgstr "This will change all layers and references that use the old token name."
|
msgstr "This will change all layers and references that use the old token name."
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
|
msgid "workspace.tokens.remap-node-warning-effects"
|
||||||
|
msgstr "This will update all tokens and references that use the old tokens name."
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
msgid "workspace.tokens.remap-warning-time"
|
msgid "workspace.tokens.remap-warning-time"
|
||||||
msgstr "This action could take a while."
|
msgstr "This action could take a while."
|
||||||
|
|
||||||
@ -8547,6 +8555,14 @@ msgstr "Renaming this token will break any reference to its old name"
|
|||||||
msgid "workspace.tokens.error-text-edition"
|
msgid "workspace.tokens.error-text-edition"
|
||||||
msgstr "Tokens can't be applied while editing text. Select the text layer instead."
|
msgstr "Tokens can't be applied while editing text. Select the text layer instead."
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||||
|
msgid "workspace.tokens.rename-group"
|
||||||
|
msgstr "Rename Tokens Group"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||||
|
msgid "workspace.tokens.rename-group-name-hint"
|
||||||
|
msgstr "Your tokens will automatically be renamed to %s.(suffix).(tokenName)"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
||||||
msgid "workspace.toolbar.assets"
|
msgid "workspace.toolbar.assets"
|
||||||
msgstr "Assets"
|
msgstr "Assets"
|
||||||
|
|||||||
@ -8175,11 +8175,13 @@ msgstr "Actualizar tokens"
|
|||||||
msgid "workspace.tokens.remap-token-references-title"
|
msgid "workspace.tokens.remap-token-references-title"
|
||||||
msgstr "¿Actualizar todas las referencias de `%s` a `%s`?"
|
msgstr "¿Actualizar todas las referencias de `%s` a `%s`?"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
|
msgid "workspace.tokens.remap-node-references-title"
|
||||||
|
msgstr "¿Renombrar `%s` to `%s` y remapear todos los tokens de este grupo?"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88
|
||||||
msgid "workspace.tokens.remap-warning-effects"
|
msgid "workspace.tokens.remap-warning-effects"
|
||||||
msgstr ""
|
msgstr "Esta acción actualizará todas las capas y referencias que usen el token antiguo"
|
||||||
"Esta acción actualizará todas las capas y referencias que usen el token "
|
|
||||||
"antiguo"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89
|
||||||
msgid "workspace.tokens.remap-warning-time"
|
msgid "workspace.tokens.remap-warning-time"
|
||||||
@ -8404,6 +8406,14 @@ msgstr ""
|
|||||||
msgid "workspace.tokens.error-text-edition"
|
msgid "workspace.tokens.error-text-edition"
|
||||||
msgstr "No se pueden aplicar tokens mientras se edita texto. Seleccione la capa de texto en su lugar."
|
msgstr "No se pueden aplicar tokens mientras se edita texto. Seleccione la capa de texto en su lugar."
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||||
|
msgid "workspace.tokens.rename-group"
|
||||||
|
msgstr "Renombrar grupo de tokens"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||||
|
msgid "workspace.tokens.rename-group-name-hint"
|
||||||
|
msgstr "Tus tokens serán automáticamente renombrados a %s.(sufijo).(token)"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
||||||
msgid "workspace.toolbar.assets"
|
msgid "workspace.toolbar.assets"
|
||||||
msgstr "Recursos"
|
msgstr "Recursos"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user