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 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 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 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 problem with alt key measures being stuck [Taiga #9348](https://tree.taiga.io/project/penpot/issue/9348)
- Fix error when reseting stroke cap - Fix error when reseting stroke cap
- Fix problem with strokes not refreshing in Safari [Taiga #9040](https://tree.taiga.io/project/penpot/issue/9040) - 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 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 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 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 ## 2.4.3

View file

@ -20,7 +20,6 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.features.components-v2 :as feat.compv2]
[app.features.fdata :as feat.fdata] [app.features.fdata :as feat.fdata]
[app.features.file-migrations :as feat.fmigr] [app.features.file-migrations :as feat.fmigr]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
@ -307,7 +306,7 @@
update-shapes update-shapes
(fn [data {:keys [page-id shape-id]}] (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 file
(update file :data #(reduce update-shapes % media-refs))] (update file :data #(reduce update-shapes % media-refs))]
@ -375,7 +374,7 @@
replace the old :component-file reference with the new replace the old :component-file reference with the new
ones, using the provided file-index." ones, using the provided file-index."
[data] [data]
(cfh/relink-media-refs data lookup-index)) (cfh/relink-refs data lookup-index))
(defn- relink-media (defn- relink-media
"A function responsible of process the :media attr of file data and "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)))) (l/error :hint "file schema validation error" :cause result))))
(insert-file! cfg file opts))) (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]) (:refer-clojure :exclude [assert])
(:require (:require
[app.binfile.common :as bfc] [app.binfile.common :as bfc]
[app.binfile.migrations :as bfm]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
@ -473,7 +474,7 @@
(read-section options)))) (read-section options))))
[:v1/metadata :v1/files :v1/rels :v1/sobjects]) [: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, ;; Knowing that the ids of the created files are in index,
;; just lookup them and return it as a set ;; just lookup them and return it as a set

View file

@ -9,6 +9,7 @@
(:refer-clojure :exclude [read]) (:refer-clojure :exclude [read])
(:require (:require
[app.binfile.common :as bfc] [app.binfile.common :as bfc]
[app.binfile.migrations :as bfm]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
@ -735,7 +736,7 @@
(bfc/process-file))] (bfc/process-file))]
(bfc/register-pending-migrations! cfg file) (bfm/register-pending-migrations! cfg file)
(bfc/save-file! cfg file ::db/return-keys false) (bfc/save-file! cfg file ::db/return-keys false)
file-id'))) file-id')))
@ -915,7 +916,7 @@
(import-file-media cfg) (import-file-media cfg)
(import-file-thumbnails cfg) (import-file-thumbnails cfg)
(bfc/apply-pending-migrations! cfg) (bfm/apply-pending-migrations! cfg)
ids))))))) ids)))))))

View file

@ -435,7 +435,10 @@
:fn (mg/resource "app/migrations/sql/0137-add-file-migration-table.sql")} :fn (mg/resource "app/migrations/sql/0137-add-file-migration-table.sql")}
{:name "0138-mod-file-data-fragment-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! (defn apply-migrations!
[pool name 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 (ns app.rpc.commands.files-snapshot
(:require (:require
[app.binfile.common :as bfc]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
@ -22,7 +23,6 @@
[app.rpc.quotes :as quotes] [app.rpc.quotes :as quotes]
[app.storage :as sto] [app.storage :as sto]
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -58,26 +58,6 @@
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
(get-file-snapshots conn 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 (defn- generate-snapshot-label
[] []
(let [ts (-> (dt/now) (let [ts (-> (dt/now)
@ -87,49 +67,53 @@
(str "snapshot-" ts))) (str "snapshot-" ts)))
(defn create-file-snapshot! (defn create-file-snapshot!
[cfg profile-id file-id label] [cfg file & {:keys [label created-by deleted-at profile-id]
(let [file (get-file cfg file-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 created-by
(if (= label :system) (name created-by)
"system"
"user")
deleted-at deleted-at
(if (= label :system) (cond
(= deleted-at :default)
(dt/plus (dt/now) (cf/get-deletion-delay)) (dt/plus (dt/now) (cf/get-deletion-delay))
(dt/instant? deleted-at)
deleted-at
:else
nil) nil)
label label
(if (= label :system) (or label (generate-snapshot-label))
(str "internal/snapshot/" (:revn file))
(or label (generate-snapshot-label)))
snapshot-id snapshot-id
(uuid/next)] (uuid/next)
(-> cfg data
(assoc ::quotes/profile-id profile-id) (blob/encode (:data file))
(assoc ::quotes/project-id (:project-id file))
(assoc ::quotes/team-id (:team-id file)) features
(assoc ::quotes/file-id (:id file)) (db/encode-pgarray (:features file) conn "text")]
(quotes/check! {::quotes/id ::quotes/snapshots-per-file}
{::quotes/id ::quotes/snapshots-per-team}))
(l/debug :hint "creating file snapshot" (l/debug :hint "creating file snapshot"
:file-id (str file-id) :file-id (str (:id file))
:id (str snapshot-id) :id (str snapshot-id)
:label label) :label label)
(db/insert! cfg :file-change (db/insert! cfg :file-change
{:id snapshot-id {:id snapshot-id
:revn (:revn file) :revn (:revn file)
:data (:data file) :data data
:version (:version file) :version (:version file)
:features (:features file) :features features
:profile-id profile-id :profile-id profile-id
:file-id (:id file) :file-id (:id file)
:label label :label label
@ -146,12 +130,25 @@
(sv/defmethod ::create-file-snapshot (sv/defmethod ::create-file-snapshot
{::doc/added "1.20" {::doc/added "1.20"
::sm/params schema:create-file-snapshot} ::sm/params schema:create-file-snapshot
[cfg {:keys [::rpc/profile-id file-id label]}] ::db/transaction true}
(db/tx-run! cfg [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id label]}]
(fn [{:keys [::db/conn] :as cfg}] (files/check-edition-permissions! conn profile-id file-id)
(files/check-edition-permissions! conn profile-id file-id) (let [file (bfc/get-file cfg file-id)
(create-file-snapshot! cfg profile-id file-id label)))) 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! (defn restore-file-snapshot!
[{:keys [::db/conn ::mbus/msgbus] :as cfg} file-id snapshot-id] [{:keys [::db/conn ::mbus/msgbus] :as cfg} file-id snapshot-id]
@ -237,8 +234,11 @@
(db/tx-run! cfg (db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}] (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id) (files/check-edition-permissions! conn profile-id file-id)
(create-file-snapshot! cfg profile-id file-id :system) (let [file (bfc/get-file cfg file-id)]
(restore-file-snapshot! cfg file-id 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 (def ^:private schema:update-file-snapshot
[:map {:title "update-file-snapshot"} [:map {:title "update-file-snapshot"}

View file

@ -34,7 +34,6 @@
;; --- Mutation: Create Team Invitation ;; --- Mutation: Create Team Invitation
(def sql:upsert-team-invitation (def sql:upsert-team-invitation
"insert into team_invitation(id, team_id, email_to, created_by, role, valid_until) "insert into team_invitation(id, team_id, email_to, created_by, role, valid_until)
values (?, ?, ?, ?, ?, ?) values (?, ?, ?, ?, ?, ?)
@ -79,27 +78,23 @@
[:role ::types.team/role] [:role ::types.team/role]
[:email ::sm/email]]) [:email ::sm/email]])
(def ^:private check-create-invitation-params! (def ^:private check-create-invitation-params
(sm/check-fn schema:create-invitation)) (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 (defn- create-invitation
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}] [{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
(dm/assert! (assert (db/connection? conn) "expected valid connection on cfg parameter")
"expected valid connection on cfg parameter" (assert (check-create-invitation-params params))
(db/connection? conn))
(dm/assert!
"expected valid params for `create-invitation` fn"
(check-create-invitation-params! params))
(let [email (profile/clean-email email) (let [email (profile/clean-email email)
member (profile/get-profile-by-email conn 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 ;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the ;; already present in the database, we proceed to add it to the
;; team as-is, without email roundtrip. ;; team as-is, without email roundtrip.
@ -125,48 +120,54 @@
nil) nil)
(let [id (uuid/next) (do
expire (dt/in-future "168h") ;; 7 days (some->> member (teams/check-profile-muted conn))
invitation (db/exec-one! conn [sql:upsert-team-invitation id (teams/check-email-bounce conn email true)
(:id team) (str/lower email) (teams/check-email-spam conn email true)
(: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)]
(when (contains? cf/flags :log-invitation-tokens) (let [id (uuid/next)
(l/info :hint "invitation token" :token itoken)) 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) (when (contains? cf/flags :log-invitation-tokens)
(audit/clean-props)) (l/info :hint "invitation token" :token itoken))
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))
(eml/send! {::eml/conn conn (let [props (-> (dissoc tprops :profile-id)
::eml/factory eml/invite-to-team (audit/clean-props))
:public-uri (cf/get :public-uri) evname (if updated?
:to email "update-team-invitation"
:invited-by (:fullname profile) "create-team-invitation")
:team (:name team) event (-> (audit/event-from-rpc-params params)
:token itoken (assoc ::audit/name evname)
:extra-data ptoken}) (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 (defn- add-member-to-team
[conn profile team role member] [conn profile team role member]

View file

@ -16,7 +16,8 @@
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond] [app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv])) [app.util.services :as sv]
[cuerdas.core :as str]))
;; --- QUERY: View Only Bundle ;; --- QUERY: View Only Bundle
@ -26,6 +27,27 @@
(update :pages (fn [pages] (filterv #(contains? allowed %) pages))) (update :pages (fn [pages] (filterv #(contains? allowed %) pages)))
(update :pages-index select-keys allowed))) (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 (defn- get-view-only-bundle
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id ::perms] :as params}] [{:keys [::db/conn] :as cfg} {:keys [profile-id file-id ::perms] :as params}]
(let [file (files/get-file cfg file-id) (let [file (files/get-file cfg file-id)
@ -37,7 +59,10 @@
team (-> (db/get conn :team {:id (:team-id project)}) team (-> (db/get conn :team {:id (:team-id project)})
(teams/decode-row)) (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) member-ids (into #{} (map :id) members)
perms (assoc perms :in-team (contains? member-ids profile-id)) 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.features.components-v2 :as feat.comp-v2]
[app.main :as main] [app.main :as main]
[app.rpc.commands.files :as files] [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) (def ^:dynamic *system* nil)
@ -96,8 +97,11 @@
(let [conn (db/get-connection system)] (let [conn (db/get-connection system)]
(->> (feat.comp-v2/get-and-lock-team-files conn team-id) (->> (feat.comp-v2/get-and-lock-team-files conn team-id)
(reduce (fn [result file-id] (reduce (fn [result file-id]
(fsnap/create-file-snapshot! system nil file-id label) (let [file (fsnap/get-file-snapshots system file-id)]
(inc result)) (fsnap/create-file-snapshot! system file
{:label label
:created-by :admin})
(inc result)))
0)))) 0))))
(defn restore-team-snapshot! (defn restore-team-snapshot!
@ -143,7 +147,10 @@
(cfv/validate-file-schema! file')) (cfv/validate-file-schema! file'))
(when (string? label) (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)] (let [file' (update file' :revn inc)]
(bfc/update-file! system file') (bfc/update-file! system file')

View file

@ -40,6 +40,11 @@
:file-id id :file-id id
:cause cause)))) :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 ;; Mark file media objects to be deleted
(db/update! conn :file-media-object (db/update! conn :file-media-object
{:deleted-at deleted-at} {:deleted-at deleted-at}

View file

@ -26,7 +26,7 @@
(defn- delete-profiles! (defn- delete-profiles!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] [{: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]}] (reduce (fn [total {:keys [id photo-id]}]
(l/trc :hint "permanently delete" :rel "profile" :id (str id)) (l/trc :hint "permanently delete" :rel "profile" :id (str id))
@ -49,7 +49,7 @@
(defn- delete-teams! (defn- delete-teams!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] [{: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]}] (reduce (fn [total {:keys [id photo-id deleted-at]}]
(l/trc :hint "permanently delete" (l/trc :hint "permanently delete"
:rel "team" :rel "team"
@ -77,7 +77,7 @@
(defn- delete-fonts! (defn- delete-fonts!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] [{: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}] (reduce (fn [total {:keys [id team-id deleted-at] :as font}]
(l/trc :hint "permanently delete" (l/trc :hint "permanently delete"
:rel "team-font-variant" :rel "team-font-variant"
@ -109,7 +109,7 @@
(defn- delete-projects! (defn- delete-projects!
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}] [{: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]}] (reduce (fn [total {:keys [id team-id deleted-at]}]
(l/trc :hint "permanently delete" (l/trc :hint "permanently delete"
:rel "project" :rel "project"
@ -135,7 +135,7 @@
(defn- delete-files! (defn- delete-files!
[{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}] [{: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}] (reduce (fn [total {:keys [id deleted-at project-id] :as file}]
(l/trc :hint "permanently delete" (l/trc :hint "permanently delete"
:rel "file" :rel "file"
@ -164,7 +164,7 @@
(defn delete-file-thumbnails! (defn delete-file-thumbnails!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] [{: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]}] (reduce (fn [total {:keys [file-id revn media-id deleted-at]}]
(l/trc :hint "permanently delete" (l/trc :hint "permanently delete"
:rel "file-thumbnail" :rel "file-thumbnail"
@ -193,7 +193,7 @@
(defn delete-file-object-thumbnails! (defn delete-file-object-thumbnails!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] [{: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]}] (reduce (fn [total {:keys [file-id object-id media-id deleted-at]}]
(l/trc :hint "permanently delete" (l/trc :hint "permanently delete"
:rel "file-tagged-object-thumbnail" :rel "file-tagged-object-thumbnail"
@ -222,7 +222,7 @@
(defn- delete-file-data-fragments! (defn- delete-file-data-fragments!
[{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}] [{: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]}] (reduce (fn [total {:keys [file-id id deleted-at data-ref-id]}]
(l/trc :hint "permanently delete" (l/trc :hint "permanently delete"
:rel "file-data-fragment" :rel "file-data-fragment"
@ -248,7 +248,7 @@
(defn- delete-file-media-objects! (defn- delete-file-media-objects!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] [{: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}] (reduce (fn [total {:keys [id file-id deleted-at] :as fmo}]
(l/trc :hint "permanently delete" (l/trc :hint "permanently delete"
:rel "file-media-object" :rel "file-media-object"
@ -275,9 +275,9 @@
FOR UPDATE FOR UPDATE
SKIP LOCKED") SKIP LOCKED")
(defn- delete-file-change! (defn- delete-file-changes!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] [{: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}] (reduce (fn [total {:keys [id file-id deleted-at] :as xlog}]
(l/trc :hint "permanently delete" (l/trc :hint "permanently delete"
:rel "file-change" :rel "file-change"
@ -299,11 +299,11 @@
#'delete-file-data-fragments! #'delete-file-data-fragments!
#'delete-file-object-thumbnails! #'delete-file-object-thumbnails!
#'delete-file-thumbnails! #'delete-file-thumbnails!
#'delete-file-changes!
#'delete-files! #'delete-files!
#'delete-projects! #'delete-projects!
#'delete-fonts! #'delete-fonts!
#'delete-teams! #'delete-teams!])
#'delete-file-change!])
(defn- execute-proc! (defn- execute-proc!
"A generic function that executes the specified proc iterativelly "A generic function that executes the specified proc iterativelly
@ -326,7 +326,7 @@
[k v] [k v]
{k (assoc v {k (assoc v
::min-age (cf/get-deletion-delay) ::min-age (cf/get-deletion-delay)
::chunk-size 50)}) ::chunk-size 100)})
(defmethod ig/init-key ::handler (defmethod ig/init-key ::handler
[_ cfg] [_ cfg]

View file

@ -97,6 +97,7 @@
(th/db-query :file-change (th/db-query :file-change
{:file-id (:id file)} {:file-id (:id file)}
{:order-by [:created-at]})] {:order-by [:created-at]})]
(t/is (= 2 (count rows))) (t/is (= 2 (count rows)))
(t/is (= "user" (:created-by row1))) (t/is (= "user" (:created-by row1)))
(t/is (= "system" (:created-by row2))))) (t/is (= "system" (:created-by row2)))))

View file

@ -589,10 +589,9 @@
(into xform:collect-media-refs (vals (:components data))) (into xform:collect-media-refs (vals (:components data)))
(into (keys (:media data))))) (into (keys (:media data)))))
(defn relink-media-refs (defn relink-refs
"A function responsible to analyze all file data and replace the "A function responsible to analyze the file data or shape for references
old :component-file reference with the new ones, using the provided and apply lookup-index on it."
file-index."
[data lookup-index] [data lookup-index]
(letfn [(process-map-form [form] (letfn [(process-map-form [form]
(cond-> 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 clips). There may be more volumes if you enable other features, as explained in the file
itself. 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 ## Install with Kubernetes

View file

@ -105,10 +105,10 @@ test("Multiple elements in context", async ({ page }) => {
await button.click({ button: "right" }); await button.click({ button: "right" });
await expect(button.getByTestId("duplicate-multi")).toBeVisible(); await expect(page.getByTestId("duplicate-multi")).toBeVisible();
await expect(button.getByTestId("file-move-multi")).toBeVisible(); await expect(page.getByTestId("file-move-multi")).toBeVisible();
await expect(button.getByTestId("file-binary-export-multi")).toBeVisible(); await expect(page.getByTestId("file-binary-export-multi")).toBeVisible();
await expect(button.getByTestId("file-delete-multi")).toBeVisible(); await expect(page.getByTestId("file-delete-multi")).toBeVisible();
}); });
test("User has create file button", async ({ page }) => { 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) permissions (:permissions team)
dashboard-local (mf/deref refs/dashboard-local)
file-menu-open? (:menu-open dashboard-local)
default-project-id default-project-id
(get default-project :id) (get default-project :id)
@ -87,7 +84,6 @@
(mf/use-effect on-resize) (mf/use-effect on-resize)
[:div {:class (stl/css :dashboard-content) [:div {:class (stl/css :dashboard-content)
:style {:pointer-events (when file-menu-open? "none")}
:on-click clear-selected-fn :on-click clear-selected-fn
:ref container} :ref container}
(case section (case section

View file

@ -55,8 +55,8 @@
projects)) projects))
(mf/defc file-menu* (mf/defc file-menu*
{::mf/props :obj}
[{:keys [files on-edit on-menu-close top left navigate origin parent-id can-edit]}] [{:keys [files on-edit on-menu-close top left navigate origin parent-id can-edit]}]
(assert (seq files) "missing `files` prop") (assert (seq files) "missing `files` prop")
(assert (fn? on-edit) "missing `on-edit` prop") (assert (fn? on-edit) "missing `on-edit` prop")
(assert (fn? on-menu-close) "missing `on-menu-close` 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.data.project :as dpj]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [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.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.pin-button :refer [pin-button*]] [app.main.ui.dashboard.pin-button :refer [pin-button*]]
[app.main.ui.dashboard.project-menu :refer [project-menu*]] [app.main.ui.dashboard.project-menu :refer [project-menu*]]
@ -198,11 +198,11 @@
:subtitle (if is-draft-proyect :subtitle (if is-draft-proyect
(tr "dashboard.empty-placeholder-drafts-subtitle") (tr "dashboard.empty-placeholder-drafts-subtitle")
(tr "dashboard.empty-placeholder-files-subtitle"))}] (tr "dashboard.empty-placeholder-files-subtitle"))}]
[:& grid {:project project [:> grid* {:project project
:files files :files files
:selected-files selected-files :selected-files selected-files
:can-edit can-edit? :can-edit can-edit?
:origin :files :origin :files
:create-fn create-file :create-fn create-file
:limit limit}])]])) :limit limit}])]]))

View file

@ -25,6 +25,7 @@
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.color-bullet :as bc] [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.file-menu :refer [file-menu*]]
[app.main.ui.dashboard.import :refer [use-import-file]] [app.main.ui.dashboard.import :refer [use-import-file]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]]
@ -241,29 +242,37 @@
counter-el)) counter-el))
(mf/defc grid-item* (mf/defc grid-item*
{::mf/props :obj}
[{:keys [file origin can-edit selected-files]}] [{: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) menu-open?
file-menu-open? (:menu-open dashboard-local) (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) selected-num
menu-ref (mf/use-ref) (count selected-files)
node-ref (mf/use-ref)
menu-ref (mf/use-ref)
is-library-view?
(= origin :libraries)
on-menu-close on-menu-close
(mf/use-fn (mf/use-fn #(st/emit! (dd/hide-file-menu)))
(fn [_]
(st/emit! (dd/hide-file-menu))))
on-select on-select
(mf/use-fn (mf/use-fn
(mf/deps selected? selected-num)
(fn [event] (fn [event]
(when (or (not selected?) (> (count selected-files) 1)) (when (or (not selected?) (> selected-num 1))
(dom/stop-propagation event) (dom/stop-propagation event)
(let [shift? (kbd/shift? event)] (let [shift? (kbd/shift? event)]
(when-not shift? (when-not shift?
@ -281,41 +290,36 @@
on-drag-start on-drag-start
(mf/use-fn (mf/use-fn
(mf/deps selected-files can-edit) (mf/deps selected? selected-num)
(fn [event] (fn [event]
(st/emit! (dd/hide-file-menu)) (st/emit! (dd/hide-file-menu))
(when can-edit (when can-edit
(let [offset (dom/get-offset-position (dom/event->native-event event)) (let [offset (dom/get-offset-position (dom/event->native-event event))
item-el (mf/ref-val node-ref)
select-current? (not (contains? selected-files (:id file))) counter-el (create-counter-element item-el
(if (not selected?)
item-el (mf/ref-val node-ref) 1
counter-el (create-counter-element selected-num))]
item-el (when (not selected?)
(if select-current?
1
(count selected-files)))]
(when select-current?
(st/emit! (dd/clear-selected-files)) (st/emit! (dd/clear-selected-files))
(st/emit! (dd/toggle-file-select file))) (st/emit! (dd/toggle-file-select file)))
(dnd/set-data! event "penpot/files" "dummy") (dnd/set-data! event "penpot/files" "dummy")
(dnd/set-allowed-effect! event "move") (dnd/set-allowed-effect! event "move")
;; set-drag-image requires that the element is rendered and ;; set-drag-image requires that the element is rendered
;; visible to the user at the moment of creating the ghost ;; and visible to the user at the moment of creating the
;; image (to make a snapshot), but you may remove it right ;; ghost image (to make a snapshot), but you may remove
;; afterwards, in the next render cycle. ;; it right afterwards, in the next render cycle.
(dom/append-child! item-el counter-el) (dom/append-child! item-el counter-el)
(dnd/set-drag-image! event item-el (:x offset) (:y offset)) (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 on-menu-click
(mf/use-fn (mf/use-fn
(mf/deps file selected?) (mf/deps file selected?)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(dom/prevent-default event)
(when-not selected? (when-not selected?
(when-not (kbd/shift? event) (when-not (kbd/shift? event)
@ -339,12 +343,10 @@
on-context-menu on-context-menu
(mf/use-fn (mf/use-fn
(mf/deps on-menu-click is-library-view) (mf/deps on-menu-click)
(fn [event] (fn [event]
(dom/stop-propagation event)
(dom/prevent-default event) (dom/prevent-default event)
(when-not is-library-view (on-menu-click event)))
(on-menu-click event))))
edit edit
(mf/use-fn (mf/use-fn
@ -362,8 +364,8 @@
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (dd/start-edit-file-name file-id)))) (st/emit! (dd/start-edit-file-name file-id))))
handle-key-down on-key-down
(mf/use-callback (mf/use-fn
(mf/deps on-navigate on-select) (mf/deps on-navigate on-select)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
@ -371,63 +373,70 @@
(on-navigate event)) (on-navigate event))
(when (kbd/shift? event) (when (kbd/shift? event)
(when (or (kbd/down-arrow? event) (kbd/left-arrow? event) (kbd/up-arrow? event) (kbd/right-arrow? 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 [:li {:class (stl/css-case :grid-item true
:project-th true :project-th true
:library is-library-view)} :library is-library-view?)}
[:div [:div
{:class (stl/css-case :selected selected? {:class (stl/css-case :selected selected?
:library is-library-view) :library is-library-view?)
:ref node-ref :ref node-ref
:role "button" :role "button"
:title (:name file) :title (:name file)
:draggable (dm/str can-edit) :draggable (dm/str can-edit)
:on-click on-select :on-click on-select
:on-key-down handle-key-down :on-key-down on-key-down
:on-double-click on-navigate :on-double-click on-navigate
:on-drag-start on-drag-start :on-drag-start on-drag-start
:on-context-menu on-context-menu} :on-context-menu on-context-menu}
[:div {:class (stl/css :overlay)}] [:div {:class (stl/css :overlay)}]
(if ^boolean is-library-view (if ^boolean is-library-view?
[:> grid-item-library* {:file file}] [:> grid-item-library* {:file file}]
[:> grid-item-thumbnail* {:file file :can-edit can-edit}]) [:> 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 :item-badge)} i/library])
[:div {:class (stl/css :info-wrapper)} [:div {:class (stl/css :info-wrapper)}
[:div {:class (stl/css :item-info)} [: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) [:& inline-edition {:content (:name file)
:on-end edit}] :on-end edit}]
[:h3 (:name file)]) [:h3 (:name file)])
[:& grid-item-metadata {:modified-at (:modified-at 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 [:div
{:class (stl/css :project-th-icon :menu) {:class (stl/css :project-th-icon :menu)
:tab-index "0" :tab-index "0"
:role "button" :role "button"
:aria-label (tr "dashboard.options") :aria-label (tr "dashboard.options")
:ref menu-ref :ref menu-ref
:id (str file-id "-action-menu") :id (dm/str file-id "-action-menu")
:on-click on-menu-click :on-click on-menu-click
:on-key-down (fn [event] :on-key-down on-menu-key-down}
(when (kbd/enter? event)
(dom/stop-propagation event)
(on-menu-click event)))}
menu-icon 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 ;; When the menu is open we disable events in the dashboard. We need to force pointer events
;; so the menu can be handled ;; so the menu can be handled
[:div {:style {:pointer-events "all"}} [:> portal-on-document* {}
[:> file-menu* {:files (vals selected-files) [:> file-menu* {:files (vals selected-files)
:left (+ 24 (:x (:menu-pos dashboard-local))) :left (+ 24 (:x menu-pos))
:top (:y (:menu-pos dashboard-local)) :top (:y menu-pos)
:can-edit can-edit :can-edit can-edit
:navigate true :navigate true
:on-edit on-edit :on-edit on-edit
@ -435,7 +444,7 @@
:origin origin :origin origin
:parent-id (dm/str file-id "-action-menu")}]])]]]]])) :parent-id (dm/str file-id "-action-menu")}]])]]]]]))
(mf/defc grid (mf/defc grid*
{::mf/props :obj} {::mf/props :obj}
[{:keys [files project origin limit create-fn can-edit selected-files]}] [{:keys [files project origin limit create-fn can-edit selected-files]}]
(let [dragging? (mf/use-state false) (let [dragging? (mf/use-state false)
@ -455,6 +464,9 @@
import-files import-files
(use-import-file project-id on-finish-import) (use-import-file project-id on-finish-import)
on-scroll
(mf/use-fn #(st/emit! (dd/hide-file-menu)))
on-drag-enter on-drag-enter
(mf/use-fn (mf/use-fn
(fn [e] (fn [e]
@ -496,6 +508,7 @@
:on-drag-over on-drag-over :on-drag-over on-drag-over
:on-drag-leave on-drag-leave :on-drag-leave on-drag-leave
:on-drop on-drop :on-drop on-drop
:on-scroll on-scroll
:ref node-ref} :ref node-ref}
(cond (cond
(nil? files) (nil? files)

View file

@ -11,7 +11,7 @@
[app.main.data.team :as dtm] [app.main.data.team :as dtm]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [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.hooks :as hooks]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@ -67,10 +67,10 @@
[:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared) [:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared)
:ref rowref} :ref rowref}
[:& grid {:files files [:> grid* {:files files
:selected-files selected-files :selected-files selected-files
:project default-project :project default-project
:origin :libraries :origin :libraries
:limit limit :limit limit
:can-edit can-edit}]]])) :can-edit can-edit}]]]))

View file

@ -11,7 +11,7 @@
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [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.hooks :as hooks]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
@ -77,7 +77,7 @@
[:div {:class (stl/css :text)} (tr "dashboard.no-matches-for" search-term)]] [:div {:class (stl/css :text)} (tr "dashboard.no-matches-for" search-term)]]
:else :else
[:& grid {:files result [:> grid* {:files result
:selected-files selected :selected-files selected
:origin :search :origin :search
:limit limit}])]])) :limit limit}])]]))

View file

@ -38,10 +38,10 @@
"Were thrilled to introduce Penpot 2.5"] "Were thrilled to introduce Penpot 2.5"]
[:p {:class (stl/css :feature-content)} [: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)} [: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)} [:p {:class (stl/css :feature-content)}
"Lets dive in!"]] "Lets dive in!"]]

View file

@ -92,7 +92,7 @@
reverse-sort? (= :desc ordering) reverse-sort? (= :desc ordering)
libs (mf/deref refs/libraries) libs (mf/deref refs/libraries)
num-libs (count libs) num-libs (count libs)
file (get libs (:id file-id)) file (get libs file-id)
components (mf/with-memo [file] (ctkl/components (:data file))) components (mf/with-memo [file] (ctkl/components (:data file)))
toggle-ordering toggle-ordering

View file

@ -108,6 +108,9 @@
(filter-fonts state fonts)) (filter-fonts state fonts))
recent-fonts (mf/deref refs/recent-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)) full-size? (boolean (and full-size show-recent))