mirror of
https://github.com/penpot/penpot.git
synced 2025-06-02 22:11:37 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
1187d64f69
30 changed files with 406 additions and 256 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))))
|
|
||||||
|
|
50
backend/src/app/binfile/migrations.clj
Normal file
50
backend/src/app/binfile/migrations.clj
Normal 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))))
|
|
@ -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
|
||||||
|
|
|
@ -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)))))))
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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;
|
|
@ -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"}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)))))
|
||||||
|
|
|
@ -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
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
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
BIN
docs/img/penpot-report.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 132 KiB |
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
15
frontend/src/app/main/ui/components/portal.cljs
Normal file
15
frontend/src/app/main/ui/components/portal.cljs
Normal 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)))
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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}])]]))
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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}]]]))
|
||||||
|
|
||||||
|
|
|
@ -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}])]]))
|
||||||
|
|
|
@ -38,10 +38,10 @@
|
||||||
"We’re thrilled to introduce Penpot 2.5"]
|
"We’re 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 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)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
"Let’s dive in!"]]
|
"Let’s dive in!"]]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue