mirror of
https://github.com/penpot/penpot.git
synced 2026-08-05 20:38:38 +00:00
✨ Remove tokens tree node (#8042)
This commit is contained in:
parent
0ecb2bc838
commit
368fa954ce
@ -14,7 +14,7 @@ test.beforeEach(async ({ page }) => {
|
|||||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Tokens - CRUD", () => {
|
test.describe("Tokens - creation", () => {
|
||||||
test("User creates border radius token", async ({ page }) => {
|
test("User creates border radius token", async ({ page }) => {
|
||||||
await testTokenCreationFlow(page, {
|
await testTokenCreationFlow(page, {
|
||||||
tokenLabel: "Border Radius",
|
tokenLabel: "Border Radius",
|
||||||
@ -1256,6 +1256,91 @@ test.describe("Tokens - CRUD", () => {
|
|||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("User creates grouped color token", async ({ page }) => {
|
||||||
|
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
|
||||||
|
await setupEmptyTokensFile(page);
|
||||||
|
|
||||||
|
await tokensSidebar
|
||||||
|
.getByRole("button", { name: "Add Token: Color" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Create grouped color token with mouse
|
||||||
|
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
|
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||||
|
|
||||||
|
await nameField.click();
|
||||||
|
await nameField.fill("dark.primary");
|
||||||
|
|
||||||
|
await valueField.click();
|
||||||
|
await valueField.fill("red");
|
||||||
|
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
await expect(submitButton).toBeEnabled();
|
||||||
|
await submitButton.click();
|
||||||
|
|
||||||
|
await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
|
||||||
|
|
||||||
|
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("User cant create regular token with value missing", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
|
||||||
|
|
||||||
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
await tokensTabPanel
|
||||||
|
.getByRole("button", { name: "Add Token: Color" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initially submit button should be disabled
|
||||||
|
await expect(submitButton).toBeDisabled();
|
||||||
|
|
||||||
|
// Fill in name but leave value empty
|
||||||
|
await nameField.click();
|
||||||
|
await nameField.fill("primary");
|
||||||
|
|
||||||
|
// Submit button should remain disabled when value is empty
|
||||||
|
await expect(submitButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("User duplicate color token", async ({ page }) => {
|
||||||
|
const { tokensSidebar, tokenContextMenuForToken } =
|
||||||
|
await setupTokensFile(page);
|
||||||
|
|
||||||
|
await expect(tokensSidebar).toBeVisible();
|
||||||
|
|
||||||
|
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
||||||
|
|
||||||
|
const colorToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: "100",
|
||||||
|
});
|
||||||
|
|
||||||
|
await colorToken.click({ button: "right" });
|
||||||
|
await expect(tokenContextMenuForToken).toBeVisible();
|
||||||
|
|
||||||
|
await tokenContextMenuForToken.getByText("Duplicate token").click();
|
||||||
|
await expect(tokenContextMenuForToken).not.toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Tokens tab - edition", () => {
|
||||||
test("User edits typography token and all fields are valid", async ({
|
test("User edits typography token and all fields are valid", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
@ -1388,67 +1473,7 @@ test.describe("Tokens - CRUD", () => {
|
|||||||
await expect(colorTokenChanged).toBeVisible();
|
await expect(colorTokenChanged).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("User creates grouped color token", async ({ page }) => {
|
test("User edits color token color while keeping custom color space", async ({
|
||||||
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
|
|
||||||
await setupEmptyTokensFile(page);
|
|
||||||
|
|
||||||
await tokensSidebar
|
|
||||||
.getByRole("button", { name: "Add Token: Color" })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Create grouped color token with mouse
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
|
||||||
|
|
||||||
await nameField.click();
|
|
||||||
await nameField.fill("dark.primary");
|
|
||||||
|
|
||||||
await valueField.click();
|
|
||||||
await valueField.fill("red");
|
|
||||||
|
|
||||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await expect(submitButton).toBeEnabled();
|
|
||||||
await submitButton.click();
|
|
||||||
|
|
||||||
await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
|
|
||||||
|
|
||||||
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User cant create regular token with value missing", async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
|
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
|
||||||
await tokensTabPanel
|
|
||||||
.getByRole("button", { name: "Add Token: Color" })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initially submit button should be disabled
|
|
||||||
await expect(submitButton).toBeDisabled();
|
|
||||||
|
|
||||||
// Fill in name but leave value empty
|
|
||||||
await nameField.click();
|
|
||||||
await nameField.fill("primary");
|
|
||||||
|
|
||||||
// Submit button should remain disabled when value is empty
|
|
||||||
await expect(submitButton).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User changes color token color while keeping custom color space", async ({
|
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||||
@ -1502,30 +1527,9 @@ test.describe("Tokens - CRUD", () => {
|
|||||||
await valueSaturationSelector.click({ position: { x: 0, y: 0 } });
|
await valueSaturationSelector.click({ position: { x: 0, y: 0 } });
|
||||||
await expect(valueField).toHaveValue(/^rgba(.*)$/);
|
await expect(valueField).toHaveValue(/^rgba(.*)$/);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("User duplicate color token", async ({ page }) => {
|
test.describe("Tokens tab - delete", () => {
|
||||||
const { tokensSidebar, tokenContextMenuForToken } =
|
|
||||||
await setupTokensFile(page);
|
|
||||||
|
|
||||||
await expect(tokensSidebar).toBeVisible();
|
|
||||||
|
|
||||||
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
|
||||||
|
|
||||||
const colorToken = tokensSidebar.getByRole("button", {
|
|
||||||
name: "100",
|
|
||||||
});
|
|
||||||
|
|
||||||
await colorToken.click({ button: "right" });
|
|
||||||
await expect(tokenContextMenuForToken).toBeVisible();
|
|
||||||
|
|
||||||
await tokenContextMenuForToken.getByText("Duplicate token").click();
|
|
||||||
await expect(tokenContextMenuForToken).not.toBeVisible();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User delete color token", async ({ page }) => {
|
test("User delete color token", async ({ page }) => {
|
||||||
const { tokensSidebar, tokenContextMenuForToken } =
|
const { tokensSidebar, tokenContextMenuForToken } =
|
||||||
await setupTokensFile(page);
|
await setupTokensFile(page);
|
||||||
@ -1546,4 +1550,40 @@ test.describe("Tokens - CRUD", () => {
|
|||||||
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, workspacePage } = await setupTokensFile(page);
|
||||||
|
|
||||||
|
await expect(tokensSidebar).toBeVisible();
|
||||||
|
|
||||||
|
// Expand color tokens
|
||||||
|
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
||||||
|
|
||||||
|
// Verify that the node and child token are visible before deletion
|
||||||
|
const colorNode = tokensSidebar.getByRole("button", {
|
||||||
|
name: "blue",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
const colorNodeToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: "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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -433,11 +433,22 @@
|
|||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [it state _]
|
(watch [it state _]
|
||||||
(let [data (dsh/lookup-file-data state)
|
(let [data (dsh/lookup-file-data state)
|
||||||
|
|
||||||
changes (-> (pcb/empty-changes it)
|
changes (-> (pcb/empty-changes it)
|
||||||
(pcb/with-library-data data)
|
(pcb/with-library-data data)
|
||||||
(pcb/set-token set-id token-id nil))]
|
(pcb/set-token set-id token-id nil))]
|
||||||
(rx/of (dch/commit-changes changes))))))
|
(rx/of (dch/commit-changes changes))))))
|
||||||
|
|
||||||
|
(defn bulk-delete-tokens
|
||||||
|
[set-id token-ids]
|
||||||
|
(dm/assert! (uuid? set-id))
|
||||||
|
(dm/assert! (every? uuid? token-ids))
|
||||||
|
(ptk/reify ::bulk-delete-tokens
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(apply rx/of
|
||||||
|
(map #(delete-token set-id %) token-ids)))))
|
||||||
|
|
||||||
(defn duplicate-token
|
(defn duplicate-token
|
||||||
[token-id]
|
[token-id]
|
||||||
(dm/assert! (uuid? token-id))
|
(dm/assert! (uuid? token-id))
|
||||||
@ -505,6 +516,19 @@
|
|||||||
(update state :workspace-tokens assoc :token-context-menu params)
|
(update state :workspace-tokens assoc :token-context-menu params)
|
||||||
(update state :workspace-tokens dissoc :token-context-menu)))))
|
(update state :workspace-tokens dissoc :token-context-menu)))))
|
||||||
|
|
||||||
|
(defn assign-token-node-context-menu
|
||||||
|
[{:keys [position] :as params}]
|
||||||
|
|
||||||
|
(when params
|
||||||
|
(assert (gpt/point? position) "expected a point instance for `position` param"))
|
||||||
|
|
||||||
|
(ptk/reify ::show-token-node-context-menu
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(if params
|
||||||
|
(update state :workspace-tokens assoc :token-node-context-menu params)
|
||||||
|
(update state :workspace-tokens dissoc :token-node-context-menu)))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; TOKEN-SET UI OPS
|
;; TOKEN-SET UI OPS
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -19,17 +19,19 @@
|
|||||||
[:expandable {:optional true} :boolean]
|
[:expandable {:optional true} :boolean]
|
||||||
[:expanded {:optional true} :boolean]
|
[:expanded {:optional true} :boolean]
|
||||||
[:icon {:optional true} :string]
|
[:icon {:optional true} :string]
|
||||||
[:on-toggle-expand fn?]])
|
[:on-toggle-expand {:optional true} fn?]
|
||||||
|
[:on-context-menu {:optional true} fn?]])
|
||||||
|
|
||||||
(mf/defc layer-button*
|
(mf/defc layer-button*
|
||||||
{::mf/schema schema:layer-button}
|
{::mf/schema schema:layer-button}
|
||||||
[{:keys [label description class is-expandable expanded icon on-toggle-expand children] :rest props}]
|
[{:keys [label description class is-expandable expanded icon on-toggle-expand on-context-menu children] :rest props}]
|
||||||
(let [button-props (mf/spread-props props
|
(let [button-props (mf/spread-props props
|
||||||
{:class [class (stl/css-case :layer-button true
|
{:class [class (stl/css-case :layer-button true
|
||||||
:layer-button--expandable is-expandable
|
:layer-button--expandable is-expandable
|
||||||
:layer-button--expanded expanded)]
|
:layer-button--expanded expanded)]
|
||||||
:type "button"
|
:type "button"
|
||||||
:on-click on-toggle-expand})]
|
:on-click on-toggle-expand
|
||||||
|
:on-context-menu on-context-menu})]
|
||||||
[:div {:class (stl/css :layer-button-wrapper)}
|
[:div {:class (stl/css :layer-button-wrapper)}
|
||||||
[:> "button" button-props
|
[:> "button" button-props
|
||||||
[:div {:class (stl/css :layer-button-content)}
|
[:div {:class (stl/css :layer-button-content)}
|
||||||
|
|||||||
@ -14,8 +14,10 @@
|
|||||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||||
[app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]]
|
[app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]]
|
||||||
[app.main.ui.workspace.tokens.management.group :refer [token-group*]]
|
[app.main.ui.workspace.tokens.management.group :refer [token-group*]]
|
||||||
|
[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]]
|
||||||
|
[cuerdas.core :as str]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(defn- get-sorted-token-groups
|
(defn- get-sorted-token-groups
|
||||||
@ -120,7 +122,27 @@
|
|||||||
|
|
||||||
[empty-group filled-group]
|
[empty-group filled-group]
|
||||||
(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 their ids
|
||||||
|
filter-tokens-by-path-ids
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps tokens)
|
||||||
|
(fn [type path]
|
||||||
|
(->> tokens
|
||||||
|
(filter (fn [token]
|
||||||
|
(let [[_ token-value] token]
|
||||||
|
(and (= (:type token-value) type) (str/starts-with? (:name token-value) path)))))
|
||||||
|
(mapv (fn [token]
|
||||||
|
(let [[_ token-value] token]
|
||||||
|
(:id token-value)))))))
|
||||||
|
|
||||||
|
delete-node
|
||||||
|
(mf/with-memo [tokens selected-token-set-id]
|
||||||
|
(fn [node type]
|
||||||
|
(let [path (:path node)
|
||||||
|
tokens-in-path-ids (filter-tokens-by-path-ids type path)]
|
||||||
|
(st/emit! (dwtl/bulk-delete-tokens selected-token-set-id tokens-in-path-ids)))))]
|
||||||
|
|
||||||
(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
|
||||||
@ -134,6 +156,7 @@
|
|||||||
|
|
||||||
[:*
|
[:*
|
||||||
[:& token-context-menu]
|
[:& token-context-menu]
|
||||||
|
[:> token-node-context-menu* {: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}]
|
||||||
|
|||||||
@ -88,7 +88,7 @@
|
|||||||
|
|
||||||
expandable? (d/nilv (seq tokens) false)
|
expandable? (d/nilv (seq tokens) false)
|
||||||
|
|
||||||
on-context-menu
|
on-pill-context-menu
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [event token]
|
(fn [event token]
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
@ -98,6 +98,15 @@
|
|||||||
:errors (:errors token)
|
:errors (:errors token)
|
||||||
:token-id (:id token)}))))
|
:token-id (:id token)}))))
|
||||||
|
|
||||||
|
on-node-context-menu
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [event node]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(st/emit! (dwtl/assign-token-node-context-menu
|
||||||
|
{:node node
|
||||||
|
:type type
|
||||||
|
:position (dom/get-client-position event)}))))
|
||||||
|
|
||||||
on-toggle-open-click
|
on-toggle-open-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps type expandable?)
|
(mf/deps type expandable?)
|
||||||
@ -159,4 +168,5 @@
|
|||||||
:selected-token-set-id selected-token-set-id
|
:selected-token-set-id selected-token-set-id
|
||||||
:is-selected-inside-layout is-selected-inside-layout
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
:on-token-pill-click on-token-pill-click
|
:on-token-pill-click on-token-pill-click
|
||||||
:on-context-menu on-context-menu}])]))
|
:on-pill-context-menu on-pill-context-menu
|
||||||
|
:on-node-context-menu on-node-context-menu}])]))
|
||||||
|
|||||||
@ -0,0 +1,83 @@
|
|||||||
|
(ns app.main.ui.workspace.tokens.management.node-context-menu
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||||
|
[app.main.refs :as refs]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.i18n :refer [tr]]
|
||||||
|
[okulary.core :as l]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(def ^:private schema:token-node-context-menu
|
||||||
|
[:map
|
||||||
|
[:on-delete-node fn?]])
|
||||||
|
|
||||||
|
(def ^:private tokens-node-menu-ref
|
||||||
|
(l/derived :token-node-context-menu refs/workspace-tokens))
|
||||||
|
|
||||||
|
(defn- prevent-default
|
||||||
|
[event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(dom/stop-propagation event))
|
||||||
|
|
||||||
|
(mf/defc token-node-context-menu*
|
||||||
|
{::mf/schema schema:token-node-context-menu}
|
||||||
|
[{:keys [on-delete-node]}]
|
||||||
|
(let [mdata (mf/deref tokens-node-menu-ref)
|
||||||
|
is-open? (boolean mdata)
|
||||||
|
dropdown-ref (mf/use-ref)
|
||||||
|
dropdown-action (mf/use-ref)
|
||||||
|
dropdown-direction* (mf/use-state "down")
|
||||||
|
dropdown-direction (deref dropdown-direction*)
|
||||||
|
dropdown-direction-change* (mf/use-ref 0)
|
||||||
|
top (+ (get-in mdata [:position :y]) 5)
|
||||||
|
left (+ (get-in mdata [:position :x]) 5)
|
||||||
|
|
||||||
|
delete-node (mf/use-fn
|
||||||
|
(mf/deps mdata)
|
||||||
|
(fn []
|
||||||
|
(let [node (get mdata :node)
|
||||||
|
type (get mdata :type)]
|
||||||
|
(when node
|
||||||
|
(on-delete-node node type)))))]
|
||||||
|
|
||||||
|
(mf/with-effect [is-open?]
|
||||||
|
(when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?))
|
||||||
|
(reset! dropdown-direction* "down")
|
||||||
|
(mf/set-ref-val! dropdown-direction-change* 0)))
|
||||||
|
|
||||||
|
(mf/with-effect [is-open? dropdown-ref dropdown-action]
|
||||||
|
(let [dropdown-element (mf/ref-val dropdown-ref)]
|
||||||
|
(when (and (= 0 (mf/ref-val dropdown-direction-change*)) dropdown-element)
|
||||||
|
(let [is-outside? (dom/is-element-outside? dropdown-element)]
|
||||||
|
(reset! dropdown-direction* (if is-outside? "up" "down"))
|
||||||
|
(mf/set-ref-val! dropdown-direction-change* (inc (mf/ref-val dropdown-direction-change*)))))))
|
||||||
|
|
||||||
|
;; FIXME: perf optimization
|
||||||
|
|
||||||
|
(when is-open?
|
||||||
|
(mf/portal
|
||||||
|
(mf/html
|
||||||
|
[:& dropdown {:show is-open?
|
||||||
|
:on-close #(st/emit! (dwtl/assign-token-node-context-menu nil))}
|
||||||
|
[:div {:class (stl/css :token-node-context-menu)
|
||||||
|
:data-testid "tokens-context-menu-for-token-node"
|
||||||
|
:ref dropdown-ref
|
||||||
|
:data-direction dropdown-direction
|
||||||
|
:style {:--bottom (if (= dropdown-direction "up")
|
||||||
|
"40px"
|
||||||
|
"unset")
|
||||||
|
:--top (dm/str top "px")
|
||||||
|
:left (dm/str left "px")}
|
||||||
|
:on-context-menu prevent-default}
|
||||||
|
(when mdata
|
||||||
|
[: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 delete-node}
|
||||||
|
(tr "labels.delete")]]])]])
|
||||||
|
(dom/get-body)))))
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
// 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/_utils.scss" as *;
|
||||||
|
@use "ds/_sizes.scss" as *;
|
||||||
|
@use "ds/_borders.scss" as *;
|
||||||
|
@use "ds/typography.scss" as t;
|
||||||
|
@use "ds/spacing.scss" as *;
|
||||||
|
@use "ds/mixins.scss" as *;
|
||||||
|
|
||||||
|
.token-node-context-menu {
|
||||||
|
--menu-inline-size: #{px2rem(240)};
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
z-index: var(--z-index-dropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-node-context-menu[data-direction="up"] {
|
||||||
|
bottom: var(--bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-node-context-menu[data-direction="down"] {
|
||||||
|
top: var(--top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-node-context-menu-list {
|
||||||
|
inline-size: var(--menu-inline-size);
|
||||||
|
padding: var(--sp-xs);
|
||||||
|
border-radius: $br-8;
|
||||||
|
border: $b-2 solid var(--color-background-quaternary);
|
||||||
|
background-color: var(--color-background-tertiary);
|
||||||
|
max-block-size: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0px 0px $sz-12 0px var(--menu-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-node-context-menu-action {
|
||||||
|
--context-menu-item-bg-color: none;
|
||||||
|
--context-menu-item-fg-color: var(--color-foreground-primary);
|
||||||
|
--context-menu-item-border-color: none;
|
||||||
|
|
||||||
|
@include t.use-typography("body-small");
|
||||||
|
appearance: none;
|
||||||
|
background: var(--context-menu-item-bg-color);
|
||||||
|
border: $b-1 solid var(--context-menu-item-border-color);
|
||||||
|
color: var(--context-menu-item-fg-color);
|
||||||
|
border-radius: $br-8;
|
||||||
|
cursor: pointer;
|
||||||
|
block-size: px2rem(32);
|
||||||
|
inline-size: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--sp-xs);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--context-menu-item-bg-color: var(--color-background-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
--context-menu-item-bg-color: var(--menu-background-color-focus);
|
||||||
|
--context-menu-item-border-color: var(--color-background-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-selected="true"] {
|
||||||
|
--context-menu-item-bg-color: var(--color-background-quaternary);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@
|
|||||||
[app.common.path-names :as cpn]
|
[app.common.path-names :as cpn]
|
||||||
[app.common.types.tokens-lib :as ctob]
|
[app.common.types.tokens-lib :as ctob]
|
||||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||||
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.ds.layers.layer-button :refer [layer-button*]]
|
[app.main.ui.ds.layers.layer-button :refer [layer-button*]]
|
||||||
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
|
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
|
||||||
@ -26,7 +27,8 @@
|
|||||||
[:selected-token-set-id {:optional true} :any]
|
[:selected-token-set-id {:optional true} :any]
|
||||||
[:tokens-lib {:optional true} :any]
|
[:tokens-lib {:optional true} :any]
|
||||||
[:on-token-pill-click {:optional true} fn?]
|
[:on-token-pill-click {:optional true} fn?]
|
||||||
[:on-context-menu {:optional true} fn?]])
|
[:on-pill-context-menu {:optional true} fn?]
|
||||||
|
[:on-node-context-menu {:optional true} fn?]])
|
||||||
|
|
||||||
(mf/defc folder-node*
|
(mf/defc folder-node*
|
||||||
{::mf/schema schema:folder-node}
|
{::mf/schema schema:folder-node}
|
||||||
@ -39,22 +41,29 @@
|
|||||||
selected-token-set-id
|
selected-token-set-id
|
||||||
tokens-lib
|
tokens-lib
|
||||||
on-token-pill-click
|
on-token-pill-click
|
||||||
on-context-menu]}]
|
on-pill-context-menu
|
||||||
|
on-node-context-menu]}]
|
||||||
(let [full-path (str (name type) "." (:path node))
|
(let [full-path (str (name type) "." (:path node))
|
||||||
is-folder-expanded (contains? (set (or unfolded-token-paths [])) full-path)
|
is-folder-expanded (contains? (set (or unfolded-token-paths [])) full-path)
|
||||||
|
|
||||||
swap-folder-expanded (mf/use-fn
|
swap-folder-expanded (mf/use-fn
|
||||||
(mf/deps (:path node) type)
|
(mf/deps (:path node) type)
|
||||||
(fn []
|
(fn []
|
||||||
(let [path (str (name type) "." (:path node))]
|
(let [path (str (name type) "." (:path node))]
|
||||||
(st/emit! (dwtl/toggle-token-path path)))))]
|
(st/emit! (dwtl/toggle-token-path path)))))
|
||||||
|
|
||||||
|
node-context-menu-prep (mf/use-fn
|
||||||
|
(mf/deps on-node-context-menu node)
|
||||||
|
(fn [event]
|
||||||
|
(when on-node-context-menu
|
||||||
|
(on-node-context-menu event node))))]
|
||||||
[:li {:class (stl/css :folder-node)}
|
[:li {:class (stl/css :folder-node)}
|
||||||
[:> layer-button* {:label (:name node)
|
[:> layer-button* {:label (:name node)
|
||||||
:expanded is-folder-expanded
|
:expanded is-folder-expanded
|
||||||
:aria-expanded is-folder-expanded
|
:aria-expanded is-folder-expanded
|
||||||
:aria-controls (str "folder-children-" (:path node))
|
:aria-controls (str "folder-children-" (:path node))
|
||||||
:is-expandable (not (:leaf node))
|
:is-expandable (not (:leaf node))
|
||||||
:on-toggle-expand swap-folder-expanded}]
|
:on-toggle-expand swap-folder-expanded
|
||||||
|
:on-context-menu node-context-menu-prep}]
|
||||||
(when is-folder-expanded
|
(when is-folder-expanded
|
||||||
(let [children-fn (:children-fn node)]
|
(let [children-fn (:children-fn node)]
|
||||||
[:div {:class (stl/css :folder-children-wrapper)
|
[:div {:class (stl/css :folder-children-wrapper)
|
||||||
@ -63,16 +72,17 @@
|
|||||||
(let [children (children-fn)]
|
(let [children (children-fn)]
|
||||||
(for [child children]
|
(for [child children]
|
||||||
(if (not (:leaf child))
|
(if (not (:leaf child))
|
||||||
[:ul {:class (stl/css :node-parent)}
|
[:ul {:class (stl/css :node-parent)
|
||||||
[:> folder-node* {:key (:path child)
|
:key (:path child)}
|
||||||
:type type
|
[:> folder-node* {:type type
|
||||||
:node child
|
:node child
|
||||||
:unfolded-token-paths unfolded-token-paths
|
:unfolded-token-paths unfolded-token-paths
|
||||||
:selected-shapes selected-shapes
|
:selected-shapes selected-shapes
|
||||||
:is-selected-inside-layout is-selected-inside-layout
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
:active-theme-tokens active-theme-tokens
|
:active-theme-tokens active-theme-tokens
|
||||||
:on-token-pill-click on-token-pill-click
|
:on-token-pill-click on-token-pill-click
|
||||||
:on-context-menu on-context-menu
|
:on-pill-context-menu on-pill-context-menu
|
||||||
|
:on-node-context-menu on-node-context-menu
|
||||||
:tokens-lib tokens-lib
|
:tokens-lib tokens-lib
|
||||||
:selected-token-set-id selected-token-set-id}]]
|
:selected-token-set-id selected-token-set-id}]]
|
||||||
(let [id (:id (:leaf child))
|
(let [id (:id (:leaf child))
|
||||||
@ -84,7 +94,7 @@
|
|||||||
:is-selected-inside-layout is-selected-inside-layout
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
:active-theme-tokens active-theme-tokens
|
:active-theme-tokens active-theme-tokens
|
||||||
:on-click on-token-pill-click
|
:on-click on-token-pill-click
|
||||||
:on-context-menu on-context-menu}])))))]))]))
|
:on-context-menu on-pill-context-menu}])))))]))]))
|
||||||
|
|
||||||
(def ^:private schema:token-tree
|
(def ^:private schema:token-tree
|
||||||
[:map
|
[:map
|
||||||
@ -97,7 +107,8 @@
|
|||||||
[:selected-token-set-id {:optional true} :any]
|
[:selected-token-set-id {:optional true} :any]
|
||||||
[:tokens-lib {:optional true} :any]
|
[:tokens-lib {:optional true} :any]
|
||||||
[:on-token-pill-click {:optional true} fn?]
|
[:on-token-pill-click {:optional true} fn?]
|
||||||
[:on-context-menu {:optional true} fn?]])
|
[:on-pill-context-menu {:optional true} fn?]
|
||||||
|
[:on-node-context-menu {:optional true} fn?]])
|
||||||
|
|
||||||
(mf/defc token-tree*
|
(mf/defc token-tree*
|
||||||
{::mf/schema schema:token-tree}
|
{::mf/schema schema:token-tree}
|
||||||
@ -110,12 +121,19 @@
|
|||||||
tokens-lib
|
tokens-lib
|
||||||
selected-token-set-id
|
selected-token-set-id
|
||||||
on-token-pill-click
|
on-token-pill-click
|
||||||
on-context-menu]}]
|
on-pill-context-menu
|
||||||
|
on-node-context-menu]}]
|
||||||
(let [separator "."
|
(let [separator "."
|
||||||
tree (mf/use-memo
|
tree (mf/use-memo
|
||||||
(mf/deps tokens)
|
(mf/deps tokens)
|
||||||
(fn []
|
(fn []
|
||||||
(cpn/build-tree-root tokens separator)))]
|
(cpn/build-tree-root tokens separator)))
|
||||||
|
can-edit? (:can-edit (deref refs/permissions))
|
||||||
|
on-node-context-menu (mf/use-fn
|
||||||
|
(mf/deps can-edit? on-node-context-menu)
|
||||||
|
(fn [event node]
|
||||||
|
(when can-edit?
|
||||||
|
(on-node-context-menu event node))))]
|
||||||
[:div {:class (stl/css :token-tree-wrapper)}
|
[:div {:class (stl/css :token-tree-wrapper)}
|
||||||
(for [node tree]
|
(for [node tree]
|
||||||
(if (:leaf node)
|
(if (:leaf node)
|
||||||
@ -127,7 +145,7 @@
|
|||||||
:is-selected-inside-layout is-selected-inside-layout
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
:active-theme-tokens active-theme-tokens
|
:active-theme-tokens active-theme-tokens
|
||||||
:on-click on-token-pill-click
|
:on-click on-token-pill-click
|
||||||
:on-context-menu on-context-menu}])
|
:on-context-menu on-pill-context-menu}])
|
||||||
;; Render segment folder
|
;; Render segment folder
|
||||||
[:ul {:class (stl/css :node-parent)
|
[:ul {:class (stl/css :node-parent)
|
||||||
:key (:path node)}
|
:key (:path node)}
|
||||||
@ -138,6 +156,7 @@
|
|||||||
:is-selected-inside-layout is-selected-inside-layout
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
:active-theme-tokens active-theme-tokens
|
:active-theme-tokens active-theme-tokens
|
||||||
:on-token-pill-click on-token-pill-click
|
:on-token-pill-click on-token-pill-click
|
||||||
:on-context-menu on-context-menu
|
:on-node-context-menu on-node-context-menu
|
||||||
|
:on-pill-context-menu on-pill-context-menu
|
||||||
:tokens-lib tokens-lib
|
:tokens-lib tokens-lib
|
||||||
:selected-token-set-id selected-token-set-id}]]))]))
|
:selected-token-set-id selected-token-set-id}]]))]))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user