From 245190f4f964b4335255e1a9f5b9d0c96a20c0ea Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 22 Oct 2025 21:36:55 +0200 Subject: [PATCH 1/9] :bug: Fix variant validation when nil --- common/src/app/common/files/validate.cljc | 30 ++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index 542652bf04..44a98ee8f8 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -438,19 +438,20 @@ children (map #(ctst/get-shape page %) shapes) prop-names (cfv/extract-properties-names (first children) (:data file))] (doseq [child children] - (if (not (ctk/is-variant? child)) - (report-error :not-a-variant - (str/ffmt "Shape % should be a variant" (:id child)) - child file page) - (do - (when (not= (:variant-id child) shape-id) - (report-error :invalid-variant-id - (str/ffmt "Variant % has invalid variant-id %" (:id child) (:variant-id child)) - child file page)) - (when (not= prop-names (cfv/extract-properties-names child (:data file))) - (report-error :invalid-variant-properties - (str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names)) - child file page))))))) + (when child + (if (not (ctk/is-variant? child)) + (report-error :not-a-variant + (str/ffmt "Shape % should be a variant" (:id child)) + child file page) + (do + (when (not= (:variant-id child) shape-id) + (report-error :invalid-variant-id + (str/ffmt "Variant % has invalid variant-id %" (:id child) (:variant-id child)) + child file page)) + (when (not= prop-names (cfv/extract-properties-names child (:data file))) + (report-error :invalid-variant-properties + (str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names)) + child file page)))))))) (defn- check-variant "Shape is a variant, so @@ -599,7 +600,8 @@ main-component (if (:deleted component) (dm/get-in component [:objects (:main-instance-id component)]) (ctst/get-shape component-page (:main-instance-id component)))] - (when-not (ctk/is-variant? main-component) + (when (and main-component + (not (ctk/is-variant? main-component))) (report-error :not-a-variant (str/ffmt "Shape % should be a variant" (:id main-component)) main-component file component-page)))) From cd6865f54b175317faa1f46e99eee5115ed6c5f7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 3 Nov 2025 19:54:02 +0100 Subject: [PATCH 2/9] :arrow_up: Update yetti dependency Bugfixes --- backend/deps.edn | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/deps.edn b/backend/deps.edn index a6f6fcfecd..31b5e48096 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -28,8 +28,8 @@ com.google.guava/guava {:mvn/version "33.4.8-jre"} funcool/yetti - {:git/tag "v11.6" - :git/sha "94dc017" + {:git/tag "v11.8" + :git/sha "1d1b33f" :git/url "https://github.com/funcool/yetti.git" :exclusions [org.slf4j/slf4j-api]} From 8307b699bf536099c24e72261dbe4b3fc90992d4 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 3 Nov 2025 19:55:37 +0100 Subject: [PATCH 3/9] :bug: Remove a race condition on file export Caused when file is deleted in the middle of an exportation. The current export process is not transactional, and on file deletion several queries can start return not-found exception because of concurrent file deletion. With the changes on this PR we allow query deleted files internally on the exportation process and make it resilent to possible concurrent deletion. --- backend/src/app/binfile/common.clj | 2 +- backend/src/app/binfile/v3.clj | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index 1f6ab99cf2..cf8508caa0 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -550,7 +550,7 @@ [cfg data file-id] (let [library-ids (get-libraries cfg [file-id])] (reduce (fn [data library-id] - (if-let [library (get-file cfg library-id)] + (if-let [library (get-file cfg library-id :include-deleted? true)] (ctf/absorb-assets data (:data library)) data)) data diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index 682ddc74de..cf88f71551 100644 --- a/backend/src/app/binfile/v3.clj +++ b/backend/src/app/binfile/v3.clj @@ -228,6 +228,7 @@ (db/tx-run! cfg (fn [cfg] (cond-> (bfc/get-file cfg file-id {:realize? true + :include-deleted? true :lock-for-update? true}) detach? (-> (ctf/detach-external-references file-id) @@ -285,14 +286,12 @@ (let [file (cond-> (select-keys file bfc/file-attrs) (:options data) - (assoc :options (:options data)) + (assoc :options (:options data))) - :always - (dissoc :data)) - - file (cond-> file - :always - (encode-file)) + file (-> file + (dissoc :data) + (dissoc :deleted-at) + (encode-file)) path (str "files/" file-id ".json")] (write-entry! output path file)) From cbae3dca3474fb8fb236298abd8b1210767460ff Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 3 Nov 2025 19:58:27 +0100 Subject: [PATCH 4/9] :sparkles: Simplify the approach for return streamable body Removing unnecesary syntax overhead with simplier abstraction --- backend/src/app/rpc.clj | 10 +++++++--- backend/src/app/rpc/commands/binfile.clj | 23 +++++++++-------------- backend/src/app/rpc/helpers.clj | 7 ++++++- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 63d778a89c..02e290bade 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -64,9 +64,13 @@ (let [mdata (meta result) response (if (fn? result) (result request) - (let [result (rph/unwrap result)] - {::yres/status (::http/status mdata 200) - ::yres/headers (::http/headers mdata {}) + (let [result (rph/unwrap result) + status (::http/status mdata 200) + headers (cond-> (::http/headers mdata {}) + (yres/stream-body? result) + (assoc "content-type" "application/octet-stream"))] + {::yres/status status + ::yres/headers headers ::yres/body result}))] (-> response (handle-response-transformation request mdata) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index f0fc211e46..98b2c04193 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -25,10 +25,10 @@ [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] + [app.rpc.helpers :as rph] [app.tasks.file-gc] [app.util.services :as sv] - [app.worker :as-alias wrk] - [yetti.response :as yres])) + [app.worker :as-alias wrk])) (set! *warn-on-reflection* true) @@ -44,7 +44,7 @@ (defn stream-export-v1 [cfg {:keys [file-id include-libraries embed-assets] :as params}] - (yres/stream-body + (rph/stream (fn [_ output-stream] (try (-> cfg @@ -59,7 +59,7 @@ (defn stream-export-v3 [cfg {:keys [file-id include-libraries embed-assets] :as params}] - (yres/stream-body + (rph/stream (fn [_ output-stream] (try (-> cfg @@ -79,16 +79,11 @@ ::sm/params schema:export-binfile} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id version file-id] :as params}] (files/check-read-permissions! pool profile-id file-id) - (fn [_] - (let [version (or version 1) - body (case (int version) - 1 (stream-export-v1 cfg params) - 2 (throw (ex-info "not-implemented" {})) - 3 (stream-export-v3 cfg params))] - - {::yres/status 200 - ::yres/headers {"content-type" "application/octet-stream"} - ::yres/body body}))) + (let [version (or version 1)] + (case (int version) + 1 (stream-export-v1 cfg params) + 2 (throw (ex-info "not-implemented" {})) + 3 (stream-export-v3 cfg params)))) ;; --- Command: import-binfile diff --git a/backend/src/app/rpc/helpers.clj b/backend/src/app/rpc/helpers.clj index a424b1f8f6..5b117b82fd 100644 --- a/backend/src/app/rpc/helpers.clj +++ b/backend/src/app/rpc/helpers.clj @@ -11,7 +11,7 @@ [app.common.data.macros :as dm] [app.http :as-alias http] [app.rpc :as-alias rpc] - [yetti.response :as-alias yres])) + [yetti.response :as yres])) ;; A utilty wrapper object for wrap service responses that does not ;; implements the IObj interface that make possible attach metadata to @@ -78,3 +78,8 @@ (let [exp (if (integer? max-age) max-age (inst-ms max-age)) val (dm/fmt "max-age=%" (int (/ exp 1000.0)))] (update response ::yres/headers assoc "cache-control" val))))) + +(defn stream + "A convenience allias for yetti.response/stream-body" + [f] + (yres/stream-body f)) From eaabe54c4b543b69b83a771e8401f8665de06f50 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 4 Nov 2025 09:58:24 +0100 Subject: [PATCH 5/9] :lipstick: Check the runner task exists as first condition --- backend/src/app/worker/runner.clj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/app/worker/runner.clj b/backend/src/app/worker/runner.clj index ea8cd08cf2..d2b0204121 100644 --- a/backend/src/app/worker/runner.clj +++ b/backend/src/app/worker/runner.clj @@ -131,6 +131,11 @@ [{:keys [::id ::timeout] :as cfg} task-id scheduled-at] (loop [task (get-task cfg task-id)] (cond + (nil? task) + (l/wrn :hint "no task found on the database" + :runner-id id + :task-id task-id) + (ex/exception? task) (if (or (db/connection-error? task) (db/serialization-error? task)) @@ -153,11 +158,6 @@ :task-id task-id :runner-id id) - (nil? task) - (l/wrn :hint "no task found on the database" - :runner-id id - :task-id task-id) - :else (let [result (run-task cfg task)] (with-meta result From c214cc15440d2af49514d040e682cbb6fe80f25e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 4 Nov 2025 09:58:55 +0100 Subject: [PATCH 6/9] :bug: Do not process runner result if no result returned --- backend/src/app/worker/runner.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app/worker/runner.clj b/backend/src/app/worker/runner.clj index d2b0204121..426d69a123 100644 --- a/backend/src/app/worker/runner.clj +++ b/backend/src/app/worker/runner.clj @@ -224,11 +224,11 @@ "failed" (handle-task-failure result) "completed" (handle-task-completion result) (throw (IllegalArgumentException. - (str "invalid status received: " status)))))) + (str "invalid status received: '" status "'")))))) (run-task-loop [[task-id scheduled-at]] (loop [result (run-task! cfg task-id scheduled-at)] - (when-let [cause (process-result result)] + (when-let [cause (some-> result process-result)] (if (or (db/connection-error? cause) (db/serialization-error? cause)) (do From 49721c0bcd752a3d980f69b9f27f583f35f020b4 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 4 Nov 2025 10:55:14 +0100 Subject: [PATCH 7/9] :sparkles: Add better logging context report on worker runner --- backend/src/app/config.clj | 4 ++++ backend/src/app/http/errors.clj | 17 ++++++++--------- backend/src/app/worker/runner.clj | 17 ++++++++++++----- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index e5339a25a4..18bec5e053 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -319,5 +319,9 @@ ([key default] (c/get config key default))) +(defn logging-context + [] + {:version/backend (:full version)}) + ;; Set value for all new threads bindings. (alter-var-root #'*assert* (constantly (contains? flags :backend-asserts))) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 0ad134c927..52d4196f18 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -25,15 +25,14 @@ (let [claims (-> {} (into (::session/token-claims request)) (into (::actoken/token-claims request)))] - {:request/path (:path request) - :request/method (:method request) - :request/params (:params request) - :request/user-agent (yreq/get-header request "user-agent") - :request/ip-addr (inet/parse-request request) - :request/profile-id (:uid claims) - :version/frontend (or (yreq/get-header request "x-frontend-version") "unknown") - :version/backend (:full cf/version)})) - + (-> (cf/logging-context) + (assoc :request/path (:path request)) + (assoc :request/method (:method request)) + (assoc :request/params (:params request)) + (assoc :request/user-agent (yreq/get-header request "user-agent")) + (assoc :request/ip-addr (inet/parse-request request)) + (assoc :request/profile-id (:uid claims)) + (assoc :version/frontend (or (yreq/get-header request "x-frontend-version") "unknown"))))) (defmulti handle-error (fn [cause _ _] diff --git a/backend/src/app/worker/runner.clj b/backend/src/app/worker/runner.clj index 426d69a123..f3f44bfd30 100644 --- a/backend/src/app/worker/runner.clj +++ b/backend/src/app/worker/runner.clj @@ -13,6 +13,7 @@ [app.common.schema :as sm] [app.common.time :as ct] [app.common.transit :as t] + [app.config :as cf] [app.db :as db] [app.metrics :as mtx] [app.redis :as rds] @@ -60,7 +61,8 @@ (defn get-error-context [_ item] - {:params item}) + (-> (cf/logging-context) + (assoc :params item))) (defn- get-task [{:keys [::db/pool]} task-id] @@ -213,6 +215,7 @@ :payload payload))) (catch Throwable cause (l/err :hint "unable to decode payload" + ::l/context (cf/logging-context) :payload payload :length (alength ^String/1 payload) :cause cause)))) @@ -236,9 +239,9 @@ :cause cause) (px/sleep timeout) (recur result)) - (do - (l/err :hint "unhandled exception on processing task result" - :cause cause))))))] + (l/err :hint "unhandled exception on processing task result" + ::l/context (cf/logging-context) + :cause cause)))))] (try (let [key (str/ffmt "penpot.worker.queue:%" queue) @@ -254,11 +257,14 @@ (if (rds/timeout-exception? cause) (do (l/err :hint "redis pop operation timeout, consider increasing redis timeout (will retry in some instants)" + ::l/context (cf/logging-context) :timeout timeout :cause cause) (px/sleep timeout)) - (l/err :hint "unhandled exception" :cause cause)))))) + (l/err :hint "unhandled exception" + ::l/context (cf/logging-context) + :cause cause)))))) (defn- start-thread! [{:keys [::id ::queue ::wrk/tenant] :as cfg}] @@ -284,6 +290,7 @@ :queue queue)) (catch Throwable cause (l/err :hint "unexpected exception" + ::l/context (cf/logging-context) :id id :queue queue :cause cause)) From 092a5139e3be10f8a96ac56be1081255b143bb5d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 4 Nov 2025 16:49:08 +0100 Subject: [PATCH 8/9] :bug: Fix incorrect token sets migration (#7673) --- common/src/app/common/types/tokens_lib.cljc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index df7aa38cf2..a215322e36 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -1991,13 +1991,19 @@ Will return a value that matches this schema: #?(:clj (defn- migrate-to-v1-4 - "Migrate the TokensLib data structure internals to v1.2 version; it + "Migrate the TokensLib data structure internals to v1.4 version; it expects input from v1.3 version" [params] (let [migrate-set-node (fn recurse [node] - (if (token-set-legacy? node) + (cond + (token-set-legacy? node) (make-token-set node) + + (token-set? node) + node + + :else (d/update-vals node recurse)))] (update params :sets d/update-vals migrate-set-node)))) From a576c0404a79b900ad55362766d5d452c00c460e Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 5 Nov 2025 12:05:00 +0100 Subject: [PATCH 9/9] :bug: Fix focus mode across page and file navigation (#7695) --- CHANGES.md | 1 + .../app/main/data/workspace/libraries.cljs | 2 +- .../app/main/data/workspace/path/drawing.cljs | 3 +- .../app/main/data/workspace/path/undo.cljs | 6 ++-- .../app/main/data/workspace/selection.cljs | 33 ++++++++++--------- .../app/main/data/workspace/thumbnails.cljs | 3 +- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fceece355a..963a6f5d85 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -75,6 +75,7 @@ - Fix paste without selection sends the new element in the back [Taiga #12382](https://tree.taiga.io/project/penpot/issue/12382) - Fix options button does not work for comments created in the lower part of the screen [Taiga #12422](https://tree.taiga.io/project/penpot/issue/12422) - Fix problem when checking usage with removed teams [Taiga #12442](https://tree.taiga.io/project/penpot/issue/12442) +- Fix focus mode persisting across page/file navigation [Taiga #12469](https://tree.taiga.io/project/penpot/issue/12469) ## 2.10.1 diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 1ad85bd56a..4fea19a2db 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -1278,7 +1278,7 @@ (watch [_ _ stream] (let [stopper-s (->> stream - (rx/filter #(or (= ::dw/finalize-page (ptk/type %)) + (rx/filter #(or (= ::dwpg/finalize-page (ptk/type %)) (= ::watch-component-changes (ptk/type %))))) workspace-data-s diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 85ae357533..d7a5409f1b 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -20,6 +20,7 @@ [app.main.data.helpers :as dsh] [app.main.data.workspace.drawing.common :as dwdc] [app.main.data.workspace.edition :as dwe] + [app.main.data.workspace.pages :as-alias dwpg] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.common :as common] [app.main.data.workspace.path.helpers :as helpers] @@ -43,7 +44,7 @@ (= type :app.main.data.workspace.path.shortcuts/esc-pressed) (= type :app.main.data.workspace.common/clear-edition-mode) (= type :app.main.data.workspace.edition/clear-edition-mode) - (= type :app.main.data.workspace/finalize-page) + (= type ::dwpg/finalize-page) (= event :interrupt) ;; ESC (and ^boolean (mse/mouse-event? event) ^boolean (mse/mouse-double-click-event? event))))) diff --git a/frontend/src/app/main/data/workspace/path/undo.cljs b/frontend/src/app/main/data/workspace/path/undo.cljs index 54b2f3eeaf..bd0a6efa9f 100644 --- a/frontend/src/app/main/data/workspace/path/undo.cljs +++ b/frontend/src/app/main/data/workspace/path/undo.cljs @@ -10,6 +10,8 @@ [app.common.data.undo-stack :as u] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.edition :as-alias dwe] + [app.main.data.workspace.pages :as-alias dwpg] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.common :as common] [app.main.data.workspace.path.state :as st] @@ -133,8 +135,8 @@ (defn- stop-undo? [event] (let [type (ptk/type event)] - (or (= :app.main.data.workspace.edition/clear-edition-mode type) - (= :app.main.data.workspace/finalize-page type)))) + (or (= ::dwe/clear-edition-mode type) + (= ::dwpg/finalize-page type)))) (def path-content-ref (letfn [(selector [state] diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 16459d7408..5424a280d9 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -24,6 +24,7 @@ [app.main.data.modal :as md] [app.main.data.workspace.collapse :as dwc] [app.main.data.workspace.edition :as dwe] + [app.main.data.workspace.pages :as-alias dwpg] [app.main.data.workspace.specialized-panel :as-alias dwsp] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.zoom :as dwz] @@ -596,21 +597,21 @@ ptk/WatchEvent (watch [_ state stream] (let [stopper (rx/filter #(or (= ::toggle-focus-mode (ptk/type %)) - (= :app.main.data.workspace/finalize-page (ptk/type %))) stream)] + (= ::dwpg/finalize-page (ptk/type %))) stream)] (when (d/not-empty? (:workspace-focus-selected state)) - (rx/merge - (rx/of dwz/zoom-to-selected-shape - (deselect-all)) - (->> (rx/from-atom refs/workspace-page-objects {:emit-current-value? true}) - (rx/take-until stopper) - (rx/map (comp set keys)) - (rx/buffer 2 1) - (rx/merge-map - ;; While focus is active, update it with any new and deleted shapes - (fn [[old-keys new-keys]] - (let [removed (set/difference old-keys new-keys) - added (set/difference new-keys old-keys)] + (->> (rx/merge + (rx/of dwz/zoom-to-selected-shape + (deselect-all)) + (->> (rx/from-atom refs/workspace-page-objects {:emit-current-value? true}) + (rx/map (comp set keys)) + (rx/buffer 2 1) + (rx/merge-map + ;; While focus is active, update it with any new and deleted shapes + (fn [[old-keys new-keys]] + (let [removed (set/difference old-keys new-keys) + added (set/difference new-keys old-keys)] - (if (or (d/not-empty? added) (d/not-empty? removed)) - (rx/of (update-focus-shapes added removed)) - (rx/empty)))))))))))) + (if (or (d/not-empty? added) (d/not-empty? removed)) + (rx/of (update-focus-shapes added removed)) + (rx/empty))))))) + (rx/take-until stopper))))))) diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 3e330baa0a..a8ff8fedf9 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -17,6 +17,7 @@ [app.main.data.helpers :as dsh] [app.main.data.persistence :as-alias dps] [app.main.data.workspace.notifications :as-alias wnt] + [app.main.data.workspace.pages :as-alias dwpg] [app.main.rasterizer :as thr] [app.main.refs :as refs] [app.main.render :as render] @@ -254,7 +255,7 @@ (let [stopper-s (rx/filter (fn [event] (as-> (ptk/type event) type - (or (= :app.main.data.workspace/finalize-page type) + (or (= ::dwpg/finalize-page type) (= ::watch-state-changes type)))) stream)