Merge remote-tracking branch 'refs/remotes/origin/develop' into develop

This commit is contained in:
Andrey Antukh 2026-05-27 13:37:17 +02:00
commit 1a1c7355e2
9 changed files with 127 additions and 31 deletions

View File

@ -147,10 +147,9 @@
(defn get-leave-org-summary
[cfg default-team-id teams-to-delete teams-to-transfer-count teams-to-exit-count]
(let [{:keys [deletable-team-ids delete-default-team? detach-from-org-team-ids]}
(let [{:keys [deletable-team-ids detach-from-org-team-ids]}
(build-leave-org-plan cfg default-team-id teams-to-delete nil)]
{:teams-to-delete (+ (count deletable-team-ids)
(if delete-default-team? 1 0))
{:teams-to-delete (count deletable-team-ids)
:teams-to-transfer teams-to-transfer-count
:teams-to-exit teams-to-exit-count
:teams-to-detach (count detach-from-org-team-ids)}))

View File

@ -1025,7 +1025,7 @@
:organization-id organization-id
:default-team-id (:id org-team)}))]
(t/is (th/success? out))
(t/is (= {:teams-to-delete 1
(t/is (= {:teams-to-delete 0
:teams-to-transfer 0
:teams-to-exit 0
:teams-to-detach 0}
@ -1052,7 +1052,7 @@
:organization-id organization-id
:default-team-id (:id org-team)}))]
(t/is (th/success? out))
(t/is (= {:teams-to-delete 2
(t/is (= {:teams-to-delete 1
:teams-to-transfer 0
:teams-to-exit 0
:teams-to-detach 0}
@ -1083,7 +1083,7 @@
:organization-id organization-id
:default-team-id (:id org-team)}))]
(t/is (th/success? out))
(t/is (= {:teams-to-delete 1
(t/is (= {:teams-to-delete 0
:teams-to-transfer 1
:teams-to-exit 0
:teams-to-detach 0}
@ -1113,7 +1113,7 @@
:organization-id organization-id
:default-team-id (:id org-team)}))]
(t/is (th/success? out))
(t/is (= {:teams-to-delete 1
(t/is (= {:teams-to-delete 0
:teams-to-transfer 0
:teams-to-exit 1
:teams-to-detach 0}

View File

@ -307,7 +307,7 @@
:id organization-id
:default-team-id your-penpot-id})]
(t/is (th/success? out))
(t/is (= {:teams-to-delete 1
(t/is (= {:teams-to-delete 0
:teams-to-transfer 0
:teams-to-exit 0
:teams-to-detach 0}

View File

@ -919,6 +919,7 @@ test.describe("Tokens - creation", () => {
test("User creates shadow token", async ({ page }) => {
const emptyNameError = "Name should be at least 1 character";
const emptyFieldError = "This field cannot be empty";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFileRender(page, {
@ -997,6 +998,14 @@ test.describe("Tokens - creation", () => {
await expect(emptyNameErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
// 5. Empty fill -> disabled + error message
await offsetXField.fill("3"); // Fill should be touched in order to show the error message
await offsetXField.fill("");
const emptyFieldErrorNode =
tokensUpdateCreateModal.getByText(emptyFieldError);
await expect(emptyFieldErrorNode).toBeVisible();
//
// ------- SUCCESSFUL FIELDS -------
//
@ -1102,6 +1111,28 @@ test.describe("Tokens - creation", () => {
await expect(
tokensTabPanel.getByRole("button", { name: "my-token-2" }),
).toBeEnabled();
//
// ------- THIRD TOKEN WITH EMPTY BLUR AND SPREAD -------
//
await addTokenButton.click();
await nameField.fill("my-token-3");
await colorField.fill("red");
// 1. Empty blur and spread
await blurField.fill("");
await spreadField.fill("");
await expect(submitButton).toBeEnabled();
await submitButton.click();
await unfoldTokenType(tokensTabPanel, "shadow");
await expect(
tokensTabPanel.getByRole("button", { name: "my-token-3" }),
).toBeEnabled();
});
test("User cant submit empty typography token or reference", async ({
@ -1691,13 +1722,21 @@ test.describe("Tokens - creation", () => {
});
});
test("User cannot create token with a conflicting name in other set", async ({ page }) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
test("User cannot create token with a conflicting name in other set", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokenThemesSetsSidebar,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFileRender(page);
await expect(tokensSidebar).toBeVisible();
await tokenThemesSetsSidebar.getByRole('button', { name: 'light', exact: true }).click();
await tokenThemesSetsSidebar
.getByRole("button", { name: "light", exact: true })
.click();
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
@ -1721,8 +1760,11 @@ test("User cannot create token with a conflicting name in other set", async ({ p
await nameField.fill("accent.default");
// An error message should appear and submit button should be disabled
await expect(tokensUpdateCreateModal.getByText('A token already exists at the path: accent.default'))
.toBeVisible()
await expect(
tokensUpdateCreateModal.getByText(
"A token already exists at the path: accent.default",
),
).toBeVisible();
await expect(submitButton).toBeDisabled();
@ -1730,8 +1772,11 @@ test("User cannot create token with a conflicting name in other set", async ({ p
await nameField.fill("colors.red");
// An error message should appear and submit button should be disabled
await expect(tokensUpdateCreateModal.getByText('A token already exists at the path: colors.red'))
.toBeVisible()
await expect(
tokensUpdateCreateModal.getByText(
"A token already exists at the path: colors.red",
),
).toBeVisible();
await expect(submitButton).toBeDisabled();

View File

@ -311,21 +311,39 @@
(swap! form (fn [state]
(-> state
(assoc-in [:data :value value-subfield index field] (if trim? (str/trim value) value))
(assoc-in [:touched :value value-subfield index field] true)
(update :errors dissoc :value)
(update :extra-errors dissoc :value)
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(mf/defc indexed-color-input*
[{:keys [name tokens token index value-subfield] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
token-name (get-in @form [:data :name] nil)
error
(get-in @form [:errors :value value-subfield index input-name])
touched?
(get-in @form [:touched :value value-subfield index input-name])
value
(get-in @form [:data :value value-subfield index input-name] "")
;; Resolution error for this specific field
indexed-error
(get-in @form [:errors :value value-subfield index input-name])
;; Empty-field error: scoped to this layer so each shadow layer is
;; evaluated independently from the others.
empty-error
(when (str/blank? value)
{:message (tr "errors.tokens.empty-field")})
error
(when touched? (or indexed-error empty-error))
color-resolved
(get-in @form [:data :value value-subfield index :color-result] "")

View File

@ -383,22 +383,40 @@
(swap! form (fn [state]
(-> state
(assoc-in [:data :value value-subfield index field] (if trim? (str/trim value) value))
(assoc-in [:touched :value value-subfield index field] true)
(update :errors dissoc :value)
(update :extra-errors dissoc :value)
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(mf/defc input-indexed*
[{:keys [name tokens token index value-subfield] :rest props}]
[{:keys [name tokens token index value-subfield nillable] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
token-name (get-in @form [:data :name] nil)
nillable (d/nilv nillable false)
error
(get-in @form [:errors :value value-subfield index input-name])
touched?
(get-in @form [:touched :value value-subfield index input-name])
value-from-form
(get-in @form [:data :value value-subfield index input-name] "")
;; Resolution error for this specific field (e.g. missing reference)
indexed-error
(get-in @form [:errors :value value-subfield index input-name])
;; Empty-field error: derived purely from the local field value so that
;; each shadow layer is evaluated independently.
empty-error
(when (and (not nillable)
(str/blank? value-from-form))
{:message (tr "errors.tokens.empty-field")})
error
(when touched? (or indexed-error empty-error))
resolve-stream
(mf/with-memo [token index input-name]
(if-let [value (get-in token [:value value-subfield index input-name])]
@ -428,7 +446,7 @@
props
(if error
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
:hint-message (or (:message error) (tr "errors.field-missing"))})
props)
props

View File

@ -57,10 +57,13 @@
(update :token-value (fn [value]
(->> (or value [])
(mapv (fn [shadow]
(d/update-when shadow :inset #(cond
(boolean? %) %
(= "true" %) true
:else false)))))))
(-> shadow
(d/update-when :inset #(cond
(boolean? %) %
(= "true" %) true
:else false))
(update :blur #(if (str/blank? %) "0" %))
(update :spread #(if (str/blank? %) "0" %))))))))
(assoc :validators [check-empty-shadow-token
check-shadow-token-self-reference]))]
@ -160,6 +163,7 @@
{:aria-label (tr "workspace.tokens.shadow-blur")
:placeholder (tr "workspace.tokens.shadow-blur")
:name :blur
:nillable true
:slot-start (mf/html [:span {:class (stl/css :visible-label)}
(str (tr "workspace.tokens.shadow-blur") ":")])
:token blur-token
@ -172,6 +176,7 @@
{:aria-label (tr "workspace.tokens.shadow-spread")
:placeholder (tr "workspace.tokens.shadow-spread")
:name :spread
:nillable true
:slot-start (mf/html [:span {:class (stl/css :visible-label)}
(str (tr "workspace.tokens.shadow-spread") ":")])
:token spread-token
@ -310,15 +315,14 @@
ref-valid? (and reference (not (str/blank? reference)))
shadows (get value :shadow)
;; To be a valid shadow it must contain one on each valid values
;; To be a valid shadow it must contain one on each valid values.
;; blur and spread are optional and default to "0" when blank.
valid-composite-shadow?
(and (seq shadows)
(every?
(fn [{:keys [offset-x offset-y blur spread color]}]
(fn [{:keys [offset-x offset-y color]}]
(and (not (str/blank? offset-x))
(not (str/blank? offset-y))
(not (str/blank? blur))
(not (str/blank? spread))
(not (str/blank? color))))
shadows))]
@ -333,7 +337,11 @@
(vector? value)
{:reference nil
:shadow value}
:shadow (mapv (fn [shadow]
(-> shadow
(update :blur #(if (str/blank? %) "0" %))
(update :spread #(if (str/blank? %) "0" %))))
value)}
:else
{:reference nil

View File

@ -8476,6 +8476,10 @@ msgstr "Edit %s token"
msgid "errors.tokens.empty-input"
msgstr "Token value cannot be empty"
#: src/app/main/data/workspace/tokens/errors.cljs
msgid "errors.tokens.empty-field"
msgstr "This field cannot be empty"
#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241
msgid "workspace.tokens.enter-token-name"
msgstr "Enter %s token name"

View File

@ -8232,6 +8232,10 @@ msgstr "Editar token de %s"
msgid "errors.tokens.empty-input"
msgstr "El valor del token no puede estar vacío"
#: src/app/main/data/workspace/tokens/errors.cljs
msgid "errors.tokens.empty-field"
msgstr "Este campo no puede estar vacío"
#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241
msgid "workspace.tokens.enter-token-name"
msgstr "Introduce un nombre para el token %s"