mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +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)
|
||||
- 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)
|
||||
- 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)
|
||||
- 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)
|
||||
@ -84,7 +85,6 @@
|
||||
- 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)
|
||||
|
||||
|
||||
## 2.14.1
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
@ -108,7 +108,6 @@
|
||||
- Ensure path content is always PathData when saving
|
||||
- Fix error when get-parent-with-data encounters non-Element nodes
|
||||
|
||||
|
||||
## 2.14.0
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
@ -2011,48 +2011,4 @@ test.describe("Tokens tab - delete", () => {
|
||||
await expect(tokenContextMenuForToken).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 {
|
||||
setupEmptyTokensFile,
|
||||
setupEmptyTokensFileRender,
|
||||
@ -341,4 +369,5 @@ export {
|
||||
setupTypographyTokensFileRender,
|
||||
testTokenCreationFlow,
|
||||
unfoldTokenType,
|
||||
createToken,
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import { test, expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import {
|
||||
createToken,
|
||||
setupTokensFileRender,
|
||||
setupTypographyTokensFileRender,
|
||||
} from "./helpers";
|
||||
@ -14,34 +15,6 @@ test.beforeEach(async ({ page }) => {
|
||||
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 tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
@ -636,62 +609,6 @@ test.describe("Remapping a single token", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||
import { setupTokensFileRender } from "./helpers";
|
||||
import { createToken, setupTokensFileRender, unfoldTokenType } from "./helpers";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
@ -27,4 +27,161 @@ test.describe("Tokens - node tree", () => {
|
||||
await tokensColorGroup.click();
|
||||
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.geom.point :as gpt]
|
||||
[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.tokens-lib :as ctob]
|
||||
[app.common.uuid :as uuid]
|
||||
@ -461,6 +463,37 @@
|
||||
|
||||
(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
|
||||
([id params] (update-token nil 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.util.array :as array]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cljs.pprint :as pp]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@ -136,8 +135,7 @@
|
||||
(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)})]
|
||||
(let [token-path (cpn/split-path (:name token) :separator ".")]
|
||||
(and (> (count token-path) 0)
|
||||
(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/close-token-type type))))))
|
||||
|
||||
|
||||
|
||||
bulk-rename-tokens-in-path
|
||||
;; Rename tokens in bulk affected by a node rename.
|
||||
(mf/use-fn
|
||||
@ -254,7 +254,6 @@
|
||||
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
|
||||
@ -263,6 +262,13 @@
|
||||
(on-remap-node-warning 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
|
||||
;; When user renames a node, we display a form modal
|
||||
(mf/use-fn
|
||||
@ -271,7 +277,18 @@
|
||||
(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})))))]
|
||||
: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]
|
||||
(when (and tokens-lib
|
||||
@ -286,6 +303,7 @@
|
||||
[:*
|
||||
[:& token-context-menu {:on-delete-token delete-token}]
|
||||
[:> token-node-context-menu* {:on-rename-node open-rename-node-modal
|
||||
:on-duplicate-node open-duplicate-node-modal
|
||||
:on-delete-node delete-node}]
|
||||
|
||||
[:> selected-set-info* {:tokens-lib tokens-lib
|
||||
|
||||
@ -18,15 +18,15 @@
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(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)
|
||||
|
||||
schema
|
||||
(mf/with-memo [active-tokens]
|
||||
(make-schema))
|
||||
|
||||
initial (mf/with-memo [node]
|
||||
{:name (:name node)})
|
||||
initial (mf/with-memo [node new-node-name]
|
||||
{:name (d/nilv new-node-name (:name node))})
|
||||
|
||||
form (fm/use-form :schema schema
|
||||
:initial initial)
|
||||
@ -35,11 +35,10 @@
|
||||
(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)))
|
||||
(when (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)))
|
||||
|
||||
hint-path (mf/with-memo [@form node]
|
||||
@ -64,7 +63,7 @@
|
||||
:max-length 255
|
||||
:variant "comfortable"
|
||||
: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}]
|
||||
[:div {:class (stl/css :form-actions)}
|
||||
[:> button* {:variant "secondary"
|
||||
@ -72,14 +71,16 @@
|
||||
:on-click on-close} (tr "labels.cancel")]
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
: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/register modal/components
|
||||
::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]
|
||||
(-> (ctob/tokens-tree tokens-in-active-set)
|
||||
(d/dissoc-in (:name node))))
|
||||
@ -111,7 +112,9 @@
|
||||
:aria-label (tr "labels.close")
|
||||
:variant "ghost"
|
||||
: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
|
||||
:tokens-tree tokens-tree-in-selected-set
|
||||
:on-close close-modal
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
(mf/defc 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)
|
||||
is-open? (boolean mdata)
|
||||
dropdown-ref (mf/use-ref)
|
||||
@ -44,6 +44,13 @@
|
||||
type (get mdata :type)]
|
||||
(when node
|
||||
(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)
|
||||
delete-node (mf/use-fn
|
||||
@ -90,6 +97,11 @@
|
||||
:type "button"
|
||||
:on-click rename-node}
|
||||
(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)}
|
||||
[:button {:class (stl/css :token-node-context-menu-action)
|
||||
:type "button"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user