From ba396001926ecede56852756812c2eadd7937531 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 26 May 2026 12:40:26 +0200 Subject: [PATCH] :bug: Fix show error on name-input --- .../playwright/ui/specs/tokens/apply.spec.js | 2 +- .../playwright/ui/specs/tokens/crud.spec.js | 19 +---- .../playwright/ui/specs/tokens/helpers.js | 24 +++--- .../playwright/ui/specs/tokens/tree.spec.js | 76 +++++++++++++++++-- frontend/src/app/main/ui/forms.cljs | 5 +- .../tokens/management/context_menu.cljs | 3 +- .../forms/controls/color_input.cljs | 25 +++--- .../management/forms/controls/combobox.cljs | 16 ++-- .../forms/controls/fonts_combobox.cljs | 19 +++-- .../management/forms/controls/input.cljs | 43 +++++++---- .../management/forms/controls/select.cljs | 3 +- .../management/forms/controls/utils.cljs | 7 +- .../management/forms/form_container.cljs | 5 +- .../tokens/management/forms/generic_form.cljs | 17 +++++ .../tokens/management/forms/modals.cljs | 3 +- .../ui/workspace/tokens/management/group.cljs | 13 ++-- .../tokens/management/token_pill.cljs | 23 ++++-- frontend/src/app/util/forms.cljs | 28 ++++--- 18 files changed, 232 insertions(+), 99 deletions(-) diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index d20ffdd6a2..301964866e 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -6,7 +6,7 @@ import { setupTypographyTokensFileRender, unfoldTokenType, createToken, - createSet + createSet, } from "./helpers"; test.beforeEach(async ({ page }) => { diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index 4c07954fa5..0dd2fbea03 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -1818,21 +1818,6 @@ test("User disables the current set but token still have resolved values shown i }); test.describe("User can't create groups that clash with token names", () => { - const changeSetInput = async (sidebar, setName, finalKey = "Enter") => { - const setInput = sidebar.locator("input:focus"); - await expect(setInput).toBeVisible(); - await setInput.fill(setName); - await setInput.press(finalKey); - }; - - const createSet = async (sidebar, setName, finalKey = "Enter") => { - const tokensTabButton = sidebar - .getByRole("button", { name: "Add set" }) - .click(); - - await changeSetInput(sidebar, setName, (finalKey = "Enter")); - }; - const createBadToken = async (page, type, name, textFieldName, value) => { const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -2102,7 +2087,7 @@ test("User can't create Text Decoration token with group name that clashes with }); const errorNode = tokensUpdateCreateModal.getByText("Group name of typ1.bad conflicts with a token of the same name in another active set."); - await expect(errorNode).toHaveCount(6); + await expect(errorNode).toHaveCount(1); await expect(submitButton).toBeDisabled(); }); @@ -2160,7 +2145,7 @@ test("User can't create Text Decoration token with group name that clashes with }); const errorNode = tokensUpdateCreateModal.getByText("Group name of sha1.bad conflicts with a token of the same name in another active set."); - await expect(errorNode).toHaveCount(5); + await expect(errorNode).toHaveCount(1); await expect(submitButton).toBeDisabled(); }); }); diff --git a/frontend/playwright/ui/specs/tokens/helpers.js b/frontend/playwright/ui/specs/tokens/helpers.js index fc142e7de5..a2471c7a9c 100644 --- a/frontend/playwright/ui/specs/tokens/helpers.js +++ b/frontend/playwright/ui/specs/tokens/helpers.js @@ -360,20 +360,20 @@ const createToken = async (page, type, name, textFieldName, value) => { await expect(tokensUpdateCreateModal).not.toBeVisible(); }; - const changeSetInput = async (sidebar, setName, finalKey = "Enter") => { - const setInput = sidebar.locator("input:focus"); - await expect(setInput).toBeVisible(); - await setInput.fill(setName); - await setInput.press(finalKey); - }; +const changeSetInput = async (sidebar, setName, finalKey = "Enter") => { + const setInput = sidebar.locator("input:focus"); + await expect(setInput).toBeVisible(); + await setInput.fill(setName); + await setInput.press(finalKey); +}; - const createSet = async (sidebar, setName, finalKey = "Enter") => { - const tokensTabButton = sidebar - .getByRole("button", { name: "Add set" }) - .click(); +const createSet = async (sidebar, setName, finalKey = "Enter") => { + const tokensTabButton = sidebar + .getByRole("button", { name: "Add set" }) + .click(); - await changeSetInput(sidebar, setName, (finalKey = "Enter")); - }; + await changeSetInput(sidebar, setName, (finalKey = "Enter")); +}; export { setupEmptyTokensFile, diff --git a/frontend/playwright/ui/specs/tokens/tree.spec.js b/frontend/playwright/ui/specs/tokens/tree.spec.js index 9f8ef72cdf..1b6279a51f 100644 --- a/frontend/playwright/ui/specs/tokens/tree.spec.js +++ b/frontend/playwright/ui/specs/tokens/tree.spec.js @@ -1,7 +1,12 @@ import { test, expect } from "@playwright/test"; import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage"; import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage"; -import { createToken, setupTokensFileRender, unfoldTokenType } from "./helpers"; +import { + createToken, + setupTokensFileRender, + unfoldTokenType, + createSet, +} from "./helpers"; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); @@ -174,14 +179,14 @@ test.describe("Tokens - node tree", () => { // Rename to move it into the collapsed light group const nameField = tokensUpdateCreateModal.getByLabel("Name"); await nameField.fill("light.base"); - await tokensUpdateCreateModal - .getByRole("button", { name: "Save" }) - .click(); + await tokensUpdateCreateModal.getByRole("button", { name: "Save" }).click(); // After rename, light group should be auto-expanded and both tokens visible await expect(lightGroup).toBeVisible(); await expect(lightAccentToken).toBeVisible(); - await expect(tokensSidebar.getByRole("button", { name: "base" })).toBeVisible(); + await expect( + tokensSidebar.getByRole("button", { name: "base" }), + ).toBeVisible(); }); test("User removes node and all child tokens", async ({ page }) => { @@ -228,3 +233,64 @@ test.describe("Tokens - node tree", () => { await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false"); }); }); + +test("User can see an error on token pill and token modal form when token has an error", async ({ + page, +}) => { + const { + tokensSidebar, + tokensUpdateCreateModal, + tokenContextMenuForToken, + tokenThemesSetsSidebar, + } = await setupTokensFileRender(page); + + await createSet(tokenThemesSetsSidebar, "set/first"); + await tokenThemesSetsSidebar.getByRole("button", { name: "first" }).click(); + + await tokenThemesSetsSidebar + .getByRole("button", { name: "first" }) + .getByRole("checkbox") + .click(); + + await createSet(tokenThemesSetsSidebar, "set/second"); + await tokenThemesSetsSidebar.getByRole("button", { name: "second" }).click(); + + await tokenThemesSetsSidebar + .getByRole("button", { name: "second" }) + .getByRole("checkbox") + .click(); + + await createToken(page, "Border radius", "a.b", "Value", "23"); + await tokenThemesSetsSidebar.getByRole("button", { name: "first" }).click(); + await createToken(page, "Border radius", "a", "Value", "25"); + await tokenThemesSetsSidebar.getByRole("button", { name: "second" }).click(); + + const brokenTokenPill = tokensSidebar.getByRole("button", { + name: "Group name of a.b conflicts", + }); + await expect(brokenTokenPill).toBeVisible(); + + await brokenTokenPill.click({ button: "right" }); + + const editTokenButton = page + .getByRole("listitem") + .filter({ hasText: "Edit token" }); + await expect(editTokenButton).toBeVisible(); + await editTokenButton.click(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + await expect(nameField).toBeVisible(); + await expect(nameField).toHaveValue("a.b"); + + const errorMessage = tokensUpdateCreateModal.getByText( + "Group name of a.b conflicts", + ); + await expect(errorMessage).toBeVisible(); + + await nameField.fill("new-name"); + await expect(errorMessage).not.toBeVisible(); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await expect(submitButton).toBeEnabled(); +}); diff --git a/frontend/src/app/main/ui/forms.cljs b/frontend/src/app/main/ui/forms.cljs index c0426dcfaa..fc24738e18 100644 --- a/frontend/src/app/main/ui/forms.cljs +++ b/frontend/src/app/main/ui/forms.cljs @@ -26,6 +26,7 @@ (get-in @form [:touched input-name])) error (get-in @form [:errors input-name]) + extra-error (get-in @form [:extra-errors input-name]) value (get-in @form [:data input-name] "") @@ -41,9 +42,9 @@ :value value}) props - (if (and error touched?) + (if (or extra-error (and error touched?)) (mf/spread-props props {:hint-type "error" - :hint-message (:message error)}) + :hint-message (:message (or error extra-error))}) props)] [:> input* props])) 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 5ce0b1c16c..03ffa7fca5 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 @@ -333,7 +333,7 @@ (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 on-delete-token]}] +(defn default-actions [{:keys [token selected-token-set-id on-delete-token errors]}] (let [{:keys [modal]} (dwta/get-token-properties token) on-copy-name #(clipboard/to-clipboard (:name token)) on-duplicate-token #(st/emit! (dwtl/duplicate-token (:id token)))] @@ -347,6 +347,7 @@ :y (.-clientY ^js event) :position :right :fields fields + :initial-errors errors :action "edit" :selected-token-set-id selected-token-set-id :token token}))))} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs index 3defb347d3..312c2f452f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs @@ -24,6 +24,7 @@ [app.main.ui.forms :as fc] [app.main.ui.workspace.colorpicker :as colorpicker] [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]] + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] [app.util.dom :as dom] [app.util.forms :as fm] [app.util.i18n :refer [tr]] @@ -282,10 +283,13 @@ (let [touched? (get-in @form [:touched input-name])] (when touched? (if error - (do - (swap! form assoc-in [:extra-errors input-name] {:message error}) - (swap! form assoc-in [:data :color-result] "") - (reset! hint* {:message error :type "error"})) + (if (csu/group-name-conflict-error? error token-name) + (swap! form assoc-in [:extra-errors ""] {:message error}) + (do + (swap! form assoc-in [:extra-errors input-name] {:message error}) + (swap! form assoc-in [:data :color-result] "") + (reset! hint* {:message error :type "error"}))) + (let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value))] (swap! form update :extra-errors dissoc input-name) (swap! form assoc-in [:data :color-result] value) @@ -312,7 +316,8 @@ (-> state (assoc-in [:data :value value-subfield index field] (if trim? (str/trim value) value)) (update :errors clean-errors) - (update :extra-errors clean-errors))))))) + (update :extra-errors clean-errors) + (update :extra-errors dissoc ""))))))) (mf/defc indexed-color-input* [{:keys [name tokens token index value-subfield] :rest props}] @@ -452,10 +457,12 @@ (some? error) (let [error' (:message error)] - (do - (swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'}) - (swap! form assoc-in [:data :value value-subfield index :color-result] "") - (reset! hint* {:message error' :type "error"}))) + (if (csu/group-name-conflict-error? error token-name) + (swap! form assoc-in [:extra-errors ""] {:message error}) + (do + (swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'}) + (swap! form assoc-in [:data :value value-subfield index :color-result] "") + (reset! hint* {:message error' :type "error"})))) :else (let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value)) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs index 6ab6f3ce0a..248cf71c8c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs @@ -78,6 +78,9 @@ error (get-in @form [:errors name]) + extra-error + (get-in @form [:extra-errors name]) + value (get-in @form [:data name] "") @@ -264,12 +267,11 @@ :on-mouse-down dom/prevent-default :on-click toggle-dropdown}]))}) props - (if (and error touched?) + (if (or extra-error (and error touched?)) (mf/spread-props props {:hint-type "error" - :hint-message (:message error)}) + :hint-message (:message (or error extra-error))}) props) - {:keys [style ready?]} (use-floating-dropdown is-open input-wrapper-ref wrapper-ref dropdown-ref)] (mf/with-effect [resolve-stream tokens token name token-name] @@ -284,9 +286,11 @@ (let [touched? (get-in @form [:touched name])] (when touched? (if error - (do - (swap! form assoc-in [:extra-errors name] {:message error}) - (reset! hint* {:message error :type "error"})) + (if (csu/group-name-conflict-error? error token-name) + (swap! form assoc-in [:extra-errors ""] {:message error}) + (do + (swap! form assoc-in [:extra-errors name] {:message error}) + (reset! hint* {:message error :type "error"}))) (let [message (tr "workspace.tokens.resolved-value" value)] (swap! form update :extra-errors dissoc name) (reset! hint* {:message message :type "hint"}))))))))] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs index 8053d8fd13..9319f8bc40 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs @@ -20,6 +20,7 @@ [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.forms :as fc] [app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]] + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] [app.util.dom :as dom] [app.util.forms :as fm] [app.util.i18n :refer [tr]] @@ -175,9 +176,11 @@ (rx/subs! (fn [{:keys [error value]}] (when touched? (if error - (do - (swap! form assoc-in [:extra-errors input-name] {:message error}) - (reset! hint* {:message error :type "error"})) + (if (csu/group-name-conflict-error? error token-name) + (swap! form assoc-in [:extra-errors ""] {:message error}) + (do + (swap! form assoc-in [:extra-errors input-name] {:message error}) + (reset! hint* {:message error :type "error"}))) (let [message (tr "workspace.tokens.resolved-value" value)] (swap! form update :extra-errors dissoc input-name) (reset! hint* {:message message :type "hint"})))))))] @@ -205,7 +208,8 @@ (-> state (assoc-in [:data :value field] (if trim? (str/trim value) value)) (update :errors clean-errors) - (update :extra-errors clean-errors))))))) + (update :extra-errors clean-errors) + (update :extra-errors dissoc ""))))))) (mf/defc composite-fonts-combobox* [{:keys [token tokens name] :rest props}] @@ -306,8 +310,11 @@ (some? error) (let [error' (:message error)] - (swap! form assoc-in [:extra-errors :value input-name] {:message error'}) - (reset! hint* {:message error' :type "error"})) + (if (csu/group-name-conflict-error? error' token-name) + (swap! form assoc-in [:extra-errors ""] {:message error'}) + (do + (swap! form assoc-in [:extra-errors :value input-name] {:message error'}) + (reset! hint* {:message error' :type "error"})))) :else (let [message (tr "workspace.tokens.resolved-value" value) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs index ff8d77387c..8c8a136f36 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs @@ -17,6 +17,7 @@ [app.main.data.workspace.tokens.format :as dwtf] [app.main.ui.ds.controls.input :as ds] [app.main.ui.forms :as fc] + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] [app.util.dom :as dom] [app.util.forms :as fm] [app.util.i18n :refer [tr]] @@ -248,9 +249,11 @@ (let [touched? (get-in @form [:touched input-name])] (when touched? (if error - (do - (swap! form assoc-in [:extra-errors input-name] {:message error}) - (reset! hint* {:message error :type "error"})) + (if (csu/group-name-conflict-error? error token-name) + (swap! form assoc-in [:extra-errors ""] {:message error}) + (do + (swap! form assoc-in [:extra-errors input-name] {:message error}) + (reset! hint* {:message error :type "error"}))) (let [message (tr "workspace.tokens.resolved-value" value)] (swap! form update :extra-errors dissoc input-name) (reset! hint* {:message message :type "hint"}))))))))] @@ -274,7 +277,8 @@ (assoc-in [:data :value field] (if trim? (str/trim value) value)) (assoc-in [:touched :value field] true) (update :errors clean-errors) - (update :extra-errors clean-errors))))))) + (update :extra-errors clean-errors) + (update :extra-errors dissoc ""))))))) (mf/defc input-composite* [{:keys [name tokens token] :rest props}] @@ -286,6 +290,9 @@ error (get-in @form [:errors :value input-name]) + extra-error + (get-in @form [:extra-errors :value input-name]) + value (get-in @form [:data :value input-name] "") @@ -319,9 +326,9 @@ :hint-message (:message hint) :hint-type (:type hint)}) props - (if (and touched? error) + (if (or extra-error (and touched? error)) (mf/spread-props props {:hint-type "error" - :hint-message (:message error)}) + :hint-message (:message (or error extra-error))}) props) props (if (and (not error) (= input-name :reference)) @@ -347,8 +354,11 @@ (some? error) (let [error' (:message error)] - (swap! form assoc-in [:extra-errors :value input-name] {:message error'}) - (reset! hint* {:message error' :type "error"})) + (if (csu/group-name-conflict-error? error' token-name) + (swap! form assoc-in [:extra-errors ""] {:message error'}) + (do + (swap! form assoc-in [:extra-errors :value input-name] {:message error'}) + (reset! hint* {:message error' :type "error"})))) :else (let [input-value (get-in @form [:data :value input-name] "") @@ -386,7 +396,8 @@ (-> state (assoc-in [:data :value value-subfield index field] (if trim? (str/trim value) value)) (update :errors clean-errors) - (update :extra-errors clean-errors))))))) + (update :extra-errors clean-errors) + (update :extra-errors dissoc ""))))))) (mf/defc input-indexed* [{:keys [name tokens token index value-subfield] :rest props}] @@ -398,6 +409,9 @@ error (get-in @form [:errors :value value-subfield index input-name]) + extra-error + (get-in @form [:extra-errors :value value-subfield index input-name]) + value-from-form (get-in @form [:data :value value-subfield index input-name] "") @@ -428,9 +442,9 @@ :hint-message (:message hint) :hint-type (:type hint)}) props - (if error + (if (or error extra-error) (mf/spread-props props {:hint-type "error" - :hint-message (:message error)}) + :hint-message (:message (or error extra-error))}) props) props @@ -457,8 +471,11 @@ (some? error) (let [error' (:message error)] - (swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'}) - (reset! hint* {:message error' :type "error"})) + (if (csu/group-name-conflict-error? error' token-name) + (swap! form assoc-in [:extra-errors ""] {:message error'}) + + (do (swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'}) + (reset! hint* {:message error' :type "error"})))) :else (let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value)) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs index 73342b1254..31c7f801dd 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs @@ -37,7 +37,8 @@ (mf/deps input-name) (fn [id] (let [is-inner? (= id "inner")] - (swap! form assoc-in [:data :value indexed-type index input-name] is-inner?)))) + (swap! form assoc-in [:data :value indexed-type index input-name] is-inner?) + (swap! form update :extra-errors dissoc "")))) props (mf/spread-props props {:default-selected (if value "inner" "drop") :variant "ghost" diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs index ce0124b546..65bd7787c6 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs @@ -119,4 +119,9 @@ (not-empty))))) (defn focusable-options [options] - (filter #(= (:type %) :token) options)) \ No newline at end of file + (filter #(= (:type %) :token) options)) + +(defn group-name-conflict-error? + [error token-name] + (let [translated-string (tr "errors.tokens.name-collision" token-name)] + (= error translated-string))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs index 0e6d55df03..b978f5a5ec 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs @@ -20,7 +20,7 @@ [rumext.v2 :as mf])) (mf/defc form-container* - [{:keys [token token-type] :rest props}] + [{:keys [token token-type initial-errors] :rest props}] (let [token-type (or (:type token) token-type) @@ -38,7 +38,8 @@ props (mf/spread-props props {:token-type token-type :tokens-tree-in-selected-set tokens-tree-in-selected-set - :token token}) + :token token + :initial-errors initial-errors}) text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")}) text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")}) font-weight-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")}) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index 0835b6c238..3e83a3bbca 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -24,6 +24,7 @@ [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] [app.main.ui.forms :as fc] [app.main.ui.workspace.tokens.management.forms.controls :as token.controls] [app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]] @@ -61,6 +62,7 @@ make-schema input-component initial + initial-errors value-type value-subfield input-value-placeholder] :as props}] @@ -112,10 +114,21 @@ :value (:value token "") :description (:description token "")})) + initial-general-errors (mf/with-memo [token initial initial-errors] + (when initial-errors + (if (= :error.style-dictionary/missing-reference (:error/code (first initial-errors))) + (if (or (= value-type :composite) + (= value-type :indexed)) + {:value {:reference {:message (wte/resolve-error-message (first initial-errors))}}} + {:value {:message (wte/resolve-error-message (first initial-errors))}}) + {"" {:message (wte/resolve-error-message (first initial-errors))}}))) form (fm/use-form :schema schema + :initial-errors initial-general-errors :initial initial) + general-errors (get-in @form [:extra-errors ""]) + on-toggle-tab (mf/use-fn (mf/deps form) @@ -288,6 +301,10 @@ :max-length max-input-length :variant "comfortable" :is-optional true}]] + (when (some? general-errors) + [:> context-notification* {:level :warning + :appearance :ghost} + (:message general-errors)]) [:div {:class (stl/css-case :button-row true :with-delete (= action "edit"))} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs index 9356d45725..2bd3726725 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs @@ -75,7 +75,7 @@ (mf/defc token-update-create-modal {::mf/wrap-props false} - [{:keys [x y position token token-type action selected-token-set-id] :as _args}] + [{:keys [x y position token token-type action selected-token-set-id initial-errors] :as _args}] (let [wrapper-style (use-viewport-position-style x y position token-type) modal-size-large* (mf/use-state (or (= token-type :typography) (= token-type :color) @@ -102,6 +102,7 @@ :action action :selected-token-set-id selected-token-set-id :token-type token-type + :initial-errors initial-errors :on-display-colorpicker update-modal-size}]])) ;; Modals ---------------------------------------------------------------------- diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index bd36c11a9c..53aec70534 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -100,18 +100,19 @@ tokens (mf/with-memo [tokens] (vec (sort-by :name tokens))) - expandable? (d/nilv (seq tokens) false) on-pill-context-menu (mf/use-fn + (mf/deps active-theme-tokens) (fn [event token] (dom/prevent-default event) - (st/emit! (dwtl/assign-token-context-menu - {:type :token - :position (dom/get-client-position event) - :errors (:errors token) - :token-id (:id token)})))) + (let [resolved-token (get active-theme-tokens (:name token))] + (st/emit! (dwtl/assign-token-context-menu + {:type :token + :position (dom/get-client-position event) + :errors (:errors resolved-token) + :token-id (:id token)}))))) on-node-context-menu (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs index c76651c151..054eaa3a2f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs @@ -16,6 +16,7 @@ [app.config :as cf] [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.color :as dwtc] + [app.main.data.workspace.tokens.errors :as wte] [app.main.data.workspace.tokens.format :as dwtf] [app.main.refs :as refs] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] @@ -101,7 +102,7 @@ (defn- generate-tooltip "Generates a tooltip for a given token" - [is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set] + [is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set is-name-collision errors] (let [{:keys [name type resolved-value value]} token resolved-value-theme (:resolved-value theme-token) resolved-value (or resolved-value-theme resolved-value) @@ -128,6 +129,9 @@ ref-not-in-active-set (tr "workspace.tokens.ref-not-valid") + is-name-collision + (wte/resolve-error-message (first errors)) + no-valid-value (tr "workspace.tokens.value-not-valid") @@ -177,7 +181,6 @@ {::mf/wrap [mf/memo]} [{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}] (let [{:keys [name value type]} token - resolved-token (get active-theme-tokens (:name token)) errors (:errors resolved-token) @@ -218,11 +221,17 @@ (and is-reference? (not (contains-reference-value? value active-theme-tokens)))) + name-collision (->> errors + (first) + (:error/code) + (= :error.token/name-collision)) + no-valid-value (seq errors) errors? (or ref-not-in-active-set - no-valid-value) + no-valid-value + name-collision) color (when (cfo/color-token? token) @@ -266,12 +275,12 @@ on-hover (mf/use-fn - (mf/deps selected-shapes is-viewer? active-theme-tokens token half-applied? no-valid-value ref-not-in-active-set) + (mf/deps selected-shapes is-viewer? active-theme-tokens token half-applied? no-valid-value ref-not-in-active-set name-collision errors) (fn [event] (let [node (dom/get-current-target event) theme-token (get active-theme-tokens name) title (generate-tooltip is-viewer? (first selected-shapes) theme-token token - half-applied? no-valid-value ref-not-in-active-set)] + half-applied? no-valid-value ref-not-in-active-set name-collision errors)] (dom/set-attribute! node "title" title))))] [:button {:class (stl/css-case @@ -301,7 +310,9 @@ [:> icon* {:icon-id i/broken-link :class (stl/css :token-pill-icon) - :aria-label (tr "workspace.tokens.missing-reference")}] + :aria-label (if name-collision + (wte/resolve-error-message (first errors)) + (tr "workspace.tokens.missing-reference"))}] color [:> swatch* {:background color diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs index f0e7e5466c..df2ff92be3 100644 --- a/frontend/src/app/util/forms.cljs +++ b/frontend/src/app/util/forms.cljs @@ -45,11 +45,15 @@ valid?))))) (defn- make-initial-state - [initial-data] + [initial-data initial-errors] (let [initial (if (fn? initial-data) (initial-data) initial-data) - initial (d/nilv initial {})] + initial (d/nilv initial {}) + initial-errors (if (fn? initial-errors) (initial-errors) initial-errors) + initial-errors (d/nilv initial-errors {})] {:initial initial + :initial-errors initial-errors :data initial + :extra-errors initial-errors :errors {} :touched {}})) @@ -64,9 +68,11 @@ (-reset! [_ new-value] (if (nil? new-value) (let [initial (-> (mf/ref-val internal-state) - (get :initial) - (make-initial-state))] - (mf/set-ref-val! internal-state initial)) + (get :initial)) + initial-errors (-> (mf/ref-val internal-state) + (get :initial-errors)) + state (make-initial-state initial initial-errors)] + (mf/set-ref-val! internal-state state)) (mf/set-ref-val! internal-state new-value)) (rerender-fn)) @@ -92,12 +98,12 @@ (rerender-fn))))) (defn use-form - [& {:keys [initial schema validators] :as opts}] + [& {:keys [initial initial-errors schema validators] :as opts}] (let [rerender-fn (use-rerender-fn) initial - (mf/with-memo [initial] - (make-initial-state initial)) + (mf/with-memo [initial initial-errors] + (make-initial-state initial initial-errors)) internal-state (mf/use-ref initial) @@ -131,14 +137,16 @@ (assoc-in [:touched field] true) (assoc-in [:data field] (if trim? (str/trim value) value)) (update :errors clean-errors) - (update :extra-errors clean-errors))))))) + (update :extra-errors clean-errors) + (update :extra-errors dissoc ""))))))) (defn update-input-value! [form field value] (swap! form (fn [state] (-> state (assoc-in [:data field] value) - (update :errors dissoc field))))) + (update :errors dissoc field) + (update :extra-errors dissoc ""))))) (defn on-input-blur [form field]