From d07e00da217f2dc819f3b8a5ad328dc4b2f7d00a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Oct 2025 14:27:32 +0200 Subject: [PATCH 1/5] :bug: Fix incorrect filtering of unread comment threads Do not return threads for deleted files --- backend/src/app/rpc/commands/comments.clj | 69 +++++++++++------------ 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index 6627157115..19d4633bf3 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -257,6 +257,8 @@ INNER JOIN project AS p ON (p.id = f.project_id) LEFT JOIN comment_thread_status AS cts ON (cts.thread_id = ct.id AND cts.profile_id = ?) LEFT JOIN profile AS pf ON (ct.owner_id = pf.id) + WHERE f.deleted_at IS NULL + AND p.deleted_at IS NULL WINDOW w AS (PARTITION BY c.thread_id ORDER BY c.created_at ASC)") (def ^:private sql:comment-threads-by-file-id @@ -270,7 +272,35 @@ ;; --- COMMAND: Get Unread Comment Threads -(declare ^:private get-unread-comment-threads) +(def ^:private sql:unread-all-comment-threads-by-team + (str "WITH threads AS (" sql:comment-threads ")" + "SELECT * FROM threads WHERE count_unread_comments > 0 AND team_id = ?")) + +;; The partial configuration will retrieve only comments created by the user and +;; threads that have a mention to the user. +(def ^:private sql:unread-partial-comment-threads-by-team + (str "WITH threads AS (" sql:comment-threads ")" + "SELECT * FROM threads + WHERE count_unread_comments > 0 + AND team_id = ? + AND (owner_id = ? OR ? = ANY(mentions))")) + +(defn- get-unread-comment-threads + [cfg profile-id team-id] + (let [profile (-> (db/get cfg :profile {:id profile-id}) + (profile/decode-row)) + notify (or (-> profile :props :notifications :dashboard-comments) :all)] + + (case notify + :all + (->> (db/exec! cfg [sql:unread-all-comment-threads-by-team profile-id team-id]) + (into [] xf-decode-row)) + + :partial + (->> (db/exec! cfg [sql:unread-partial-comment-threads-by-team profile-id team-id profile-id profile-id]) + (into [] xf-decode-row)) + + []))) (def ^:private schema:get-unread-comment-threads @@ -281,41 +311,8 @@ {::doc/added "1.15" ::sm/params schema:get-unread-comment-threads} [cfg {:keys [::rpc/profile-id team-id] :as params}] - (db/run! - cfg - (fn [{:keys [::db/conn]}] - (teams/check-read-permissions! conn profile-id team-id) - (get-unread-comment-threads conn profile-id team-id)))) - -(def sql:unread-all-comment-threads-by-team - (str "WITH threads AS (" sql:comment-threads ")" - "SELECT * FROM threads WHERE count_unread_comments > 0 AND team_id = ?")) - -;; The partial configuration will retrieve only comments created by the user and -;; threads that have a mention to the user. -(def sql:unread-partial-comment-threads-by-team - (str "WITH threads AS (" sql:comment-threads ")" - "SELECT * FROM threads - WHERE count_unread_comments > 0 - AND team_id = ? - AND (owner_id = ? OR ? = ANY(mentions))")) - -(defn- get-unread-comment-threads - [conn profile-id team-id] - (let [profile (-> (db/get conn :profile {:id profile-id}) - (profile/decode-row)) - notify (or (-> profile :props :notifications :dashboard-comments) :all)] - - (case notify - :all - (->> (db/exec! conn [sql:unread-all-comment-threads-by-team profile-id team-id]) - (into [] xf-decode-row)) - - :partial - (->> (db/exec! conn [sql:unread-partial-comment-threads-by-team profile-id team-id profile-id profile-id]) - (into [] xf-decode-row)) - - []))) + (teams/check-read-permissions! cfg profile-id team-id) + (get-unread-comment-threads cfg profile-id team-id)) ;; --- COMMAND: Get Single Comment Thread From b5648e1241636e0f7afc8e06915d9f36b17f1e04 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Oct 2025 14:28:37 +0200 Subject: [PATCH 2/5] :arrow_up: Update yarn to 2.10.3 on frontend module --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 91c14de3cd..37612cd0e7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.9.2+sha512.1fc009bc09d13cfd0e19efa44cbfc2b9cf6ca61482725eb35bbc5e257e093ebf4130db6dfe15d604ff4b79efd8e1e8e99b25fa7d0a6197c9f9826358d4d65c3c", + "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f", "browserslist": [ "defaults" ], From fc35dc77ce57a215fbec12057859efbe0bcf5cea Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Oct 2025 14:31:20 +0200 Subject: [PATCH 3/5] :bug: Enable migrations on calculate file library summary --- backend/src/app/rpc/commands/files.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 2229b63f4a..22b9b08a1a 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -588,7 +588,7 @@ calculate-from-db (fn [] - (let [file (bfc/get-file cfg id :migrate? false) + (let [file (bfc/get-file cfg id) result (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] (calculate-library-summary file))] (-> file From 160873c63ececda147aeef905715bdfbcbfbbdff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 6 Oct 2025 12:41:52 +0200 Subject: [PATCH 4/5] :bug: Remove duplicated token set ids --- common/src/app/common/files/migrations.cljc | 8 +- common/src/app/common/types/tokens_lib.cljc | 199 +++++++++++++++----- 2 files changed, 156 insertions(+), 51 deletions(-) diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index 1531642dc9..c72a76c706 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -33,6 +33,7 @@ [app.common.types.shape.shadow :as ctss] [app.common.types.shape.text :as ctst] [app.common.types.text :as types.text] + [app.common.types.tokens-lib :as types.tokens-lib] [app.common.uuid :as uuid] [clojure.set :as set] [cuerdas.core :as str])) @@ -1612,6 +1613,10 @@ (update component :path #(d/nilv % "")))] (d/update-when data :components d/update-vals update-component))) +(defmethod migrate-data "0014-fix-tokens-lib-duplicate-ids" + [data _] + (update data :tokens-lib types.tokens-lib/fix-duplicate-token-set-ids)) + (def available-migrations (into (d/ordered-set) ["legacy-2" @@ -1681,4 +1686,5 @@ "0010-fix-swap-slots-pointing-non-existent-shapes" "0011-fix-invalid-text-touched-flags" "0012-fix-position-data" - "0013-fix-component-path"])) + "0013-fix-component-path" + "0014-fix-tokens-lib-duplicate-ids"])) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 11ea10d9dd..48a17afbd0 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -352,19 +352,23 @@ (def check-token-set (sm/check-fn schema:token-set :hint "expected valid token set")) +(defn map->token-set + [& {:as attrs}] + (TokenSet. (:id attrs) + (:name attrs) + (:description attrs) + (:modified-at attrs) + (:tokens attrs))) + (defn make-token-set [& {:as attrs}] - (let [attrs (-> attrs - (update :id #(or % (uuid/next))) - (update :modified-at #(or % (ct/now))) - (update :tokens #(into (d/ordered-map) %)) - (update :description d/nilv "") - (check-token-set-attrs))] - (TokenSet. (:id attrs) - (:name attrs) - (:description attrs) - (:modified-at attrs) - (:tokens attrs)))) + (-> attrs + (update :id #(or % (uuid/next))) + (update :modified-at #(or % (ct/now))) + (update :tokens #(into (d/ordered-map) %)) + (update :description d/nilv "") + (check-token-set-attrs) + (map->token-set))) (def ^:private set-prefix "S-") @@ -516,10 +520,21 @@ [:fn d/ordered-map?]]]}} [:ref ::node]]) +(defn- not-repeated-ids + [sets] + ;; TODO: this check will not be necessary after refactoring the internal structure of TokensLib + ;; since we'll use a map of sets indexed by id. Thus, it should be removed. + (let [ids (->> (tree-seq d/ordered-map? vals sets) + (filter (partial instance? TokenSet)) + (map get-id)) + ids' (set ids)] + (= (count ids) (count ids')))) + (def ^:private schema:token-sets [:and {:title "TokenSets"} [:map-of :string schema:token-set-node] - [:fn d/ordered-map?]]) + [:fn d/ordered-map?] + [:fn not-repeated-ids]]) (def ^:private check-token-sets (sm/check-fn schema:token-sets :hint "expected valid token sets")) @@ -1355,8 +1370,15 @@ Will return a value that matches this schema: data (d/oassoc data hidden-theme-name (make-hidden-theme)))))) +(defn map->tokens-lib + "Make a new instance of TokensLib from a map, but skiping all + validation; it is used for create new instances from trusted + sources" + [& {:keys [sets themes active-themes]}] + (TokensLib. sets themes active-themes)) + (defn make-tokens-lib - "Create an empty or prepopulated tokens library." + "Make a new instance of TokensLib from a map and validates the input" [& {:keys [sets themes active-themes]}] (let [sets (or sets (d/ordered-map)) themes (-> (or themes (d/ordered-map)) @@ -1824,7 +1846,12 @@ Will return a value that matches this schema: nil decoded-json))) -;; === Serialization handlers for RPC API (transit) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SERIALIZATION (TRANSIT) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Serialization used for communicate data in transit between backend +;; and the frontend (t/add-handlers! {:id "penpot/tokens-lib" @@ -1847,18 +1874,49 @@ Will return a value that matches this schema: :wfn datafy :rfn #(map->Token %)}) -;; === Serialization handlers for database (fressian) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; MIGRATIONS HELPERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn fix-duplicate-token-set-ids + "Given an instance of TokensLib fixes it internal sets data sturcture + for ensure each set has unique id; + + Specific function for file data migrations" + [tokens-lib] + (let [seen-ids + (volatile! #{}) + + migrate-set-node + (fn recurse [node] + (if (token-set? node) + (if (contains? @seen-ids (get-id node)) + (-> (datafy node) + (assoc :id (uuid/next)) + (map->token-set)) + (do + (vswap! seen-ids conj (get-id node)) + node)) + (d/update-vals node recurse)))] + + (-> (datafy tokens-lib) + (update :sets d/update-vals migrate-set-node) + (map->tokens-lib)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SERIALIZATION (FRESIAN) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Serialization used for the internal storage on the file data, it +;; uses and, space and cpu efficient fresian serialization. #?(:clj - (defn- read-tokens-lib-v1-1 - "Reads the tokens lib data structure and ensures that hidden - theme exists and adds missing ID on themes" - [r] - (let [sets (fres/read-object! r) - themes (fres/read-object! r) - active-themes (fres/read-object! r) + (defn- migrate-to-v1-2 + "Migrate the TokensLib data structure internals to v1.2 version; it + expects input from v1.1 version" + [{:keys [themes] :as params}] - ;; Ensure we have at least a hidden theme + (let [;; Ensure we have at least a hidden theme themes (ensure-hidden-theme themes) @@ -1877,22 +1935,18 @@ Will return a value that matches this schema: (keys themes))))) themes (keys themes))] - - (->TokensLib sets themes active-themes)))) + (assoc params :themes themes)))) #?(:clj - (defn- read-tokens-lib-v1-2 - "Reads the tokens lib data structure and add ids to tokens, sets and themes." - [r] - (let [sets (fres/read-object! r) - themes (fres/read-object! r) - active-themes (fres/read-object! r) - - migrate-token + (defn- migrate-to-v1-3 + "Migrate the TokensLib data structure internals to v1.3 version; it + expects input from v1.2 version" + [{:keys [sets themes] :as params}] + (let [migrate-token (fn [token] (assoc token :id (uuid/next))) - migrate-sets-node + migrate-set-node (fn recurse [node] (if (token-set-legacy? node) (make-token-set @@ -1902,7 +1956,7 @@ Will return a value that matches this schema: (d/update-vals node recurse))) sets - (d/update-vals sets migrate-sets-node) + (d/update-vals sets migrate-set-node) migrate-theme (fn [theme] @@ -1912,9 +1966,12 @@ Will return a value that matches this schema: (assoc theme :id uuid/zero :external-id "") - (assoc theme ;; Rename the :id field to :external-id, and add - :id (or (uuid/parse* (:id theme)) ;; a new :id that is the same as the old if if - (uuid/next)) ;; this is an uuid, else a new uuid is generated. + ;; Rename the :id field to :external-id, and add a + ;; new :id that is the same as the old if if this is an + ;; uuid, else a new uuid is generated. + (assoc theme + :id (or (uuid/parse* (:id theme)) + (uuid/next)) :external-id (:id theme))))) migrate-theme-group @@ -1924,7 +1981,54 @@ Will return a value that matches this schema: themes (d/update-vals themes migrate-theme-group)] - (->TokensLib sets themes active-themes)))) + (assoc params + :themes themes + :sets sets)))) + +#?(:clj + (defn- migrate-to-v1-4 + "Migrate the TokensLib data structure internals to v1.2 version; it + expects input from v1.3 version" + [params] + (let [migrate-set-node + (fn recurse [node] + (if (token-set-legacy? node) + (make-token-set node) + (d/update-vals node recurse)))] + + (update params :sets d/update-vals migrate-set-node)))) + +#?(:clj + (defn- read-tokens-lib-v1-1 + "Reads the tokens lib data structure and ensures that hidden + theme exists and adds missing ID on themes" + [r] + (let [sets (fres/read-object! r) + themes (fres/read-object! r) + active-themes (fres/read-object! r)] + + (-> {:sets sets + :themes themes + :active-themes active-themes} + (migrate-to-v1-2) + (migrate-to-v1-3) + (migrate-to-v1-4) + (map->tokens-lib))))) + +#?(:clj + (defn- read-tokens-lib-v1-2 + "Reads the tokens lib data structure and add ids to tokens, sets and themes." + [r] + (let [sets (fres/read-object! r) + themes (fres/read-object! r) + active-themes (fres/read-object! r)] + + (-> {:sets sets + :themes themes + :active-themes active-themes} + (migrate-to-v1-3) + (migrate-to-v1-4) + (map->tokens-lib))))) #?(:clj (defn- read-tokens-lib-v1-3 @@ -1933,18 +2037,13 @@ Will return a value that matches this schema: [r] (let [sets (fres/read-object! r) themes (fres/read-object! r) - active-themes (fres/read-object! r) + active-themes (fres/read-object! r)] - migrate-sets-node - (fn recurse [node] - (if (token-set-legacy? node) - (make-token-set node) - (d/update-vals node recurse))) - - sets - (d/update-vals sets migrate-sets-node)] - - (->TokensLib sets themes active-themes)))) + (-> {:sets sets + :themes themes + :active-themes active-themes} + (migrate-to-v1-4) + (map->tokens-lib))))) #?(:clj (defn- write-tokens-lib From 515b381f66bf251e8653f030851b393761d332e9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Oct 2025 15:09:34 +0200 Subject: [PATCH 5/5] :bug: Fix random failure of tokens lib test --- common/test/common_tests/types/tokens_lib_test.cljc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc index 8c837658ab..4b86480e12 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -855,7 +855,9 @@ (t/deftest transit-serialization (let [tokens-lib (-> (ctob/make-tokens-lib) - (ctob/add-set (ctob/make-token-set :name "test-token-set")) + (ctob/add-set (ctob/make-token-set + :id (thi/new-id! :test-token-set) + :name "test-token-set")) (ctob/add-token (thi/id :test-token-set) (ctob/make-token :name "test-token" :type :boolean