mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 19:06:18 +00:00
🎉 Duplicate token group (#8886)
This commit is contained in:
parent
a803bde2ff
commit
9e4c8981be
@ -18,6 +18,7 @@
|
|||||||
- 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)
|
||||||
- Copy and paste entire rows in existing table (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8498)
|
- Copy and paste entire rows in existing table (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8498)
|
||||||
- Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137)
|
- Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137)
|
||||||
|
- Duplicate token group [Taiga #10653](https://tree.taiga.io/project/penpot/us/10653)
|
||||||
- Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568)
|
- Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568)
|
||||||
- Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713)
|
- Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713)
|
||||||
- Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466)
|
- Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466)
|
||||||
@ -84,7 +85,6 @@
|
|||||||
- Guard delete undo against missing sibling order [Github #8858](https://github.com/penpot/penpot/pull/8858)
|
- Guard delete undo against missing sibling order [Github #8858](https://github.com/penpot/penpot/pull/8858)
|
||||||
- Fix ICounted error on numeric-input token dropdown keyboard nav [Github #8803](https://github.com/penpot/penpot/pull/8803)
|
- Fix ICounted error on numeric-input token dropdown keyboard nav [Github #8803](https://github.com/penpot/penpot/pull/8803)
|
||||||
|
|
||||||
|
|
||||||
## 2.14.1
|
## 2.14.1
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
### :sparkles: New features & Enhancements
|
||||||
@ -108,7 +108,6 @@
|
|||||||
- Ensure path content is always PathData when saving
|
- Ensure path content is always PathData when saving
|
||||||
- Fix error when get-parent-with-data encounters non-Element nodes
|
- Fix error when get-parent-with-data encounters non-Element nodes
|
||||||
|
|
||||||
|
|
||||||
## 2.14.0
|
## 2.14.0
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
### :boom: Breaking changes & Deprecations
|
||||||
|
|||||||
@ -2011,48 +2011,4 @@ test.describe("Tokens tab - delete", () => {
|
|||||||
await expect(tokenContextMenuForToken).not.toBeVisible();
|
await expect(tokenContextMenuForToken).not.toBeVisible();
|
||||||
await expect(colorToken).not.toBeVisible();
|
await expect(colorToken).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("User removes node and all child tokens", async ({ page }) => {
|
|
||||||
const { tokensSidebar } = await setupTokensFileRender(page);
|
|
||||||
|
|
||||||
await expect(tokensSidebar).toBeVisible();
|
|
||||||
|
|
||||||
// Expand color tokens
|
|
||||||
await unfoldTokenType(tokensSidebar, "color");
|
|
||||||
|
|
||||||
// Verify that the node and child token are visible before deletion
|
|
||||||
const colorNode = tokensSidebar.getByRole("button", {
|
|
||||||
name: "colors",
|
|
||||||
exact: true,
|
|
||||||
});
|
|
||||||
const colorNodeToken = tokensSidebar.getByRole("button", {
|
|
||||||
name: "colors.blue.100",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Select a node and right click on it to open context menu
|
|
||||||
await expect(colorNode).toBeVisible();
|
|
||||||
await expect(colorNodeToken).toBeVisible();
|
|
||||||
await colorNode.click({ button: "right" });
|
|
||||||
|
|
||||||
// select "Delete" from the context menu
|
|
||||||
const deleteNodeButton = page.getByRole("button", {
|
|
||||||
name: "Delete",
|
|
||||||
exact: true,
|
|
||||||
});
|
|
||||||
await expect(deleteNodeButton).toBeVisible();
|
|
||||||
await deleteNodeButton.click();
|
|
||||||
|
|
||||||
// Verify that the node is removed
|
|
||||||
await expect(colorNode).not.toBeVisible();
|
|
||||||
// Verify that child token is also removed
|
|
||||||
await expect(colorNodeToken).not.toBeVisible();
|
|
||||||
|
|
||||||
// Save the type button to verify that expands/folds
|
|
||||||
const tokenTypeButton = await tokensSidebar.getByRole("button", {
|
|
||||||
name: "Color",
|
|
||||||
exact: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -332,6 +332,34 @@ const unfoldTokenType = async (tokensTabPanel, type) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createToken = async (page, type, name, textFieldName, value) => {
|
||||||
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
|
||||||
|
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
|
||||||
|
flags: ["enable-token-shadow"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create base token
|
||||||
|
await tokensTabPanel
|
||||||
|
.getByRole("button", { name: `Add Token: ${type}` })
|
||||||
|
.click();
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
|
await nameField.fill(name);
|
||||||
|
|
||||||
|
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||||
|
name: textFieldName,
|
||||||
|
});
|
||||||
|
await colorField.fill(value);
|
||||||
|
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
await submitButton.click();
|
||||||
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
setupEmptyTokensFile,
|
setupEmptyTokensFile,
|
||||||
setupEmptyTokensFileRender,
|
setupEmptyTokensFileRender,
|
||||||
@ -341,4 +369,5 @@ export {
|
|||||||
setupTypographyTokensFileRender,
|
setupTypographyTokensFileRender,
|
||||||
testTokenCreationFlow,
|
testTokenCreationFlow,
|
||||||
unfoldTokenType,
|
unfoldTokenType,
|
||||||
|
createToken,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { test, expect } from "@playwright/test";
|
|||||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||||
import {
|
import {
|
||||||
|
createToken,
|
||||||
setupTokensFileRender,
|
setupTokensFileRender,
|
||||||
setupTypographyTokensFileRender,
|
setupTypographyTokensFileRender,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
@ -14,34 +15,6 @@ test.beforeEach(async ({ page }) => {
|
|||||||
await WasmWorkspacePage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
await WasmWorkspacePage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
const createToken = async (page, type, name, textFieldName, value) => {
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
|
||||||
|
|
||||||
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
|
|
||||||
flags: ["enable-token-shadow"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create base token
|
|
||||||
await tokensTabPanel
|
|
||||||
.getByRole("button", { name: `Add Token: ${type}` })
|
|
||||||
.click();
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill(name);
|
|
||||||
|
|
||||||
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
|
||||||
name: textFieldName,
|
|
||||||
});
|
|
||||||
await colorField.fill(value);
|
|
||||||
|
|
||||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createTokenCombobox = async (page, type, name, textFieldName, value) => {
|
const createTokenCombobox = async (page, type, name, textFieldName, value) => {
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
|
||||||
@ -636,62 +609,6 @@ test.describe("Remapping a single token", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Remapping group of 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 }) => {
|
test("User renames a group - and remaps", async ({ page }) => {
|
||||||
const { tokensSidebar } = await setupTokensFileRender(page);
|
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||||
const workspacePage = new WasmWorkspacePage(page);
|
const workspacePage = new WasmWorkspacePage(page);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||||
import { setupTokensFileRender } from "./helpers";
|
import { createToken, setupTokensFileRender, unfoldTokenType } from "./helpers";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await WasmWorkspacePage.init(page);
|
await WasmWorkspacePage.init(page);
|
||||||
@ -27,4 +27,161 @@ test.describe("Tokens - node tree", () => {
|
|||||||
await tokensColorGroup.click();
|
await tokensColorGroup.click();
|
||||||
await expect(colorToken).not.toBeVisible();
|
await expect(colorToken).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("User renames a group", 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 duplicates a group", 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 "Duplicate" from the context menu
|
||||||
|
const duplicateNodeButton = page.getByRole("button", {
|
||||||
|
name: "Duplicate",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
await expect(duplicateNodeButton).toBeVisible();
|
||||||
|
await duplicateNodeButton.click();
|
||||||
|
|
||||||
|
// Expect the duplicate modal to be visible, fill in the new name and submit
|
||||||
|
const tokenDuplicateNodeModal = page.getByTestId("token-rename-node-modal");
|
||||||
|
await expect(tokenDuplicateNodeModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokenDuplicateNodeModal.getByRole("textbox", {
|
||||||
|
name: "Name",
|
||||||
|
});
|
||||||
|
await nameField.fill("darker");
|
||||||
|
|
||||||
|
const submitButton = tokenDuplicateNodeModal.getByRole("button", {
|
||||||
|
name: "Duplicate",
|
||||||
|
});
|
||||||
|
await submitButton.click();
|
||||||
|
|
||||||
|
// Verify that the node has been duplicated and tokens are visible
|
||||||
|
const darkerNode = tokensSidebar.getByRole("button", {
|
||||||
|
name: "darker",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const darkerNodeToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: "darker.primary",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(darkerNode).toBeVisible();
|
||||||
|
await expect(darkerNodeToken).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("User removes node and all child tokens", async ({ page }) => {
|
||||||
|
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||||
|
|
||||||
|
await expect(tokensSidebar).toBeVisible();
|
||||||
|
|
||||||
|
// Expand color tokens
|
||||||
|
await unfoldTokenType(tokensSidebar, "color");
|
||||||
|
|
||||||
|
// Verify that the node and child token are visible before deletion
|
||||||
|
const colorNode = tokensSidebar.getByRole("button", {
|
||||||
|
name: "colors",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
const colorNodeToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: "colors.blue.100",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select a node and right click on it to open context menu
|
||||||
|
await expect(colorNode).toBeVisible();
|
||||||
|
await expect(colorNodeToken).toBeVisible();
|
||||||
|
await colorNode.click({ button: "right" });
|
||||||
|
|
||||||
|
// select "Delete" from the context menu
|
||||||
|
const deleteNodeButton = page.getByRole("button", {
|
||||||
|
name: "Delete",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
await expect(deleteNodeButton).toBeVisible();
|
||||||
|
await deleteNodeButton.click();
|
||||||
|
|
||||||
|
// Verify that the node is removed
|
||||||
|
await expect(colorNode).not.toBeVisible();
|
||||||
|
// Verify that child token is also removed
|
||||||
|
await expect(colorNodeToken).not.toBeVisible();
|
||||||
|
|
||||||
|
// Save the type button to verify that expands/folds
|
||||||
|
const tokenTypeButton = await tokensSidebar.getByRole("button", {
|
||||||
|
name: "Color",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,6 +11,8 @@
|
|||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.logic.tokens :as clt]
|
[app.common.logic.tokens :as clt]
|
||||||
|
[app.common.path-names :as cpn]
|
||||||
|
[app.common.test-helpers.ids-map :as cthi]
|
||||||
[app.common.types.shape :as cts]
|
[app.common.types.shape :as cts]
|
||||||
[app.common.types.tokens-lib :as ctob]
|
[app.common.types.tokens-lib :as ctob]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
@ -461,6 +463,37 @@
|
|||||||
|
|
||||||
(rx/of (create-token-with-set token)))))))
|
(rx/of (create-token-with-set token)))))))
|
||||||
|
|
||||||
|
(defn bulk-create-tokens
|
||||||
|
[set-id token-ids type node new-node-name]
|
||||||
|
(assert (uuid? set-id) "expected uuid for `set-id`")
|
||||||
|
(assert (every? uuid? token-ids) "expected a collection of uuids for `token-ids`")
|
||||||
|
(assert (keyword? type) "expected keyword for `type`")
|
||||||
|
(assert (string? new-node-name) "expected string for `new-node-name`")
|
||||||
|
|
||||||
|
(ptk/reify ::bulk-create-tokens
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [it state _]
|
||||||
|
(let [token-set (lookup-token-set state set-id)
|
||||||
|
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 (->
|
||||||
|
(cpn/split-path (:name token) :separator ".")
|
||||||
|
(assoc (:depth node) new-node-name)
|
||||||
|
(cpn/join-path :separator "." :with-spaces? false))
|
||||||
|
token' (->> (merge token {:name new-name
|
||||||
|
:id (cthi/new-id! (:name new-name))})
|
||||||
|
(into {})
|
||||||
|
(ctob/make-token))]
|
||||||
|
(pcb/set-token changes (ctob/get-id token-set) (:id token') token')))
|
||||||
|
(-> (pcb/empty-changes it)
|
||||||
|
(pcb/with-library-data data))
|
||||||
|
token-ids)]
|
||||||
|
(rx/of
|
||||||
|
(dch/commit-changes changes)
|
||||||
|
(ptk/data-event ::ev/event {::ev/name "bulk-create-tokens" :type type}))))))
|
||||||
|
|
||||||
(defn update-token
|
(defn update-token
|
||||||
([id params] (update-token nil id params))
|
([id params] (update-token nil id params))
|
||||||
([set-id id params]
|
([set-id id params]
|
||||||
|
|||||||
@ -22,7 +22,6 @@
|
|||||||
[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]))
|
||||||
|
|
||||||
@ -136,8 +135,7 @@
|
|||||||
(fn [tokens-filtered-by-type node]
|
(fn [tokens-filtered-by-type node]
|
||||||
(->> tokens-filtered-by-type
|
(->> tokens-filtered-by-type
|
||||||
(filter (fn [token]
|
(filter (fn [token]
|
||||||
(let [token-path (cpn/split-path (:name token) :separator ".")
|
(let [token-path (cpn/split-path (:name token) :separator ".")]
|
||||||
_ (pp/pprint {:token-path token-path :count (count token-path)})]
|
|
||||||
(and (> (count token-path) 0)
|
(and (> (count token-path) 0)
|
||||||
(str/starts-with? (:name token) (str (:path node) ".")))))))))
|
(str/starts-with? (:name token) (str (:path node) ".")))))))))
|
||||||
|
|
||||||
@ -192,6 +190,8 @@
|
|||||||
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
|
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
|
||||||
(st/emit! (dwtl/close-token-type type))))))
|
(st/emit! (dwtl/close-token-type type))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bulk-rename-tokens-in-path
|
bulk-rename-tokens-in-path
|
||||||
;; Rename tokens in bulk affected by a node rename.
|
;; Rename tokens in bulk affected by a node rename.
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -254,7 +254,6 @@
|
|||||||
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
|
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
|
||||||
tokens-filtered-by-type (get tokens-by-type type)
|
tokens-filtered-by-type (get tokens-by-type type)
|
||||||
tokens-in-current-path (filter-tokens-by-path tokens-filtered-by-type node)
|
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]
|
token-references-count (reduce (fn [count token]
|
||||||
(+ count (remap/count-token-references file-data (:name token))))
|
(+ count (remap/count-token-references file-data (:name token))))
|
||||||
0
|
0
|
||||||
@ -263,6 +262,13 @@
|
|||||||
(on-remap-node-warning node type new-node-name)
|
(on-remap-node-warning node type new-node-name)
|
||||||
(bulk-rename-tokens-in-path node type new-node-name)))))
|
(bulk-rename-tokens-in-path node type new-node-name)))))
|
||||||
|
|
||||||
|
on-duplicate-node
|
||||||
|
(fn [node type new-node-name]
|
||||||
|
(let [tokens-in-path-ids (filter-tokens-by-path-ids type (:path node))]
|
||||||
|
(st/emit!
|
||||||
|
(modal/hide)
|
||||||
|
(dwtl/bulk-create-tokens selected-token-set-id tokens-in-path-ids type node new-node-name))))
|
||||||
|
|
||||||
open-rename-node-modal
|
open-rename-node-modal
|
||||||
;; When user renames a node, we display a form modal
|
;; When user renames a node, we display a form modal
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -271,7 +277,18 @@
|
|||||||
(let [on-rename-node-handler #(on-rename-node node type %)]
|
(let [on-rename-node-handler #(on-rename-node node type %)]
|
||||||
(st/emit! (modal/show :tokens/rename-node {:node node
|
(st/emit! (modal/show :tokens/rename-node {:node node
|
||||||
:tokens-in-active-set selected-token-set-tokens
|
:tokens-in-active-set selected-token-set-tokens
|
||||||
:on-rename on-rename-node-handler})))))]
|
:on-rename on-rename-node-handler})))))
|
||||||
|
|
||||||
|
open-duplicate-node-modal
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps selected-token-set-tokens on-duplicate-node)
|
||||||
|
(fn [node type]
|
||||||
|
(let [on-duplicate-node-handler #(on-duplicate-node node type %)]
|
||||||
|
(st/emit! (modal/show :tokens/rename-node {:new-node-name (str (:name node) "-copy")
|
||||||
|
:node node
|
||||||
|
:variant "duplicate"
|
||||||
|
:tokens-in-active-set selected-token-set-tokens
|
||||||
|
:on-rename on-duplicate-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
|
||||||
@ -286,6 +303,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:& token-context-menu {:on-delete-token delete-token}]
|
[:& token-context-menu {:on-delete-token delete-token}]
|
||||||
[:> token-node-context-menu* {:on-rename-node open-rename-node-modal
|
[:> token-node-context-menu* {:on-rename-node open-rename-node-modal
|
||||||
|
:on-duplicate-node open-duplicate-node-modal
|
||||||
:on-delete-node delete-node}]
|
:on-delete-node delete-node}]
|
||||||
|
|
||||||
[:> selected-set-info* {:tokens-lib tokens-lib
|
[:> selected-set-info* {:tokens-lib tokens-lib
|
||||||
|
|||||||
@ -18,15 +18,15 @@
|
|||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(mf/defc rename-node-form*
|
(mf/defc rename-node-form*
|
||||||
[{:keys [node active-tokens tokens-tree on-close on-submit]}]
|
[{:keys [new-node-name node active-tokens tokens-tree variant on-close on-submit]}]
|
||||||
(let [make-schema #(cfo/make-node-token-schema active-tokens tokens-tree node)
|
(let [make-schema #(cfo/make-node-token-schema active-tokens tokens-tree node)
|
||||||
|
|
||||||
schema
|
schema
|
||||||
(mf/with-memo [active-tokens]
|
(mf/with-memo [active-tokens]
|
||||||
(make-schema))
|
(make-schema))
|
||||||
|
|
||||||
initial (mf/with-memo [node]
|
initial (mf/with-memo [node new-node-name]
|
||||||
{:name (:name node)})
|
{:name (d/nilv new-node-name (:name node))})
|
||||||
|
|
||||||
form (fm/use-form :schema schema
|
form (fm/use-form :schema schema
|
||||||
:initial initial)
|
:initial initial)
|
||||||
@ -35,11 +35,10 @@
|
|||||||
(mf/deps form on-submit)
|
(mf/deps form on-submit)
|
||||||
(fn []
|
(fn []
|
||||||
(let [name (get-in @form [:clean-data :name])]
|
(let [name (get-in @form [:clean-data :name])]
|
||||||
(when (and (get-in @form [:touched :name]) (not= name (:name node)))
|
(when (not= name (:name node))
|
||||||
(on-submit name)))))
|
(on-submit name)))))
|
||||||
|
|
||||||
is-disabled? (or (not (:valid @form))
|
is-disabled? (or (not (:valid @form))
|
||||||
(not (get-in @form [:touched :name]))
|
|
||||||
(= (get-in @form [:clean-data :name]) (:name node)))
|
(= (get-in @form [:clean-data :name]) (:name node)))
|
||||||
|
|
||||||
hint-path (mf/with-memo [@form node]
|
hint-path (mf/with-memo [@form node]
|
||||||
@ -64,7 +63,7 @@
|
|||||||
:max-length 255
|
:max-length 255
|
||||||
:variant "comfortable"
|
:variant "comfortable"
|
||||||
:hint-type "hint"
|
:hint-type "hint"
|
||||||
:hint-message (tr "workspace.tokens.rename-group-name-hint" hint-path)
|
:hint-message (when (= variant "rename") (tr "workspace.tokens.rename-group-name-hint" hint-path))
|
||||||
:auto-focus true}]
|
:auto-focus true}]
|
||||||
[:div {:class (stl/css :form-actions)}
|
[:div {:class (stl/css :form-actions)}
|
||||||
[:> button* {:variant "secondary"
|
[:> button* {:variant "secondary"
|
||||||
@ -72,14 +71,16 @@
|
|||||||
:on-click on-close} (tr "labels.cancel")]
|
:on-click on-close} (tr "labels.cancel")]
|
||||||
[:> fc/form-submit* {:variant "primary"
|
[:> fc/form-submit* {:variant "primary"
|
||||||
:disabled is-disabled?
|
:disabled is-disabled?
|
||||||
:name "rename"} (tr "labels.rename")]]]))
|
:name "rename"} (if (= variant "rename") (tr "labels.rename") (tr "labels.duplicate"))]]]))
|
||||||
|
|
||||||
(mf/defc rename-node-modal
|
(mf/defc rename-node-modal
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
::mf/register-as :tokens/rename-node}
|
::mf/register-as :tokens/rename-node}
|
||||||
[{:keys [node tokens-in-active-set on-rename]}]
|
[{:keys [new-node-name node tokens-in-active-set on-rename variant]}]
|
||||||
|
|
||||||
(let [tokens-tree-in-selected-set
|
(let [variant (d/nilv variant "rename") ;; "rename" or "duplicate"
|
||||||
|
|
||||||
|
tokens-tree-in-selected-set
|
||||||
(mf/with-memo [tokens-in-active-set node]
|
(mf/with-memo [tokens-in-active-set node]
|
||||||
(-> (ctob/tokens-tree tokens-in-active-set)
|
(-> (ctob/tokens-tree tokens-in-active-set)
|
||||||
(d/dissoc-in (:name node))))
|
(d/dissoc-in (:name node))))
|
||||||
@ -111,7 +112,9 @@
|
|||||||
:aria-label (tr "labels.close")
|
:aria-label (tr "labels.close")
|
||||||
:variant "ghost"
|
:variant "ghost"
|
||||||
:icon i/close}]
|
:icon i/close}]
|
||||||
[:> rename-node-form* {:node node
|
[:> rename-node-form* {:new-node-name new-node-name
|
||||||
|
:node node
|
||||||
|
:variant variant
|
||||||
:active-tokens tokens-in-active-set
|
:active-tokens tokens-in-active-set
|
||||||
:tokens-tree tokens-tree-in-selected-set
|
:tokens-tree tokens-tree-in-selected-set
|
||||||
:on-close close-modal
|
:on-close close-modal
|
||||||
|
|||||||
@ -27,7 +27,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-rename-node on-delete-node]}]
|
[{:keys [on-rename-node on-duplicate-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)
|
||||||
@ -44,6 +44,13 @@
|
|||||||
type (get mdata :type)]
|
type (get mdata :type)]
|
||||||
(when node
|
(when node
|
||||||
(on-rename-node node type)))))
|
(on-rename-node node type)))))
|
||||||
|
duplicate-node (mf/use-fn
|
||||||
|
(mf/deps mdata on-duplicate-node)
|
||||||
|
(fn []
|
||||||
|
(let [node (get mdata :node)
|
||||||
|
type (get mdata :type)]
|
||||||
|
(when node
|
||||||
|
(on-duplicate-node node type)))))
|
||||||
|
|
||||||
container (hooks/use-portal-container)
|
container (hooks/use-portal-container)
|
||||||
delete-node (mf/use-fn
|
delete-node (mf/use-fn
|
||||||
@ -90,6 +97,11 @@
|
|||||||
:type "button"
|
:type "button"
|
||||||
:on-click rename-node}
|
:on-click rename-node}
|
||||||
(tr "labels.rename")]]
|
(tr "labels.rename")]]
|
||||||
|
[:li {:class (stl/css :token-node-context-menu-listitem)}
|
||||||
|
[:button {:class (stl/css :token-node-context-menu-action)
|
||||||
|
:type "button"
|
||||||
|
:on-click duplicate-node}
|
||||||
|
(tr "labels.duplicate")]]
|
||||||
[: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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user