0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-06 12:01:19 -05:00

Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2025-02-24 12:49:04 +01:00
commit 1187d64f69
30 changed files with 406 additions and 256 deletions

View file

@ -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

View file

@ -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))))

View file

@ -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))))

View file

@ -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

View file

@ -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)))))))

View file

@ -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]

View file

@ -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;

View file

@ -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"}

View file

@ -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]

View file

@ -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))

View file

@ -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')

View file

@ -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}

View file

@ -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]

View file

@ -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)))))

View file

@ -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

BIN
docs/img/dev-tools-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
docs/img/dev-tools-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
docs/img/penpot-report.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View file

@ -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 <code class="language-bash">Console</code>, you can see the exact version that's being used.
<figure>
<a href="/img/dev-tools-1.png" target="_blank">
<img src="/img/dev-tools-1.png" alt="Devtools > Console" />
</a>
</figure>
Other interesting tab in the devtools is the <code class="language-bash">Network</code> tab, to check if there is a request that throws errors.
<figure>
<a href="/img/dev-tools-2.png" target="_blank">
<img src="/img/dev-tools-2.png" alt="Devtools > Network" />
</a>
</figure>
**Penpot Report**
When Penpot crashes, it provides a report with very useful information. Don't miss it!
<figure>
<a href="/img/penpot-report.png" target="_blank">
<img src="/img/penpot-report.png" alt="Penpot report" />
</a>
</figure>
## Install with Kubernetes

View file

@ -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 }) => {

View file

@ -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)))

View file

@ -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

View file

@ -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")

View file

@ -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}])]]))

View file

@ -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)

View file

@ -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}]]]))

View file

@ -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}])]]))

View file

@ -38,10 +38,10 @@
"Were 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 thats not all—weve also tackled numerous bug fixes and optimizations that will improve performance when working with long texts."]
"But thats not all—weve also tackled numerous bug fixes and optimizations."]
[:p {:class (stl/css :feature-content)}
"Lets dive in!"]]

View file

@ -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

View file

@ -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))