diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index 35e7326eed..66a3591ee4 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -158,7 +158,7 @@ test.describe("Tokens - creation", () => { const selfReferenceError = "Token has self reference"; const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); await tokensSidebar .getByRole("button", { name: "Add Token: Color" }) @@ -189,7 +189,7 @@ test.describe("Tokens - creation", () => { // 2. Invalid value → disabled + error message await valueField.fill("1"); const invalidValueErrorNode = - tokensUpdateCreateModal.getByText(invalidValueError); + tokensUpdateCreateModal.getByText(invalidValueError); await expect(invalidValueErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -197,7 +197,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -207,7 +207,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("{color.primary}"); const selfRefErrorNode = - tokensUpdateCreateModal.getByText(selfReferenceError); + tokensUpdateCreateModal.getByText(selfReferenceError); await expect(selfRefErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -320,7 +320,7 @@ test.describe("Tokens - creation", () => { const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -356,7 +356,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -366,7 +366,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("{my-token}"); const selfRefErrorNode = - tokensUpdateCreateModal.getByText(selfReferenceError); + tokensUpdateCreateModal.getByText(selfReferenceError); await expect(selfRefErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -459,13 +459,13 @@ test.describe("Tokens - creation", () => { test("User creates font weight token", async ({ page }) => { const invalidValueError = - "Invalid font weight value: use numeric values (100-950) or standard names (thin, light, regular, bold, etc.) optionally followed by 'Italic'"; + "Invalid font weight value: use numeric values (100-950) or standard names (thin, light, regular, bold, etc.) optionally followed by 'Italic'"; const emptyNameError = "Name should be at least 1 character"; const selfReferenceError = "Token has self reference"; const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -501,7 +501,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("red"); const invalidValueErrorNode = - tokensUpdateCreateModal.getByText(invalidValueError); + tokensUpdateCreateModal.getByText(invalidValueError); await expect(invalidValueErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -510,7 +510,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -520,7 +520,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("{my-token}"); const selfRefErrorNode = - tokensUpdateCreateModal.getByText(selfReferenceError); + tokensUpdateCreateModal.getByText(selfReferenceError); await expect(selfRefErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -595,13 +595,13 @@ test.describe("Tokens - creation", () => { test("User creates text case token", async ({ page }) => { const invalidValueError = - "Invalid token value: only none, Uppercase, Lowercase or Capitalize are accepted"; + "Invalid token value: only none, Uppercase, Lowercase or Capitalize are accepted"; const emptyNameError = "Name should be at least 1 character"; const selfReferenceError = "Token has self reference"; const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -637,7 +637,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("red"); const invalidValueErrorNode = - tokensUpdateCreateModal.getByText(invalidValueError); + tokensUpdateCreateModal.getByText(invalidValueError); await expect(invalidValueErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -646,7 +646,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -656,7 +656,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("{my-token}"); const selfRefErrorNode = - tokensUpdateCreateModal.getByText(selfReferenceError); + tokensUpdateCreateModal.getByText(selfReferenceError); await expect(selfRefErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -711,13 +711,13 @@ test.describe("Tokens - creation", () => { test("User creates text decoration token", async ({ page }) => { const invalidValueError = - "Invalid token value: only none, underline and strike-through are accepted"; + "Invalid token value: only none, underline and strike-through are accepted"; const emptyNameError = "Name should be at least 1 character"; const selfReferenceError = "Token has self reference"; const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -755,7 +755,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("red"); const invalidValueErrorNode = - tokensUpdateCreateModal.getByText(invalidValueError); + tokensUpdateCreateModal.getByText(invalidValueError); await expect(invalidValueErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -764,7 +764,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -774,7 +774,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("{my-token}"); const selfRefErrorNode = - tokensUpdateCreateModal.getByText(selfReferenceError); + tokensUpdateCreateModal.getByText(selfReferenceError); await expect(selfRefErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -831,7 +831,7 @@ test.describe("Tokens - creation", () => { const emptyNameError = "Name should be at least 1 character"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] }); + await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] }); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -900,7 +900,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -977,9 +977,9 @@ test.describe("Tokens - creation", () => { await nameField.fill("my-token-2"); const referenceToggle = - tokensUpdateCreateModal.getByTestId("reference-opt"); + tokensUpdateCreateModal.getByTestId("reference-opt"); const compositeToggle = - tokensUpdateCreateModal.getByTestId("composite-opt"); + tokensUpdateCreateModal.getByTestId("composite-opt"); await referenceToggle.click(); const referenceInput = tokensUpdateCreateModal.getByPlaceholder( @@ -1012,7 +1012,7 @@ test.describe("Tokens - creation", () => { page, }) => { const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } = - await setupTypographyTokensFile(page); + await setupTypographyTokensFile(page); const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); await tokensTabPanel @@ -1038,7 +1038,7 @@ test.describe("Tokens - creation", () => { // Switch to reference tab, should not be submittable either const referenceTabButton = - tokensUpdateCreateModal.getByTestId("reference-opt"); + tokensUpdateCreateModal.getByTestId("reference-opt"); await referenceTabButton.click(); await expect(submitButton).toBeDisabled(); }); @@ -1047,7 +1047,7 @@ test.describe("Tokens - creation", () => { const emptyNameError = "Name should be at least 1 character"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page, {flags: ["enable-token-shadow"]}); + await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] }); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -1116,7 +1116,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -1198,9 +1198,9 @@ test.describe("Tokens - creation", () => { await nameField.fill("my-token-2"); const referenceToggle = - tokensUpdateCreateModal.getByTestId("reference-opt"); + tokensUpdateCreateModal.getByTestId("reference-opt"); const compositeToggle = - tokensUpdateCreateModal.getByTestId("composite-opt"); + tokensUpdateCreateModal.getByTestId("composite-opt"); await referenceToggle.click(); const referenceInput = tokensUpdateCreateModal.getByPlaceholder( @@ -1232,7 +1232,7 @@ test.describe("Tokens - creation", () => { test("User creates typography token", async ({ page }) => { const emptyNameError = "Name should be at least 1 character"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -1287,7 +1287,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -1443,9 +1443,9 @@ test.describe("Tokens - creation", () => { await nameField.fill("my-token-2"); const referenceToggle = - tokensUpdateCreateModal.getByTestId("reference-opt"); + tokensUpdateCreateModal.getByTestId("reference-opt"); const compositeToggle = - tokensUpdateCreateModal.getByTestId("composite-opt"); + tokensUpdateCreateModal.getByTestId("composite-opt"); await referenceToggle.click(); @@ -1479,7 +1479,7 @@ test.describe("Tokens - creation", () => { test("User adds typography token with reference", async ({ page }) => { const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } = - await setupTypographyTokensFile(page); + await setupTypographyTokensFile(page); const newTokenTitle = "NewReference"; @@ -1508,7 +1508,7 @@ test.describe("Tokens - creation", () => { }); const resolvedValue = - await tokensUpdateCreateModal.getByText("Resolved value:"); + await tokensUpdateCreateModal.getByText("Resolved value:"); await expect(resolvedValue).toBeVisible(); await expect(resolvedValue).toContainText("Font Family: 42dot Sans"); await expect(resolvedValue).toContainText("Font Size: 100"); @@ -1531,7 +1531,7 @@ test.describe("Tokens - creation", () => { test("User creates grouped color token", async ({ page }) => { const { workspacePage, tokensUpdateCreateModal, tokensSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); await tokensSidebar .getByRole("button", { name: "Add Token: Color" }) @@ -1591,7 +1591,7 @@ test.describe("Tokens - creation", () => { test("User duplicate color token", async ({ page }) => { const { tokensSidebar, tokenContextMenuForToken } = - await setupTokensFile(page); + await setupTokensFile(page); await expect(tokensSidebar).toBeVisible(); @@ -1613,15 +1613,11 @@ test.describe("Tokens - creation", () => { }); }); - - test("User creates grouped color token", async ({ page }) => { const { workspacePage, tokensUpdateCreateModal, tokensSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); - await tokensSidebar - .getByRole("button", { name: "Add Token: Color" }) - .click(); + await tokensSidebar.getByRole("button", { name: "Add Token: Color" }).click(); // Create grouped color token with mouse @@ -1647,9 +1643,7 @@ test("User creates grouped color token", async ({ page }) => { await expect(tokensSidebar.getByLabel("primary")).toBeEnabled(); }); -test("User cant create regular token with value missing", async ({ - page, -}) => { +test("User cant create regular token with value missing", async ({ page }) => { const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page); const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -1677,7 +1671,7 @@ test("User cant create regular token with value missing", async ({ test("User duplicate color token", async ({ page }) => { const { tokensSidebar, tokenContextMenuForToken } = - await setupTokensFile(page); + await setupTokensFile(page); await expect(tokensSidebar).toBeVisible(); @@ -1703,7 +1697,7 @@ test.describe("Tokens tab - edition", () => { page, }) => { const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } = - await setupTypographyTokensFile(page); + await setupTypographyTokensFile(page); await tokensSidebar .getByRole("button") @@ -1724,8 +1718,8 @@ test.describe("Tokens tab - edition", () => { // Fill font-family to verify to verify that input value doesn't get split into list of characters const fontFamilyField = tokensUpdateCreateModal - .getByLabel("Font family") - .first(); + .getByLabel("Font family") + .first(); await fontFamilyField.fill("OneWord"); // Invalidate incorrect values for font size @@ -1746,11 +1740,11 @@ test.describe("Tokens tab - edition", () => { const fontWeightField = tokensUpdateCreateModal.getByLabel(/Font Weight/i); const letterSpacingField = - tokensUpdateCreateModal.getByLabel(/Letter Spacing/i); + tokensUpdateCreateModal.getByLabel(/Letter Spacing/i); const lineHeightField = tokensUpdateCreateModal.getByLabel(/Line Height/i); const textCaseField = tokensUpdateCreateModal.getByLabel(/Text Case/i); const textDecorationField = - tokensUpdateCreateModal.getByLabel(/Text Decoration/i); + tokensUpdateCreateModal.getByLabel(/Text Decoration/i); // Capture all values before switching tabs const originalValues = { @@ -1765,14 +1759,14 @@ test.describe("Tokens tab - edition", () => { // Switch to reference tab and back to composite tab const referenceTabButton = - tokensUpdateCreateModal.getByTestId("reference-opt"); + tokensUpdateCreateModal.getByTestId("reference-opt"); await referenceTabButton.click(); // Empty reference tab should be disabled await expect(saveButton).toBeDisabled(); const compositeTabButton = - tokensUpdateCreateModal.getByTestId("composite-opt"); + tokensUpdateCreateModal.getByTestId("composite-opt"); await compositeTabButton.click(); // Filled composite tab should be enabled @@ -1799,7 +1793,7 @@ test.describe("Tokens tab - edition", () => { page, }) => { const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } = - await setupTokensFile(page); + await setupTokensFile(page); await expect(tokensSidebar).toBeVisible(); @@ -1835,7 +1829,7 @@ test.describe("Tokens tab - edition", () => { page, }) => { const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); await tokensTabPanel @@ -1890,7 +1884,7 @@ test.describe("Tokens tab - edition", () => { test.describe("Tokens tab - delete", () => { test("User delete color token", async ({ page }) => { const { tokensSidebar, tokenContextMenuForToken } = - await setupTokensFile(page); + await setupTokensFile(page); await expect(tokensSidebar).toBeVisible(); @@ -1910,7 +1904,7 @@ test.describe("Tokens tab - delete", () => { }); test("User removes node and all child tokens", async ({ page }) => { - const { tokensSidebar, workspacePage } = await setupTokensFile(page); + const { tokensSidebar } = await setupTokensFile(page); await expect(tokensSidebar).toBeVisible(); @@ -1919,7 +1913,7 @@ test.describe("Tokens tab - delete", () => { // Verify that the node and child token are visible before deletion const colorNode = tokensSidebar.getByRole("button", { - name: "blue", + name: "colors", exact: true, }); const colorNodeToken = tokensSidebar.getByRole("button", { @@ -1943,5 +1937,13 @@ test.describe("Tokens tab - delete", () => { 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"); }); }); diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs index 10a0275662..0a21d54464 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -62,6 +62,52 @@ (watch [_ _ _] (rx/of (dwsh/update-shapes [id] #(merge % attrs))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Toggle tree nodes +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- remove-paths-recursively + [path paths] + (->> paths + (remove #(str/starts-with? % (str path))) + vec)) + +(defn add-path + [path paths] + (let [split-path (cpn/split-path path :separator ".") + partial-paths (->> split-path + (reduce + (fn [acc segment] + (let [new-acc (if (empty? acc) + segment + (str (last acc) "." segment))] + (conj acc new-acc))) + []))] + (->> paths + (into partial-paths) + distinct + vec))) + +(defn clear-tokens-paths + [] + (ptk/reify ::clear-tokens-paths + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-tokens :unfolded-token-paths] [])))) + +(defn toggle-token-path + [path] + (ptk/reify ::toggle-token-path + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-tokens :unfolded-token-paths] + (fn [paths] + (let [paths (or paths [])] + (if (some #(= % path) paths) + (remove-paths-recursively path paths) + (add-path path paths)))))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TOKENS Actions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -245,8 +291,9 @@ (pcb/with-library-data data) (clt/generate-toggle-token-set tlib name))] - (rx/of (dch/commit-changes changes) - (dwtp/propagate-workspace-tokens)))))) + (rx/of + (dch/commit-changes changes) + (dwtp/propagate-workspace-tokens)))))) (defn toggle-token-set-group [group-path] @@ -257,6 +304,7 @@ changes (-> (pcb/empty-changes) (pcb/with-library-data data) (clt/generate-toggle-token-set-group (get-tokens-lib state) group-path))] + (rx/of (dch/commit-changes changes) (dwtp/propagate-workspace-tokens)))))) @@ -486,35 +534,7 @@ ;; TOKEN UI OPS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn clean-tokens-paths - [] - (ptk/reify ::clean-tokens-paths - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-tokens :unfolded-token-paths] [])))) -(defn toggle-token-path - [path] - (ptk/reify ::toggle-token-path - ptk/UpdateEvent - (update [_ state] - (update-in state [:workspace-tokens :unfolded-token-paths] - (fn [paths] - (let [paths (or paths [])] - (if (some #(= % path) paths) - (vec (remove #(or (= % path) - (str/starts-with? % (str path "."))) - paths)) - (let [split-path (cpn/split-path path :separator ".") - partial-paths (reduce - (fn [acc segment] - (let [new-acc (if (empty? acc) - segment - (str (last acc) "." segment))] - (conj acc new-acc))) - [] - split-path)] - (into paths partial-paths))))))))) (defn assign-token-context-menu [{:keys [position] :as params}] diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs index a8e6f7685a..1f124b7b8b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs @@ -94,10 +94,7 @@ ;; This only checks for the currently explicitly selected set ;; id, it is ephimeral and can be nil ;; FIXME: this is a repeated deref for the same `:workspace-tokens` state - selected-token-set-id - (mf/deref refs/selected-token-set-id) - - + selected-token-set-id (mf/deref refs/selected-token-set-id) ;; If we have not selected any set explicitly we just ;; select the first one from the list of sets @@ -112,6 +109,7 @@ tokens (sd/use-resolved-tokens* tokens) + ;; Group tokens by their type tokens-by-type (mf/with-memo [tokens selected-token-set-tokens] (let [tokens (reduce-kv (fn [tokens k _] @@ -129,9 +127,9 @@ ;; Filter tokens by their path and return their ids filter-tokens-by-path-ids (mf/use-fn - (mf/deps tokens) + (mf/deps selected-token-set-tokens) (fn [type path] - (->> tokens + (->> selected-token-set-tokens (filter (fn [token] (let [[_ token-value] token] (and (= (:type token-value) type) (str/starts-with? (:name token-value) path))))) @@ -139,12 +137,47 @@ (let [[_ token-value] token] (:id token-value))))))) + remaining-tokens-of-type-in-set? + (mf/use-fn + (fn [selected-token-set-tokens tokens-in-path-ids] + (let [token-ids (set tokens-in-path-ids) + remaining-tokens (filter (fn [token] + (not (contains? token-ids (:id token)))) + selected-token-set-tokens) + _ (prn "Remaining tokens:" remaining-tokens)] + (seq remaining-tokens)))) + + delete-token + (mf/with-memo [selected-token-set-tokens selected-token-set-id] + (fn [token] + (let [id (:id token) + type (:type token) + path (:name token) + tokens-by-type (ctob/group-by-type selected-token-set-tokens) + tokens-filtered-by-type (get tokens-by-type type) + tokens-in-path-ids (filter-tokens-by-path-ids type path) + remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type tokens-in-path-ids)] + ;; Delete the token + (st/emit! (dwtl/delete-token selected-token-set-id id)) + ;; Remove from unfolded tree path + (if remaining-tokens? + (st/emit! (dwtl/toggle-token-path (str (name type) "." path))) + (st/emit! (dwtl/toggle-token-path (name type))))))) + delete-node - (mf/with-memo [tokens selected-token-set-id] + (mf/with-memo [selected-token-set-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)))))] + tokens-by-type (ctob/group-by-type selected-token-set-tokens) + tokens-filtered-by-type (get tokens-by-type type) + tokens-in-path-ids (filter-tokens-by-path-ids type path) + remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type tokens-in-path-ids)] + ;; Delete tokens in path + (st/emit! (dwtl/bulk-delete-tokens selected-token-set-id tokens-in-path-ids)) + ;; Remove from unfolded tree path + (if remaining-tokens? + (st/emit! (dwtl/toggle-token-path (str (name type) "." path))) + (st/emit! (dwtl/toggle-token-path (name type)))))))] (mf/with-effect [tokens-lib selected-token-set-id] (when (and tokens-lib @@ -157,7 +190,7 @@ (st/emit! (dwtl/set-selected-token-set-id (ctob/get-id match))))))) [:* - [:& token-context-menu] + [:& token-context-menu {:on-delete-token delete-token}] [:> token-node-context-menu* {:on-delete-node delete-node}] [:> selected-set-info* {:tokens-lib tokens-lib diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index 1dcb40ea18..ef1ccb0128 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -316,8 +316,9 @@ (generic-attribute-actions #{:y} "Y" (assoc context-data :on-update-shape dwta/update-shape-position))) (clean-separators)))})) -(defn default-actions [{:keys [token selected-token-set-id]}] - (let [{:keys [modal]} (dwta/get-token-properties token)] +(defn default-actions [{:keys [token selected-token-set-id on-delete-token]}] + (let [{:keys [modal]} (dwta/get-token-properties token) + on-duplicate-token #(st/emit! (dwtl/duplicate-token (:id token)))] [{:title (tr "workspace.tokens.edit") :no-selectable true :action (fn [event] @@ -333,12 +334,10 @@ :token token}))))} {:title (tr "workspace.tokens.duplicate") :no-selectable true - :action #(st/emit! (dwtl/duplicate-token (:id token)))} + :action on-duplicate-token} {:title (tr "workspace.tokens.delete") :no-selectable true - :action #(st/emit! (dwtl/delete-token - selected-token-set-id - (:id token)))}])) + :action #(on-delete-token token)}])) (defn- allowed-shape-attributes [shapes] (reduce into #{} (map #(ctt/shape-type->attributes (:type %) (:layout %)) shapes))) @@ -464,7 +463,7 @@ :selected? selected?}])]))) (mf/defc token-context-menu-tree - [{:keys [width errors] :as mdata}] + [{:keys [width errors on-delete-token] :as mdata}] (let [objects (mf/deref refs/workspace-page-objects) selected (mf/deref refs/selected-shapes) @@ -488,10 +487,11 @@ :errors errors :selected-token-set-id selected-token-set-id :selected-shapes selected-shapes - :is-selected-inside-layout is-selected-inside-layout}]])) + :is-selected-inside-layout is-selected-inside-layout + :on-delete-token on-delete-token}]])) (mf/defc token-context-menu - [] + [{:keys [on-delete-token]}] (let [mdata (mf/deref tokens-menu-ref) is-open? (boolean mdata) width (mf/use-state 0) @@ -538,5 +538,5 @@ :left (dm/str left "px")} :on-context-menu prevent-default} (when mdata - [:& token-context-menu-tree (assoc mdata :width @width)])]]) + [:& token-context-menu-tree (assoc mdata :width @width :on-delete-token on-delete-token)])]]) (dom/get-body))))) diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index d530394b99..e16fad82b6 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -16,6 +16,7 @@ [rumext.v2 :as mf])) (defn- on-select-token-set-click [id] + (st/emit! (dwtl/clear-tokens-paths)) (st/emit! (dwtl/set-selected-token-set-id id))) (defn- on-toggle-token-set-click [name]