diff --git a/CHANGES.md b/CHANGES.md index 828d5af8b..7c7a1a6db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -70,6 +70,7 @@ is a number of cores) - Fix missing state refresh on notifications update [Taiga #10253](https://tree.taiga.io/project/penpot/issue/10253) - Fix icon visualization on select component [Taiga #8889](https://tree.taiga.io/project/penpot/issue/8889) - Fix typo on integration tests docs [Taiga #10112](https://tree.taiga.io/project/penpot/issue/10112) +- Fix menu shadow color [Taiga #10102](https://tree.taiga.io/project/penpot/issue/10102) - Fix problem with alt key measures being stuck [Taiga #9348](https://tree.taiga.io/project/penpot/issue/9348) - Fix error when reseting stroke cap - Fix problem with strokes not refreshing in Safari [Taiga #9040](https://tree.taiga.io/project/penpot/issue/9040) @@ -86,6 +87,7 @@ is a number of cores) - Fix rename locked boards [Taiga #10174](https://tree.taiga.io/project/penpot/issue/10174) - Fix update-libraries dialog disappear when clicking outside [Taiga #10238](https://tree.taiga.io/project/penpot/issue/10238) - Fix incorrect handling of team access requests with deleted/recreated users +- Fix incorect handling of profile settings related to invitation notifications [Taiga #10252](https://tree.taiga.io/project/penpot/issue/10252) ## 2.4.3 diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index 2ae8d2171..95fae4dfe 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -20,7 +20,6 @@ [app.config :as cf] [app.db :as db] [app.db.sql :as sql] - [app.features.components-v2 :as feat.compv2] [app.features.fdata :as feat.fdata] [app.features.file-migrations :as feat.fmigr] [app.loggers.audit :as-alias audit] @@ -307,7 +306,7 @@ update-shapes (fn [data {:keys [page-id shape-id]}] - (d/update-in-when data [:pages-index page-id :objects shape-id] cfh/relink-media-refs lookup-index)) + (d/update-in-when data [:pages-index page-id :objects shape-id] cfh/relink-refs lookup-index)) file (update file :data #(reduce update-shapes % media-refs))] @@ -375,7 +374,7 @@ replace the old :component-file reference with the new ones, using the provided file-index." [data] - (cfh/relink-media-refs data lookup-index)) + (cfh/relink-refs data lookup-index)) (defn- relink-media "A function responsible of process the :media attr of file data and @@ -523,38 +522,3 @@ (l/error :hint "file schema validation error" :cause result)))) (insert-file! cfg file opts))) - -(defn register-pending-migrations! - "All features that are enabled and requires explicit migration are - added to the state for a posterior migration step." - [cfg {:keys [id features] :as file}] - (doseq [feature (-> (::features cfg) - (set/difference cfeat/no-migration-features) - (set/difference cfeat/backend-only-features) - (set/difference features))] - (vswap! *state* update :pending-to-migrate (fnil conj []) [feature id])) - - file) - -(defn apply-pending-migrations! - "Apply alredy registered pending migrations to files" - [cfg] - (doseq [[feature file-id] (-> *state* deref :pending-to-migrate)] - (case feature - "components/v2" - (feat.compv2/migrate-file! cfg file-id - :validate? (::validate cfg true) - :skip-on-graphic-error? true) - - "fdata/shape-data-type" - nil - - ;; There is no migration needed, but we don't want to allow - ;; copy paste nor import of variant files into no-variant teams - "variants/v1" - nil - - (ex/raise :type :internal - :code :no-migration-defined - :hint (str/ffmt "no migation for feature '%' on file importation" feature) - :feature feature)))) diff --git a/backend/src/app/binfile/migrations.clj b/backend/src/app/binfile/migrations.clj new file mode 100644 index 000000000..62d180b49 --- /dev/null +++ b/backend/src/app/binfile/migrations.clj @@ -0,0 +1,50 @@ +;; 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 + +(ns app.binfile.migrations + "A binfile related migrations handling" + (:require + [app.binfile.common :as bfc] + [app.common.exceptions :as ex] + [app.common.features :as cfeat] + [app.features.components-v2 :as feat.compv2] + [clojure.set :as set] + [cuerdas.core :as str])) + +(defn register-pending-migrations! + "All features that are enabled and requires explicit migration are + added to the state for a posterior migration step." + [cfg {:keys [id features] :as file}] + (doseq [feature (-> (::features cfg) + (set/difference cfeat/no-migration-features) + (set/difference cfeat/backend-only-features) + (set/difference features))] + (vswap! bfc/*state* update :pending-to-migrate (fnil conj []) [feature id])) + + file) + +(defn apply-pending-migrations! + "Apply alredy registered pending migrations to files" + [cfg] + (doseq [[feature file-id] (-> bfc/*state* deref :pending-to-migrate)] + (case feature + "components/v2" + (feat.compv2/migrate-file! cfg file-id + :validate? (::validate cfg true) + :skip-on-graphic-error? true) + + "fdata/shape-data-type" + nil + + ;; There is no migration needed, but we don't want to allow + ;; copy paste nor import of variant files into no-variant teams + "variants/v1" + nil + + (ex/raise :type :internal + :code :no-migration-defined + :hint (str/ffmt "no migation for feature '%' on file importation" feature) + :feature feature)))) diff --git a/backend/src/app/binfile/v1.clj b/backend/src/app/binfile/v1.clj index 0f94df969..2f70ed50d 100644 --- a/backend/src/app/binfile/v1.clj +++ b/backend/src/app/binfile/v1.clj @@ -9,6 +9,7 @@ (:refer-clojure :exclude [assert]) (:require [app.binfile.common :as bfc] + [app.binfile.migrations :as bfm] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] @@ -473,7 +474,7 @@ (read-section options)))) [:v1/metadata :v1/files :v1/rels :v1/sobjects]) - (bfc/apply-pending-migrations! cfg) + (bfm/apply-pending-migrations! cfg) ;; Knowing that the ids of the created files are in index, ;; just lookup them and return it as a set diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index dcda35455..dc6bf7b80 100644 --- a/backend/src/app/binfile/v3.clj +++ b/backend/src/app/binfile/v3.clj @@ -9,6 +9,7 @@ (:refer-clojure :exclude [read]) (:require [app.binfile.common :as bfc] + [app.binfile.migrations :as bfm] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] @@ -735,7 +736,7 @@ (bfc/process-file))] - (bfc/register-pending-migrations! cfg file) + (bfm/register-pending-migrations! cfg file) (bfc/save-file! cfg file ::db/return-keys false) file-id'))) @@ -915,7 +916,7 @@ (import-file-media cfg) (import-file-thumbnails cfg) - (bfc/apply-pending-migrations! cfg) + (bfm/apply-pending-migrations! cfg) ids))))))) diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index f30075f94..795a9bea5 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -435,7 +435,10 @@ :fn (mg/resource "app/migrations/sql/0137-add-file-migration-table.sql")} {:name "0138-mod-file-data-fragment-table.sql" - :fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")}]) + :fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")} + + {:name "0139-mod-file-change-table.sql" + :fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}]) (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0139-mod-file-change-table.sql b/backend/src/app/migrations/sql/0139-mod-file-change-table.sql new file mode 100644 index 000000000..0b8171325 --- /dev/null +++ b/backend/src/app/migrations/sql/0139-mod-file-change-table.sql @@ -0,0 +1,5 @@ +ALTER TABLE file_change + DROP CONSTRAINT file_change_file_id_fkey, + DROP CONSTRAINT file_change_profile_id_fkey, + ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE, + ADD FOREIGN KEY (profile_id) REFERENCES profile(id) ON DELETE SET NULL DEFERRABLE; diff --git a/backend/src/app/rpc/commands/files_snapshot.clj b/backend/src/app/rpc/commands/files_snapshot.clj index 43e3f1c95..71689560a 100644 --- a/backend/src/app/rpc/commands/files_snapshot.clj +++ b/backend/src/app/rpc/commands/files_snapshot.clj @@ -6,6 +6,7 @@ (ns app.rpc.commands.files-snapshot (:require + [app.binfile.common :as bfc] [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.schema :as sm] @@ -22,7 +23,6 @@ [app.rpc.quotes :as quotes] [app.storage :as sto] [app.util.blob :as blob] - [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] [cuerdas.core :as str])) @@ -58,26 +58,6 @@ (files/check-read-permissions! conn profile-id file-id) (get-file-snapshots conn file-id)))) -(def ^:private sql:get-file - "SELECT f.*, - p.id AS project_id, - p.team_id AS team_id - FROM file AS f - INNER JOIN project AS p ON (p.id = f.project_id) - WHERE f.id = ?") - -(defn- get-file - [cfg file-id] - (let [file (->> (db/exec-one! cfg [sql:get-file file-id]) - (feat.fdata/resolve-file-data cfg))] - (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] - (-> file - (update :data blob/decode) - (update :data feat.fdata/process-pointers deref) - (update :data feat.fdata/process-objects (partial into {})) - (update :data assoc ::id file-id) - (update :data blob/encode))))) - (defn- generate-snapshot-label [] (let [ts (-> (dt/now) @@ -87,49 +67,53 @@ (str "snapshot-" ts))) (defn create-file-snapshot! - [cfg profile-id file-id label] - (let [file (get-file cfg file-id) + [cfg file & {:keys [label created-by deleted-at profile-id] + :or {deleted-at :default + created-by :system}}] + + (assert (#{:system :user :admin} created-by) + "expected valid keyword for created-by") + + (let [conn + (db/get-connection cfg) - ;; NOTE: final user never can provide label as `:system` - ;; keyword because the validator implies label always as - ;; string; keyword is used for signal a special case created-by - (if (= label :system) - "system" - "user") + (name created-by) deleted-at - (if (= label :system) + (cond + (= deleted-at :default) (dt/plus (dt/now) (cf/get-deletion-delay)) + + (dt/instant? deleted-at) + deleted-at + + :else nil) label - (if (= label :system) - (str "internal/snapshot/" (:revn file)) - (or label (generate-snapshot-label))) + (or label (generate-snapshot-label)) snapshot-id - (uuid/next)] + (uuid/next) - (-> cfg - (assoc ::quotes/profile-id profile-id) - (assoc ::quotes/project-id (:project-id file)) - (assoc ::quotes/team-id (:team-id file)) - (assoc ::quotes/file-id (:id file)) - (quotes/check! {::quotes/id ::quotes/snapshots-per-file} - {::quotes/id ::quotes/snapshots-per-team})) + data + (blob/encode (:data file)) + + features + (db/encode-pgarray (:features file) conn "text")] (l/debug :hint "creating file snapshot" - :file-id (str file-id) + :file-id (str (:id file)) :id (str snapshot-id) :label label) (db/insert! cfg :file-change {:id snapshot-id :revn (:revn file) - :data (:data file) + :data data :version (:version file) - :features (:features file) + :features features :profile-id profile-id :file-id (:id file) :label label @@ -146,12 +130,25 @@ (sv/defmethod ::create-file-snapshot {::doc/added "1.20" - ::sm/params schema:create-file-snapshot} - [cfg {:keys [::rpc/profile-id file-id label]}] - (db/tx-run! cfg - (fn [{:keys [::db/conn] :as cfg}] - (files/check-edition-permissions! conn profile-id file-id) - (create-file-snapshot! cfg profile-id file-id label)))) + ::sm/params schema:create-file-snapshot + ::db/transaction true} + [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id label]}] + (files/check-edition-permissions! conn profile-id file-id) + (let [file (bfc/get-file cfg file-id) + project (db/get-by-id cfg :project (:project-id file))] + + (-> cfg + (assoc ::quotes/profile-id profile-id) + (assoc ::quotes/project-id (:project-id file)) + (assoc ::quotes/team-id (:team-id project)) + (assoc ::quotes/file-id (:id file)) + (quotes/check! {::quotes/id ::quotes/snapshots-per-file} + {::quotes/id ::quotes/snapshots-per-team})) + + (create-file-snapshot! cfg file + {:label label + :profile-id profile-id + :created-by :user}))) (defn restore-file-snapshot! [{:keys [::db/conn ::mbus/msgbus] :as cfg} file-id snapshot-id] @@ -237,8 +234,11 @@ (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (files/check-edition-permissions! conn profile-id file-id) - (create-file-snapshot! cfg profile-id file-id :system) - (restore-file-snapshot! cfg file-id id)))) + (let [file (bfc/get-file cfg file-id)] + (create-file-snapshot! cfg file + {:profile-id profile-id + :created-by :system}) + (restore-file-snapshot! cfg file-id id))))) (def ^:private schema:update-file-snapshot [:map {:title "update-file-snapshot"} diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index c376b6fae..0fd0d9127 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -34,7 +34,6 @@ ;; --- Mutation: Create Team Invitation - (def sql:upsert-team-invitation "insert into team_invitation(id, team_id, email_to, created_by, role, valid_until) values (?, ?, ?, ?, ?, ?) @@ -79,27 +78,23 @@ [:role ::types.team/role] [:email ::sm/email]]) -(def ^:private check-create-invitation-params! +(def ^:private check-create-invitation-params (sm/check-fn schema:create-invitation)) +(defn- allow-invitation-emails? + [member] + (let [notifications (dm/get-in member [:props :notifications])] + (not= :none (:email-invites notifications)))) + (defn- create-invitation [{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}] - (dm/assert! - "expected valid connection on cfg parameter" - (db/connection? conn)) - - (dm/assert! - "expected valid params for `create-invitation` fn" - (check-create-invitation-params! params)) + (assert (db/connection? conn) "expected valid connection on cfg parameter") + (assert (check-create-invitation-params params)) (let [email (profile/clean-email email) member (profile/get-profile-by-email conn email)] - (teams/check-profile-muted conn member) - (teams/check-email-bounce conn email true) - (teams/check-email-spam conn email true) - ;; When we have email verification disabled and invitation user is ;; already present in the database, we proceed to add it to the ;; team as-is, without email roundtrip. @@ -125,48 +120,54 @@ nil) - (let [id (uuid/next) - expire (dt/in-future "168h") ;; 7 days - invitation (db/exec-one! conn [sql:upsert-team-invitation id - (:id team) (str/lower email) - (:id profile) - (name role) expire - (name role) expire]) - updated? (not= id (:id invitation)) - profile-id (:id profile) - tprops {:profile-id profile-id - :invitation-id (:id invitation) - :valid-until expire - :team-id (:id team) - :member-email (:email-to invitation) - :member-id (:id member) - :role role} - itoken (create-invitation-token cfg tprops) - ptoken (create-profile-identity-token cfg profile-id)] + (do + (some->> member (teams/check-profile-muted conn)) + (teams/check-email-bounce conn email true) + (teams/check-email-spam conn email true) - (when (contains? cf/flags :log-invitation-tokens) - (l/info :hint "invitation token" :token itoken)) + (let [id (uuid/next) + expire (dt/in-future "168h") ;; 7 days + invitation (db/exec-one! conn [sql:upsert-team-invitation id + (:id team) (str/lower email) + (:id profile) + (name role) expire + (name role) expire]) + updated? (not= id (:id invitation)) + profile-id (:id profile) + tprops {:profile-id profile-id + :invitation-id (:id invitation) + :valid-until expire + :team-id (:id team) + :member-email (:email-to invitation) + :member-id (:id member) + :role role} + itoken (create-invitation-token cfg tprops) + ptoken (create-profile-identity-token cfg profile-id)] - (let [props (-> (dissoc tprops :profile-id) - (audit/clean-props)) - evname (if updated? - "update-team-invitation" - "create-team-invitation") - event (-> (audit/event-from-rpc-params params) - (assoc ::audit/name evname) - (assoc ::audit/props props))] - (audit/submit! cfg event)) + (when (contains? cf/flags :log-invitation-tokens) + (l/info :hint "invitation token" :token itoken)) - (eml/send! {::eml/conn conn - ::eml/factory eml/invite-to-team - :public-uri (cf/get :public-uri) - :to email - :invited-by (:fullname profile) - :team (:name team) - :token itoken - :extra-data ptoken}) + (let [props (-> (dissoc tprops :profile-id) + (audit/clean-props)) + evname (if updated? + "update-team-invitation" + "create-team-invitation") + event (-> (audit/event-from-rpc-params params) + (assoc ::audit/name evname) + (assoc ::audit/props props))] + (audit/submit! cfg event)) - itoken)))) + (when (allow-invitation-emails? member) + (eml/send! {::eml/conn conn + ::eml/factory eml/invite-to-team + :public-uri (cf/get :public-uri) + :to email + :invited-by (:fullname profile) + :team (:name team) + :token itoken + :extra-data ptoken})) + + itoken))))) (defn- add-member-to-team [conn profile team role member] diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index 1eeb13818..7e471e4fc 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -16,7 +16,8 @@ [app.rpc.commands.teams :as teams] [app.rpc.cond :as-alias cond] [app.rpc.doc :as-alias doc] - [app.util.services :as sv])) + [app.util.services :as sv] + [cuerdas.core :as str])) ;; --- QUERY: View Only Bundle @@ -26,6 +27,27 @@ (update :pages (fn [pages] (filterv #(contains? allowed %) pages))) (update :pages-index select-keys allowed))) +(defn obfuscate-email + [email] + (let [[name domain] + (str/split email "@" 2) + + [_ rest] + (str/split domain "." 2) + + name + (if (> (count name) 3) + (str (subs name 0 1) (apply str (take (dec (count name)) (repeat "*")))) + "****")] + + (str name "@****." rest))) + +(defn anonymize-member + [member] + (-> (select-keys member [:id :email :name :fullname :photo-id]) + (update :email obfuscate-email) + (assoc :can-read true))) + (defn- get-view-only-bundle [{:keys [::db/conn] :as cfg} {:keys [profile-id file-id ::perms] :as params}] (let [file (files/get-file cfg file-id) @@ -37,7 +59,10 @@ team (-> (db/get conn :team {:id (:team-id project)}) (teams/decode-row)) - members (teams/get-team-members conn (:team-id project)) + members (cond->> (teams/get-team-members conn (:team-id project)) + (= :share-link (:type perms)) + (mapv anonymize-member)) + member-ids (into #{} (map :id) members) perms (assoc perms :in-team (contains? member-ids profile-id)) diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index 2ea26e3bb..d8293253e 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -15,7 +15,8 @@ [app.features.components-v2 :as feat.comp-v2] [app.main :as main] [app.rpc.commands.files :as files] - [app.rpc.commands.files-snapshot :as fsnap])) + [app.rpc.commands.files-snapshot :as fsnap] + [app.util.time :as dt])) (def ^:dynamic *system* nil) @@ -96,8 +97,11 @@ (let [conn (db/get-connection system)] (->> (feat.comp-v2/get-and-lock-team-files conn team-id) (reduce (fn [result file-id] - (fsnap/create-file-snapshot! system nil file-id label) - (inc result)) + (let [file (fsnap/get-file-snapshots system file-id)] + (fsnap/create-file-snapshot! system file + {:label label + :created-by :admin}) + (inc result))) 0)))) (defn restore-team-snapshot! @@ -143,7 +147,10 @@ (cfv/validate-file-schema! file')) (when (string? label) - (fsnap/create-file-snapshot! system nil file-id label)) + (fsnap/create-file-snapshot! system file + {:label label + :deleted-at (dt/in-future {:days 30}) + :created-by :admin})) (let [file' (update file' :revn inc)] (bfc/update-file! system file') diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj index b9939c8be..29370e61e 100644 --- a/backend/src/app/tasks/delete_object.clj +++ b/backend/src/app/tasks/delete_object.clj @@ -40,6 +40,11 @@ :file-id id :cause cause)))) + ;; Mark file change to be deleted + (db/update! conn :file-change + {:deleted-at deleted-at} + {:file-id id}) + ;; Mark file media objects to be deleted (db/update! conn :file-media-object {:deleted-at deleted-at} diff --git a/backend/src/app/tasks/objects_gc.clj b/backend/src/app/tasks/objects_gc.clj index e08bdce44..843feb1c0 100644 --- a/backend/src/app/tasks/objects_gc.clj +++ b/backend/src/app/tasks/objects_gc.clj @@ -26,7 +26,7 @@ (defn- delete-profiles! [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-profiles min-age chunk-size] {:chunk-size 5}) + (->> (db/plan conn [sql:get-profiles min-age chunk-size] {:fetch-size 5}) (reduce (fn [total {:keys [id photo-id]}] (l/trc :hint "permanently delete" :rel "profile" :id (str id)) @@ -49,7 +49,7 @@ (defn- delete-teams! [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-teams min-age chunk-size] {:chunk-size 5}) + (->> (db/plan conn [sql:get-teams min-age chunk-size] {:fetch-size 5}) (reduce (fn [total {:keys [id photo-id deleted-at]}] (l/trc :hint "permanently delete" :rel "team" @@ -77,7 +77,7 @@ (defn- delete-fonts! [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-fonts min-age chunk-size] {:chunk-size 5}) + (->> (db/plan conn [sql:get-fonts min-age chunk-size] {:fetch-size 5}) (reduce (fn [total {:keys [id team-id deleted-at] :as font}] (l/trc :hint "permanently delete" :rel "team-font-variant" @@ -109,7 +109,7 @@ (defn- delete-projects! [{:keys [::db/conn ::min-age ::chunk-size] :as cfg}] - (->> (db/cursor conn [sql:get-projects min-age chunk-size] {:chunk-size 5}) + (->> (db/plan conn [sql:get-projects min-age chunk-size] {:fetch-size 5}) (reduce (fn [total {:keys [id team-id deleted-at]}] (l/trc :hint "permanently delete" :rel "project" @@ -135,7 +135,7 @@ (defn- delete-files! [{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}] - (->> (db/cursor conn [sql:get-files min-age chunk-size] {:chunk-size 5}) + (->> (db/plan conn [sql:get-files min-age chunk-size] {:fetch-size 5}) (reduce (fn [total {:keys [id deleted-at project-id] :as file}] (l/trc :hint "permanently delete" :rel "file" @@ -164,7 +164,7 @@ (defn delete-file-thumbnails! [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-file-thumbnails min-age chunk-size] {:chunk-size 5}) + (->> (db/plan conn [sql:get-file-thumbnails min-age chunk-size] {:fetch-size 5}) (reduce (fn [total {:keys [file-id revn media-id deleted-at]}] (l/trc :hint "permanently delete" :rel "file-thumbnail" @@ -193,7 +193,7 @@ (defn delete-file-object-thumbnails! [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-file-object-thumbnails min-age chunk-size] {:chunk-size 5}) + (->> (db/plan conn [sql:get-file-object-thumbnails min-age chunk-size] {:fetch-size 5}) (reduce (fn [total {:keys [file-id object-id media-id deleted-at]}] (l/trc :hint "permanently delete" :rel "file-tagged-object-thumbnail" @@ -222,7 +222,7 @@ (defn- delete-file-data-fragments! [{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}] - (->> (db/cursor conn [sql:get-file-data-fragments min-age chunk-size] {:chunk-size 5}) + (->> (db/plan conn [sql:get-file-data-fragments min-age chunk-size] {:fetch-size 5}) (reduce (fn [total {:keys [file-id id deleted-at data-ref-id]}] (l/trc :hint "permanently delete" :rel "file-data-fragment" @@ -248,7 +248,7 @@ (defn- delete-file-media-objects! [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-file-media-objects min-age chunk-size] {:chunk-size 5}) + (->> (db/plan conn [sql:get-file-media-objects min-age chunk-size] {:fetch-size 5}) (reduce (fn [total {:keys [id file-id deleted-at] :as fmo}] (l/trc :hint "permanently delete" :rel "file-media-object" @@ -275,9 +275,9 @@ FOR UPDATE SKIP LOCKED") -(defn- delete-file-change! +(defn- delete-file-changes! [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-file-change min-age chunk-size] {:chunk-size 5}) + (->> (db/plan conn [sql:get-file-change min-age chunk-size] {:fetch-size 5}) (reduce (fn [total {:keys [id file-id deleted-at] :as xlog}] (l/trc :hint "permanently delete" :rel "file-change" @@ -299,11 +299,11 @@ #'delete-file-data-fragments! #'delete-file-object-thumbnails! #'delete-file-thumbnails! + #'delete-file-changes! #'delete-files! #'delete-projects! #'delete-fonts! - #'delete-teams! - #'delete-file-change!]) + #'delete-teams!]) (defn- execute-proc! "A generic function that executes the specified proc iterativelly @@ -326,7 +326,7 @@ [k v] {k (assoc v ::min-age (cf/get-deletion-delay) - ::chunk-size 50)}) + ::chunk-size 100)}) (defmethod ig/init-key ::handler [_ cfg] diff --git a/backend/test/backend_tests/rpc_file_snapshot_test.clj b/backend/test/backend_tests/rpc_file_snapshot_test.clj index 90e7366ee..1da63c4f3 100644 --- a/backend/test/backend_tests/rpc_file_snapshot_test.clj +++ b/backend/test/backend_tests/rpc_file_snapshot_test.clj @@ -97,6 +97,7 @@ (th/db-query :file-change {:file-id (:id file)} {:order-by [:created-at]})] + (t/is (= 2 (count rows))) (t/is (= "user" (:created-by row1))) (t/is (= "system" (:created-by row2))))) diff --git a/common/src/app/common/files/helpers.cljc b/common/src/app/common/files/helpers.cljc index f167601b6..e73aed5ea 100644 --- a/common/src/app/common/files/helpers.cljc +++ b/common/src/app/common/files/helpers.cljc @@ -589,10 +589,9 @@ (into xform:collect-media-refs (vals (:components data))) (into (keys (:media data))))) -(defn relink-media-refs - "A function responsible to analyze all file data and replace the - old :component-file reference with the new ones, using the provided - file-index." +(defn relink-refs + "A function responsible to analyze the file data or shape for references + and apply lookup-index on it." [data lookup-index] (letfn [(process-map-form [form] (cond-> form diff --git a/docs/img/dev-tools-1.png b/docs/img/dev-tools-1.png new file mode 100644 index 000000000..11496cf77 Binary files /dev/null and b/docs/img/dev-tools-1.png differ diff --git a/docs/img/dev-tools-2.png b/docs/img/dev-tools-2.png new file mode 100644 index 000000000..125c0a7d7 Binary files /dev/null and b/docs/img/dev-tools-2.png differ diff --git a/docs/img/penpot-report.png b/docs/img/penpot-report.png new file mode 100644 index 000000000..3240691e4 Binary files /dev/null and b/docs/img/penpot-report.png differ diff --git a/docs/technical-guide/getting-started.md b/docs/technical-guide/getting-started.md index f0cff3c80..70248a62e 100644 --- a/docs/technical-guide/getting-started.md +++ b/docs/technical-guide/getting-started.md @@ -280,6 +280,65 @@ Postgres database and another one for the assets uploaded by your users (images clips). There may be more volumes if you enable other features, as explained in the file itself. +### Troubleshooting + +Knowing how to do Penpot troubleshooting can be very useful; on the one hand, it helps to create issues easier to resolve, since they include relevant information from the beginning which also makes them get solved faster; on the other hand, many times troubleshooting gives the necessary information to resolve a problem autonomously, without even creating an issue. + +Troubleshooting requires patience and practice; you have to read the stacktrace carefully, even if it looks like a mess at first. It takes some practice to learn how to read the traces properly and extract important information. + +If your Penpot installation is not working as intended, there are several places to look up searching for hints: + +**Docker logs** + +Check if all containers are up and running: +```bash +docker compose -p penpot -f docker-compose.yaml ps +``` + +Check logs of all Penpot: +```bash +docker compose -p penpot -f docker-compose.yaml logs -f +``` + +If there is too much information and you'd like to check just one service at a time: +```bash +docker compose -p penpot -f docker-compose.yaml logs penpot-frontend -f +``` + +You can always check the logs form a specific container: +```bash +docker logs -f penpot-penpot-postgres-1 +``` + +**Browser logs** + +The browser provides as well useful information to corner the issue. + +First, use the devtools to ensure which version and flags you're using. Go to your Penpot instance in the browser and press F12; you'll see the devtools. In the Console, you can see the exact version that's being used. + +
+ + Devtools > Console + +
+ +Other interesting tab in the devtools is the Network tab, to check if there is a request that throws errors. + +
+ + Devtools > Network + +
+ +**Penpot Report** + +When Penpot crashes, it provides a report with very useful information. Don't miss it! + +
+ + Penpot report + +
## Install with Kubernetes diff --git a/frontend/playwright/ui/specs/dashboard.spec.js b/frontend/playwright/ui/specs/dashboard.spec.js index 58f2f07d0..768106634 100644 --- a/frontend/playwright/ui/specs/dashboard.spec.js +++ b/frontend/playwright/ui/specs/dashboard.spec.js @@ -105,10 +105,10 @@ test("Multiple elements in context", async ({ page }) => { await button.click({ button: "right" }); - await expect(button.getByTestId("duplicate-multi")).toBeVisible(); - await expect(button.getByTestId("file-move-multi")).toBeVisible(); - await expect(button.getByTestId("file-binary-export-multi")).toBeVisible(); - await expect(button.getByTestId("file-delete-multi")).toBeVisible(); + await expect(page.getByTestId("duplicate-multi")).toBeVisible(); + await expect(page.getByTestId("file-move-multi")).toBeVisible(); + await expect(page.getByTestId("file-binary-export-multi")).toBeVisible(); + await expect(page.getByTestId("file-delete-multi")).toBeVisible(); }); test("User has create file button", async ({ page }) => { diff --git a/frontend/src/app/main/ui/components/portal.cljs b/frontend/src/app/main/ui/components/portal.cljs new file mode 100644 index 000000000..5fadf20e7 --- /dev/null +++ b/frontend/src/app/main/ui/components/portal.cljs @@ -0,0 +1,15 @@ +;; 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 + +(ns app.main.ui.components.portal + (:require + [rumext.v2 :as mf])) + +(mf/defc portal-on-document* + [{:keys [children]}] + (mf/portal + (mf/html [:* children]) + (.-body js/document))) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 7649a264d..d0d7bafde 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -59,9 +59,6 @@ permissions (:permissions team) - dashboard-local (mf/deref refs/dashboard-local) - file-menu-open? (:menu-open dashboard-local) - default-project-id (get default-project :id) @@ -87,7 +84,6 @@ (mf/use-effect on-resize) [:div {:class (stl/css :dashboard-content) - :style {:pointer-events (when file-menu-open? "none")} :on-click clear-selected-fn :ref container} (case section diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index f3ef69ca4..f882c2179 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -55,8 +55,8 @@ projects)) (mf/defc file-menu* - {::mf/props :obj} [{:keys [files on-edit on-menu-close top left navigate origin parent-id can-edit]}] + (assert (seq files) "missing `files` prop") (assert (fn? on-edit) "missing `on-edit` prop") (assert (fn? on-menu-close) "missing `on-menu-close` prop") diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 251a7bbd2..08d2230c6 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -13,7 +13,7 @@ [app.main.data.project :as dpj] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.dashboard.grid :refer [grid]] + [app.main.ui.dashboard.grid :refer [grid*]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.pin-button :refer [pin-button*]] [app.main.ui.dashboard.project-menu :refer [project-menu*]] @@ -198,11 +198,11 @@ :subtitle (if is-draft-proyect (tr "dashboard.empty-placeholder-drafts-subtitle") (tr "dashboard.empty-placeholder-files-subtitle"))}] - [:& grid {:project project - :files files - :selected-files selected-files - :can-edit can-edit? - :origin :files - :create-fn create-file - :limit limit}])]])) + [:> grid* {:project project + :files files + :selected-files selected-files + :can-edit can-edit? + :origin :files + :create-fn create-file + :limit limit}])]])) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index bc5963634..4a3112ca2 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -25,6 +25,7 @@ [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.components.color-bullet :as bc] + [app.main.ui.components.portal :refer [portal-on-document*]] [app.main.ui.dashboard.file-menu :refer [file-menu*]] [app.main.ui.dashboard.import :refer [use-import-file]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] @@ -241,29 +242,37 @@ counter-el)) (mf/defc grid-item* - {::mf/props :obj} [{:keys [file origin can-edit selected-files]}] - (let [file-id (:id file) + (let [file-id (get file :id) + state (mf/deref refs/dashboard-local) - is-library-view (= origin :libraries) + menu-pos + (get state :menu-pos) - dashboard-local (mf/deref refs/dashboard-local) - file-menu-open? (:menu-open dashboard-local) + menu-open? + (and (get state :menu-open) + (= file-id (:file-id state))) - selected? (contains? selected-files file-id) + selected? + (contains? selected-files file-id) - node-ref (mf/use-ref) - menu-ref (mf/use-ref) + selected-num + (count selected-files) + + node-ref (mf/use-ref) + menu-ref (mf/use-ref) + + is-library-view? + (= origin :libraries) on-menu-close - (mf/use-fn - (fn [_] - (st/emit! (dd/hide-file-menu)))) + (mf/use-fn #(st/emit! (dd/hide-file-menu))) on-select (mf/use-fn + (mf/deps selected? selected-num) (fn [event] - (when (or (not selected?) (> (count selected-files) 1)) + (when (or (not selected?) (> selected-num 1)) (dom/stop-propagation event) (let [shift? (kbd/shift? event)] (when-not shift? @@ -281,41 +290,36 @@ on-drag-start (mf/use-fn - (mf/deps selected-files can-edit) + (mf/deps selected? selected-num) (fn [event] (st/emit! (dd/hide-file-menu)) (when can-edit - (let [offset (dom/get-offset-position (dom/event->native-event event)) - - select-current? (not (contains? selected-files (:id file))) - - item-el (mf/ref-val node-ref) - counter-el (create-counter-element - item-el - (if select-current? - 1 - (count selected-files)))] - (when select-current? + (let [offset (dom/get-offset-position (dom/event->native-event event)) + item-el (mf/ref-val node-ref) + counter-el (create-counter-element item-el + (if (not selected?) + 1 + selected-num))] + (when (not selected?) (st/emit! (dd/clear-selected-files)) (st/emit! (dd/toggle-file-select file))) (dnd/set-data! event "penpot/files" "dummy") (dnd/set-allowed-effect! event "move") - ;; set-drag-image requires that the element is rendered and - ;; visible to the user at the moment of creating the ghost - ;; image (to make a snapshot), but you may remove it right - ;; afterwards, in the next render cycle. + ;; set-drag-image requires that the element is rendered + ;; and visible to the user at the moment of creating the + ;; ghost image (to make a snapshot), but you may remove + ;; it right afterwards, in the next render cycle. (dom/append-child! item-el counter-el) (dnd/set-drag-image! event item-el (:x offset) (:y offset)) - (ts/raf #(.removeChild ^js item-el counter-el)))))) + (ts/raf #(dom/remove-child! item-el counter-el)))))) on-menu-click (mf/use-fn (mf/deps file selected?) (fn [event] (dom/stop-propagation event) - (dom/prevent-default event) (when-not selected? (when-not (kbd/shift? event) @@ -339,12 +343,10 @@ on-context-menu (mf/use-fn - (mf/deps on-menu-click is-library-view) + (mf/deps on-menu-click) (fn [event] - (dom/stop-propagation event) (dom/prevent-default event) - (when-not is-library-view - (on-menu-click event)))) + (on-menu-click event))) edit (mf/use-fn @@ -362,8 +364,8 @@ (dom/stop-propagation event) (st/emit! (dd/start-edit-file-name file-id)))) - handle-key-down - (mf/use-callback + on-key-down + (mf/use-fn (mf/deps on-navigate on-select) (fn [event] (dom/stop-propagation event) @@ -371,63 +373,70 @@ (on-navigate event)) (when (kbd/shift? event) (when (or (kbd/down-arrow? event) (kbd/left-arrow? event) (kbd/up-arrow? event) (kbd/right-arrow? event)) - (on-select event)) ;; TODO Fix this - )))] + ;; TODO Fix this + (on-select event))))) + + on-menu-key-down + (mf/use-fn + (mf/deps on-menu-click) + (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (dom/prevent-default event) + (on-menu-click event))))] [:li {:class (stl/css-case :grid-item true :project-th true - :library is-library-view)} + :library is-library-view?)} [:div {:class (stl/css-case :selected selected? - :library is-library-view) + :library is-library-view?) :ref node-ref :role "button" :title (:name file) :draggable (dm/str can-edit) :on-click on-select - :on-key-down handle-key-down + :on-key-down on-key-down :on-double-click on-navigate :on-drag-start on-drag-start :on-context-menu on-context-menu} [:div {:class (stl/css :overlay)}] - (if ^boolean is-library-view + (if ^boolean is-library-view? [:> grid-item-library* {:file file}] [:> grid-item-thumbnail* {:file file :can-edit can-edit}]) - (when (and (:is-shared file) (not is-library-view)) + (when (and (:is-shared file) (not is-library-view?)) [:div {:class (stl/css :item-badge)} i/library]) [:div {:class (stl/css :info-wrapper)} [:div {:class (stl/css :item-info)} - (if (and (= file-id (:file-id dashboard-local)) (:edition dashboard-local)) + (if (and (= file-id (:file-id state)) (:edition state)) [:& inline-edition {:content (:name file) :on-end edit}] [:h3 (:name file)]) [:& grid-item-metadata {:modified-at (:modified-at file)}]] - [:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))} + [:div {:class (stl/css-case :project-th-actions true :force-display menu-open?)} [:div {:class (stl/css :project-th-icon :menu) :tab-index "0" :role "button" :aria-label (tr "dashboard.options") :ref menu-ref - :id (str file-id "-action-menu") + :id (dm/str file-id "-action-menu") :on-click on-menu-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (on-menu-click event)))} + :on-key-down on-menu-key-down} + menu-icon - (when (and selected? file-menu-open?) + (when (and selected? menu-open?) ;; When the menu is open we disable events in the dashboard. We need to force pointer events ;; so the menu can be handled - [:div {:style {:pointer-events "all"}} + [:> portal-on-document* {} [:> file-menu* {:files (vals selected-files) - :left (+ 24 (:x (:menu-pos dashboard-local))) - :top (:y (:menu-pos dashboard-local)) + :left (+ 24 (:x menu-pos)) + :top (:y menu-pos) :can-edit can-edit :navigate true :on-edit on-edit @@ -435,7 +444,7 @@ :origin origin :parent-id (dm/str file-id "-action-menu")}]])]]]]])) -(mf/defc grid +(mf/defc grid* {::mf/props :obj} [{:keys [files project origin limit create-fn can-edit selected-files]}] (let [dragging? (mf/use-state false) @@ -455,6 +464,9 @@ import-files (use-import-file project-id on-finish-import) + on-scroll + (mf/use-fn #(st/emit! (dd/hide-file-menu))) + on-drag-enter (mf/use-fn (fn [e] @@ -496,6 +508,7 @@ :on-drag-over on-drag-over :on-drag-leave on-drag-leave :on-drop on-drop + :on-scroll on-scroll :ref node-ref} (cond (nil? files) diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index dd9464c60..3f0dbee34 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -11,7 +11,7 @@ [app.main.data.team :as dtm] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.dashboard.grid :refer [grid]] + [app.main.ui.dashboard.grid :refer [grid*]] [app.main.ui.hooks :as hooks] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -67,10 +67,10 @@ [:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared) :ref rowref} - [:& grid {:files files - :selected-files selected-files - :project default-project - :origin :libraries - :limit limit - :can-edit can-edit}]]])) + [:> grid* {:files files + :selected-files selected-files + :project default-project + :origin :libraries + :limit limit + :can-edit can-edit}]]])) diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index 0ad67d06a..76ec19777 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -11,7 +11,7 @@ [app.main.data.dashboard :as dd] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.dashboard.grid :refer [grid]] + [app.main.ui.dashboard.grid :refer [grid*]] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.util.dom :as dom] @@ -77,7 +77,7 @@ [:div {:class (stl/css :text)} (tr "dashboard.no-matches-for" search-term)]] :else - [:& grid {:files result - :selected-files selected - :origin :search - :limit limit}])]])) + [:> grid* {:files result + :selected-files selected + :origin :search + :limit limit}])]])) diff --git a/frontend/src/app/main/ui/releases/v2_5.cljs b/frontend/src/app/main/ui/releases/v2_5.cljs index fcbddc92c..c39c4aebb 100644 --- a/frontend/src/app/main/ui/releases/v2_5.cljs +++ b/frontend/src/app/main/ui/releases/v2_5.cljs @@ -38,10 +38,10 @@ "We’re thrilled to introduce Penpot 2.5"] [:p {:class (stl/css :feature-content)} - "This release brings multi-step gradients, along with comment notifications, making it easier than ever to communicate with your team members. Now you also can easily copy/paste groups of styles between layers and share direct links to specific boards, among other new capabilities considered true gems for designers and team collaboration."] + "Packed with powerful new features and little big details. This release brings multi-step gradients, along with comment notifications, making it easier than ever to communicate with your team members. Now you also can easily copy/paste groups of styles between layers and share direct links to specific boards, among other new capabilities considered true gems for designers and team collaboration."] [:p {:class (stl/css :feature-content)} - "But that’s not all—we’ve also tackled numerous bug fixes and optimizations that will improve performance when working with long texts."] + "But that’s not all—we’ve also tackled numerous bug fixes and optimizations."] [:p {:class (stl/css :feature-content)} "Let’s dive in!"]] diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index d0d8ca8be..066e1d4ce 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -92,7 +92,7 @@ reverse-sort? (= :desc ordering) libs (mf/deref refs/libraries) num-libs (count libs) - file (get libs (:id file-id)) + file (get libs file-id) components (mf/with-memo [file] (ctkl/components (:data file))) toggle-ordering diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index c4ef3a93d..3bce96473 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -108,6 +108,9 @@ (filter-fonts state fonts)) recent-fonts (mf/deref refs/recent-fonts) + recent-fonts (mf/with-memo [state recent-fonts] + (filter-fonts state recent-fonts)) + full-size? (boolean (and full-size show-recent))