From c4d1a1bc94a1bdf0ba4543663fecb52970312fab Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 11 Aug 2026 13:03:29 +0200 Subject: [PATCH 1/6] :bug: Fix node deleting (#11126) * :bug: Fix delete path node * :bug: Fix typography on shortcuts list --- frontend/src/app/main/data/shortcuts.cljs | 35 +++++++++++-------- .../main/data/workspace/path/shortcuts.cljs | 8 ++++- frontend/src/app/main/ui/shortcuts.scss | 3 +- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/main/data/shortcuts.cljs b/frontend/src/app/main/data/shortcuts.cljs index 19a473ae54..fa5e86e547 100644 --- a/frontend/src/app/main/data/shortcuts.cljs +++ b/frontend/src/app/main/data/shortcuts.cljs @@ -204,20 +204,27 @@ (defn- bind! [shortcuts] - (->> shortcuts - (remove #(:disabled (second %))) - (run! (fn [[key {:keys [command fn type overwrite]}]] - (let [callback (wrap-cb key fn) - commands (if (vector? command) - (into-array command) - #js [command])] - (if (vector? type) - (do (mousetrap/bind commands callback (nth type 0) overwrite) - (mousetrap/bind commands callback (nth type 1) overwrite)) - (let [undefined (js* "(void 0)")] - (if type - (mousetrap/bind commands callback type overwrite) - (mousetrap/bind commands callback undefined overwrite))))))))) + (let [entries (remove #(:disabled (second %)) shortcuts) + bind-fn (fn [[key {:keys [command fn type overwrite]}]] + (let [callback (wrap-cb key fn) + commands (if (vector? command) + (into-array command) + #js [command])] + (if (vector? type) + (do (mousetrap/bind commands callback (nth type 0) overwrite) + (mousetrap/bind commands callback (nth type 1) overwrite)) + (let [undefined (js* "(void 0)")] + (if type + (mousetrap/bind commands callback type overwrite) + (mousetrap/bind commands callback undefined overwrite))))))] + ;; Bind non-overwrite entries first so that entries flagged with + ;; `:overwrite` are bound last and can reliably splice out the + ;; colliding callbacks bound earlier (mousetrap's overwrite only + ;; removes callbacks that were already registered for the same + ;; combo). Map iteration order is hash-based, so we must force the + ;; order explicitly. + (run! bind-fn (remove (comp :overwrite second) entries)) + (run! bind-fn (filter (comp :overwrite second) entries)))) (defn- reset! ([] diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs index fe35b33e40..f4ed3dd8f3 100644 --- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -37,6 +37,7 @@ :command "p" :subsections [:path-editor] :section [:workspace] + :overwrite true :fn #(st/emit! (drp/change-edit-mode :draw))} :add-node {:tooltip (ds/shift "+") @@ -49,7 +50,9 @@ :command ["del" "backspace"] :subsections [:path-editor] :section [:workspace] - :fn #(st/emit! (drp/remove-node))} + :overwrite true + :fn #(st/emit! + (drp/remove-node))} :merge-nodes {:tooltip (ds/meta "J") :command (ds/c-mod "j") @@ -67,6 +70,7 @@ :command "k" :subsections [:path-editor] :section [:workspace] + :overwrite true :fn #(st/emit! (drp/separate-nodes))} :make-corner {:tooltip "X" @@ -79,6 +83,7 @@ :command "c" :subsections [:path-editor] :section [:workspace] + :overwrite true :fn #(st/emit! (drp/make-curve))} :snap-nodes {:tooltip (ds/meta "'") @@ -91,6 +96,7 @@ :escape {:tooltip (ds/esc) :command ["escape" "enter" "v"] :section [:workspace] + :overwrite true :fn #(st/emit! (esc-pressed))} :undo {:tooltip (ds/meta "Z") diff --git a/frontend/src/app/main/ui/shortcuts.scss b/frontend/src/app/main/ui/shortcuts.scss index 7d582e4e71..b92405e5c4 100644 --- a/frontend/src/app/main/ui/shortcuts.scss +++ b/frontend/src/app/main/ui/shortcuts.scss @@ -21,7 +21,7 @@ .section-title, .subsection-title { - @include t.use-typography("title-small"); + @include t.use-typography("headline-small"); display: flex; align-items: center; @@ -43,6 +43,7 @@ } .subsection-title { + block-size: $sz-32; text-transform: none; padding-inline-start: var(--sp-m); } From d7daefafe21f1623413f0c94046e030e6085bbb1 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 11 Aug 2026 13:05:47 +0200 Subject: [PATCH 2/6] :bug: Fix select shape after enter path edition (#11205) --- .../main/data/workspace/path/shortcuts.cljs | 13 +++- .../data/workspace_path_edition_test.cljs | 78 +++++++++++++++++++ frontend/test/frontend_tests/runner.cljs | 2 + 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 frontend/test/frontend_tests/data/workspace_path_edition_test.cljs diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs index f4ed3dd8f3..71403d731f 100644 --- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -94,7 +94,18 @@ :fn #(st/emit! (drp/toggle-snap))} :escape {:tooltip (ds/esc) - :command ["escape" "enter" "v"] + :command ["escape" "v"] + :section [:workspace] + :fn #(st/emit! (esc-pressed))} + + ;; Reuses the `:start-editing` key (instead of adding "enter" to + ;; the `:escape` command above) so that merging this shortcut set + ;; on top of the base workspace shortcuts (see `dsc/push-shortcuts`) + ;; deterministically replaces the workspace's `enter` binding + ;; (which enters path edit mode) instead of both ending up bound + ;; to the same physical key at once. + :start-editing {:tooltip (ds/enter) + :command "enter" :section [:workspace] :overwrite true :fn #(st/emit! (esc-pressed))} diff --git a/frontend/test/frontend_tests/data/workspace_path_edition_test.cljs b/frontend/test/frontend_tests/data/workspace_path_edition_test.cljs new file mode 100644 index 0000000000..4cfabcdce2 --- /dev/null +++ b/frontend/test/frontend_tests/data/workspace_path_edition_test.cljs @@ -0,0 +1,78 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.data.workspace-path-edition-test + (:require + [app.common.data :as d] + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.shapes :as cths] + [app.main.data.shortcuts :as dsc] + [app.main.data.workspace :as dw] + [app.main.data.workspace.path.shortcuts :as psc] + [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.shortcuts :as wsc] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.pages :as thp] + [frontend-tests.helpers.state :as ths] + [potok.v2.core :as ptk])) + +(t/use-fixtures :each + {:before thp/reset-idmap!}) + +(defn- enter-command? + [command] + (if (vector? command) + (some #(= % "enter") command) + (= command "enter"))) + +(t/deftest test-enter-key-is-bound-once-while-path-editing + ;; Regression test for the physical "enter" key ending up bound to + ;; two different shortcuts at once while path editing is active: one + ;; that (re)enters edition mode and one that exits it. `push-shortcuts` + ;; merges shortcut groups by map key (see `app.main.data.shortcuts`), + ;; not by physical key/command, so two shortcuts under different keys + ;; that both claim "enter" survive the merge and both would fire on a + ;; single keypress, breaking the toggle. + (let [file (cthf/sample-file :file1) + store (ths/setup-store file)] + (ptk/emit! store (dsc/push-shortcuts ::workspace wsc/shortcuts :workspace)) + (ptk/emit! store (dsc/push-shortcuts ::path psc/shortcuts :workspace :merge-shortcuts :auto)) + (let [effective (get-in @store [:shortcuts ::path]) + matches (->> effective + (filter (fn [[_ sc]] (enter-command? (:command sc)))) + (map first))] + (t/is (= 1 (count matches)) + (str "expected exactly one shortcut bound to \"enter\" while path editing, got " matches))))) + +(defn- run-scenario + [shape-type] + (let [file (-> (cthf/sample-file :file1) + (cths/add-sample-shape :test-shape :type shape-type)) + shape-id (:id (cths/get-shape file :test-shape)) + store (ths/setup-store file)] + ;; Select the shape, then reproduce what a physical Enter keypress + ;; now dispatches at each step: `start-editing-selected` to enter + ;; path edition mode, `esc-pressed` (-> :interrupt) to exit it, and + ;; `start-editing-selected` again to re-enter. + (ptk/emit! store (dws/select-shapes (d/ordered-set shape-id))) + + (ptk/emit! store (dw/start-editing-selected)) + (t/is (= shape-id (get-in @store [:workspace-local :edition])) + (str "expected " (name shape-type) " to enter path edition mode")) + + (ptk/emit! store (psc/esc-pressed)) + (t/is (nil? (get-in @store [:workspace-local :edition])) + (str "expected " (name shape-type) " to exit path edition mode")) + (t/is (= #{shape-id} (get-in @store [:workspace-local :selected])) + (str "expected " (name shape-type) " to remain selected after exiting path edition mode")) + + (ptk/emit! store (dw/start-editing-selected)) + (t/is (= shape-id (get-in @store [:workspace-local :edition])) + (str "expected " (name shape-type) " to enter path edition mode again")))) + +(t/deftest test-enter-toggles-path-editing-mode + (doseq [shape-type [:rect :circle :path :image]] + (run-scenario shape-type))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index e0c06b4a08..7c0c5f0889 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -21,6 +21,7 @@ [frontend-tests.data.workspace-mcp-test] [frontend-tests.data.workspace-media-test] [frontend-tests.data.workspace-pages-test] + [frontend-tests.data.workspace-path-edition-test] [frontend-tests.data.workspace-reflow-test] [frontend-tests.data.workspace-shortcuts-test] [frontend-tests.data.workspace-texts-test] @@ -113,6 +114,7 @@ 'frontend-tests.data.workspace-mcp-test 'frontend-tests.data.workspace-media-test 'frontend-tests.data.workspace-pages-test + 'frontend-tests.data.workspace-path-edition-test 'frontend-tests.data.workspace-reflow-test 'frontend-tests.data.workspace-shortcuts-test 'frontend-tests.data.workspace-texts-test From 69ef7e86cd23d2a93062f9d8dc8b1ee4ab075e71 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 11 Aug 2026 13:06:30 +0200 Subject: [PATCH 3/6] :bug: Fix colorpicker z-index (#11207) --- .../src/app/main/ui/workspace/colorpicker.scss | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.scss b/frontend/src/app/main/ui/workspace/colorpicker.scss index b9fe67d6f5..d69a25f651 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker.scss @@ -12,16 +12,20 @@ @use "refactor/basic-rules.scss" as *; .colorpicker-tooltip { - @extend %modal-background; - --colorpicker-width: #{$sz-284}; - left: calc(10 * px2rem(140)); - padding: var(--sp-m); - width: var(--colorpicker-width); - overflow: auto; + position: absolute; display: flex; flex-direction: column; + inset-inline-start: calc(10 * px2rem(140)); + inline-size: var(--colorpicker-width); + padding: var(--sp-m); + border-radius: $br-8; + box-shadow: 0 0 12px 0 var(--color-shadow-dark); + color: var(--color-foreground-primary); + background-color: var(--color-background-primary); + z-index: var(--z-index-set); + overflow: auto; } .colorpicker { From 02c31e734892aadc3b4165230ab7325798778b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Tue, 11 Aug 2026 13:30:04 +0200 Subject: [PATCH 4/6] :bug: Cache Nitrate SSO checks during navigation (#11209) --- frontend/src/app/main/ui/routes.cljs | 52 +++++++++--- frontend/test/frontend_tests/runner.cljs | 2 + .../test/frontend_tests/ui/routes_test.cljs | 85 +++++++++++++++++++ 3 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 frontend/test/frontend_tests/ui/routes_test.cljs diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index 5b5c985043..cc0d07537d 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.routes (:require [app.common.data.macros :as dm] + [app.common.time :as ct] [app.common.uri :as u] [app.common.uuid :as uuid] [app.config :as cf] @@ -21,6 +22,12 @@ [cuerdas.core :as str] [potok.v2.core :as ptk])) +(def ^:private sso-authorization-max-age-ms + (* 5 60 1000)) + +(defonce ^:private sso-authorization-cache + (atom {})) + (def routes [["/auth" ["/login" :auth-login] @@ -102,26 +109,43 @@ "Authorization filter for dashboard and workspace routes. Checks if the team being navigated to has an organization with SSO active. If so, calls :check-nitrate-sso and either proceeds with navigation - or redirects to the SSO provider URL." + or redirects to the SSO provider URL. Successful checks are cached for five + minutes per profile and team; redirect results are never cached." [match send-event-info? url] - (let [route-name (name (get-in match [:data :name])) - relevant? (and (contains? cf/flags :admin-console) - (or (str/starts-with? route-name "dashboard") - (str/starts-with? route-name "workspace"))) - team-id-str (when relevant? - (or (get-in match [:query-params :team-id]) - (get-in match [:params :path :team-id]))) - team-id (some-> team-id-str uuid/parse*)] - (if (some? team-id) + (let [route-name (name (get-in match [:data :name])) + relevant? (and (contains? cf/flags :admin-console) + (or (str/starts-with? route-name "dashboard") + (str/starts-with? route-name "workspace"))) + team-id-str (when relevant? + (or (get-in match [:query-params :team-id]) + (get-in match [:params :path :team-id]))) + team-id (some-> team-id-str uuid/parse*) + profile-id (get-in @st/state [:profile :id]) + cache-key [profile-id team-id] + authorized-at (get @sso-authorization-cache cache-key) + cache-valid? (and (some? authorized-at) + (< (ct/diff-ms authorized-at (ct/now)) + sso-authorization-max-age-ms)) + navigate #(st/emit! (rt/navigated match send-event-info?))] + (cond + (nil? team-id) + (navigate) + + cache-valid? + (navigate) + + :else (->> (rp/cmd! :check-nitrate-sso {:team-id team-id :url url}) (rx/subs! (fn [{:keys [authorized redirect-uri]}] (if authorized - (st/emit! (rt/navigated match send-event-info?)) - (when redirect-uri (st/emit! (rt/nav-raw :uri (str redirect-uri)))))) + (do + (swap! sso-authorization-cache assoc cache-key (ct/now)) + (navigate)) + (when redirect-uri + (st/emit! (rt/nav-raw :uri (str redirect-uri)))))) (fn [cause] - (errors/on-error cause)))) - (st/emit! (rt/navigated match send-event-info?))))) + (errors/on-error cause))))))) (defn- handle-sso-error-and-navigate "Check if the current route has an SSO error marker. If so, assign an diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 7c0c5f0889..a29132a22e 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -72,6 +72,7 @@ [frontend-tests.ui.gradient-handlers-test] [frontend-tests.ui.layout-container-multiple-test] [frontend-tests.ui.measures-menu-props-test] + [frontend-tests.ui.routes-test] [frontend-tests.ui.settings-password-schema-test] [frontend-tests.ui.settings-shortcuts-test] [frontend-tests.util-clipboard-test] @@ -163,6 +164,7 @@ 'frontend-tests.ui.gradient-handlers-test 'frontend-tests.ui.layout-container-multiple-test 'frontend-tests.ui.measures-menu-props-test + 'frontend-tests.ui.routes-test 'frontend-tests.render-dimensions-test 'frontend-tests.text-editor-paste-guard-test 'frontend-tests.ui.settings-password-schema-test diff --git a/frontend/test/frontend_tests/ui/routes_test.cljs b/frontend/test/frontend_tests/ui/routes_test.cljs new file mode 100644 index 0000000000..3ebb7edbbe --- /dev/null +++ b/frontend/test/frontend_tests/ui/routes_test.cljs @@ -0,0 +1,85 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.ui.routes-test + (:require + [app.common.time :as ct] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.main.repo :as rp] + [app.main.store :as st] + [app.main.ui.routes :as routes] + [beicon.v2.core :as rx] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.mock :as mock])) + +(defn- workspace-match + [team-id] + {:data {:name :workspace} + :params {:path {}} + :query-params {:team-id (str team-id)}}) + +(t/deftest sso-check-is-cached-for-five-minutes + (let [team-id (uuid/next) + match (workspace-match team-id) + now (atom (ct/inst "2026-08-11T10:00:00Z")) + rpc-calls (atom 0) + events (atom [])] + (with-redefs [cf/flags (conj cf/flags :admin-console) + ct/now (mock/stub (fn [] @now)) + rp/cmd! (mock/stub + (fn [command params] + (t/is (= :check-nitrate-sso command)) + (t/is (= team-id (:team-id params))) + (swap! rpc-calls inc) + (rx/of {:authorized true}))) + st/emit! (mock/stub + (fn [& emitted] + (swap! events into emitted)))] + (#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace") + (reset! now (ct/plus @now #js {:minutes 4 :seconds 59})) + (#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace") + + (t/is (= 1 @rpc-calls)) + (t/is (= 2 (count @events)))))) + +(t/deftest sso-check-is-refreshed-after-five-minutes + (let [team-id (uuid/next) + match (workspace-match team-id) + now (atom (ct/inst "2026-08-11T10:00:00Z")) + rpc-calls (atom 0)] + (with-redefs [cf/flags (conj cf/flags :admin-console) + ct/now (mock/stub (fn [] @now)) + rp/cmd! (mock/stub + (fn [_ _] + (swap! rpc-calls inc) + (rx/of {:authorized true}))) + st/emit! mock/noop] + (#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace") + (reset! now (ct/plus @now #js {:minutes 5})) + (#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace") + + (t/is (= 2 @rpc-calls))))) + +(t/deftest sso-redirect-result-is-not-cached + (let [team-id (uuid/next) + match (workspace-match team-id) + rpc-calls (atom 0) + events (atom [])] + (with-redefs [cf/flags (conj cf/flags :admin-console) + rp/cmd! (mock/stub + (fn [_ _] + (swap! rpc-calls inc) + (rx/of {:authorized false + :redirect-uri "https://idp.example.com/authorize"}))) + st/emit! (mock/stub + (fn [& emitted] + (swap! events into emitted)))] + (#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace") + (#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace") + + (t/is (= 2 @rpc-calls)) + (t/is (= 2 (count @events)))))) From 53985dc630b0c39ea2d20f9d807af1b2d44c1d95 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 11 Aug 2026 13:49:48 +0200 Subject: [PATCH 5/6] :bug: Fix setting dark theme on onboarding (#11212) * :bug: Fix setting dark theme on onboarding * :tada: Add test --- backend/src/app/rpc/commands/auth.clj | 2 +- .../register/verify-token-email-verified.json | 28 ++++++++++++ frontend/playwright/ui/pages/RegisterPage.js | 36 +++++++++++++++ .../ui/specs/email-verification.spec.js | 45 +++++++++++++++++++ frontend/src/app/main/data/profile.cljs | 40 +++++++++-------- 5 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 frontend/playwright/data/register/verify-token-email-verified.json create mode 100644 frontend/playwright/ui/specs/email-verification.spec.js diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 07460633c4..78d2ac45c4 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -367,7 +367,7 @@ email (str/lower email) fullname (d/normalize-string (:fullname params)) locale (d/normalize-string locale) - theme (d/normalize-string theme) + theme (some-> theme d/normalize-string not-empty) photo-id (some->> (or (:oidc/picture props) (:google/picture props) diff --git a/frontend/playwright/data/register/verify-token-email-verified.json b/frontend/playwright/data/register/verify-token-email-verified.json new file mode 100644 index 0000000000..347bc4057c --- /dev/null +++ b/frontend/playwright/data/register/verify-token-email-verified.json @@ -0,0 +1,28 @@ +{ + "~:iss": "~:verify-email", + "~:profile-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:profile": { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:email": "foo@example.com", + "~:fullname": "Princesa Leia", + "~:auth-backend": "penpot", + "~:is-active": true, + "~:is-demo": false, + "~:is-muted": false, + "~:is-blocked": false, + "~:theme": "", + "~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713533116365", + "~:modified-at": "~m1713533116365", + "~:props": { + "~:nudge": { + "~:big": 10, + "~:small": 1 + }, + "~:v2-info-shown": true, + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false + } + } +} diff --git a/frontend/playwright/ui/pages/RegisterPage.js b/frontend/playwright/ui/pages/RegisterPage.js index 8d3633e678..ef43f56469 100644 --- a/frontend/playwright/ui/pages/RegisterPage.js +++ b/frontend/playwright/ui/pages/RegisterPage.js @@ -29,6 +29,42 @@ export class RegisterPage extends BasePage { ); } + /** + * Mocks a successful email-verification token exchange (the link the + * user clicks from the verification email) and every RPC the dashboard + * needs to render right after landing on it, so the flow can be + * exercised end-to-end without a real backend. + */ + async setupEmailVerificationSuccess() { + await this.mockConfigFlags(["disable-onboarding"]); + await this.mockRPC( + "verify-token", + "register/verify-token-email-verified.json", + ); + await this.mockRPCs({ + "get-teams": "logged-in-user/get-teams-default.json", + "get-font-variants?team-id=*": + "logged-in-user/get-font-variants-empty.json", + "get-projects?team-id=*": "logged-in-user/get-projects-default.json", + "get-team-members?team-id=*": + "logged-in-user/get-team-members-your-penpot.json", + "get-team-users?team-id=*": + "logged-in-user/get-team-users-single-user.json", + "get-unread-comment-threads?team-id=*": + "logged-in-user/get-team-users-single-user.json", + "get-team-recent-files?team-id=*": + "logged-in-user/get-team-recent-files-empty.json", + "get-profiles-for-file-comments": + "logged-in-user/get-profiles-for-file-comments-empty.json", + "get-builtin-templates": + "logged-in-user/get-built-in-templates-empty.json", + }); + } + + async goToVerifyToken(token = "verify-email-token") { + await this.page.goto(`/#/auth/verify-token?token=${token}`); + } + static async init(page) { await BasePage.init(page); } diff --git a/frontend/playwright/ui/specs/email-verification.spec.js b/frontend/playwright/ui/specs/email-verification.spec.js new file mode 100644 index 0000000000..6be76653ce --- /dev/null +++ b/frontend/playwright/ui/specs/email-verification.spec.js @@ -0,0 +1,45 @@ +import { test, expect } from "@playwright/test"; +import { RegisterPage } from "../pages/RegisterPage"; + +// Regression test for the bug where a freshly verified account (whose +// profile never had a theme persisted) ended up with an empty string as +// its theme instead of falling back to the dark default: the workspace +// switched to light mode and Settings > UI Theme showed a blank field. + +test.beforeEach(async ({ page }) => { + await RegisterPage.initWithLoggedOutUser(page); +}); + +test.describe("Email verification", () => { + test("Newly verified account defaults to the dark theme", async ({ + page, + }) => { + const registerPage = new RegisterPage(page); + await registerPage.setupEmailVerificationSuccess(); + + await registerPage.goToVerifyToken(); + await page.waitForURL("**/dashboard/**"); + + // `default` is the body class applied for dark theme, `light` for + // light theme (see app.util.theme/set-color-scheme). + await expect(page.locator("body")).toHaveClass(/default/); + await expect(page.locator("body")).not.toHaveClass(/light/); + }); + + test("Settings > UI Theme shows Penpot Dark (default) selected, not blank", async ({ + page, + }) => { + const registerPage = new RegisterPage(page); + await registerPage.setupEmailVerificationSuccess(); + + await registerPage.goToVerifyToken(); + await page.waitForURL("**/dashboard/**"); + + await page.goto("/#/settings/options"); + + // The language select is the first combobox on the page, the theme + // select is the second one. + const themeSelect = page.getByRole("combobox").nth(1); + await expect(themeSelect).toHaveText("Penpot Dark (default)"); + }); +}); diff --git a/frontend/src/app/main/data/profile.cljs b/frontend/src/app/main/data/profile.cljs index 93a948c119..3f1237c2b7 100644 --- a/frontend/src/app/main/data/profile.cljs +++ b/frontend/src/app/main/data/profile.cljs @@ -43,28 +43,30 @@ (defn set-profile "Initialize profile state, only logged-in profile data should be passed to this event" - [{:keys [id] :as profile}] - (ptk/reify ::set-profile - IDeref - (-deref [_] profile) + [profile] + (let [profile (update profile :theme not-empty) + id (:id profile)] + (ptk/reify ::set-profile + IDeref + (-deref [_] profile) - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc :profile-id id) - (assoc :profile profile))) + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc :profile-id id) + (assoc :profile profile))) - ptk/WatchEvent - (watch [_ state _] - (let [profile (:profile state)] - (->> (rx/from (i18n/set-locale (:lang profile))) - (rx/ignore)))) + ptk/WatchEvent + (watch [_ state _] + (let [profile (:profile state)] + (->> (rx/from (i18n/set-locale (:lang profile))) + (rx/ignore)))) - ptk/EffectEvent - (effect [_ state _] - (let [profile (:profile state)] - (swap! storage/user assoc :profile profile) - (plugins.register/init))))) + ptk/EffectEvent + (effect [_ state _] + (let [profile (:profile state)] + (swap! storage/user assoc :profile profile) + (plugins.register/init)))))) (def profile-fetched? (ptk/type? ::profile-fetched)) From 985d219810a1740f14297aeee212869e3087b06f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Tue, 11 Aug 2026 14:36:07 +0200 Subject: [PATCH 6/6] :bug: Fix confusing copy for feams in organizations (#11213) --- frontend/translations/en.po | 4 ++-- frontend/translations/es.po | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index c9dcc0f1dd..e5e8967f42 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1243,7 +1243,7 @@ msgstr "Your project has been moved successfully" #, unused msgid "dashboard.team-belong-organization" -msgstr "This team now belongs to %s" +msgstr "This team is now part of the organization %s" #: src/app/main/ui/dashboard/team.cljs:1602 msgid "dashboard.team-info" @@ -1255,7 +1255,7 @@ msgstr "Team members" #, unused msgid "dashboard.team-no-longer-belong-organization" -msgstr "This team no longer belongs to the organization %s" +msgstr "This team is no longer part of the organization %s" #: src/app/main/ui/dashboard/team.cljs:1609 msgid "dashboard.team-organization" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 1fb282b4e1..84790f1d86 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1254,7 +1254,7 @@ msgstr "Tu proyecto ha sido movido con éxito" #, unused msgid "dashboard.team-belong-organization" -msgstr "Este equipo ahora pertenece a la organización %s" +msgstr "Este equipo ahora es parte de la organización %s" #: src/app/main/ui/dashboard/team.cljs:1602 msgid "dashboard.team-info" @@ -1266,7 +1266,7 @@ msgstr "Integrantes del equipo" #, unused msgid "dashboard.team-no-longer-belong-organization" -msgstr "Este equipo ya no pertenece a la organización %s" +msgstr "Este equipo ya no es parte de la organización %s" #: src/app/main/ui/dashboard/team.cljs:1609 msgid "dashboard.team-organization"