mirror of
https://github.com/penpot/penpot.git
synced 2025-04-09 13:31:23 -05:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
e189dc965d
24 changed files with 603 additions and 374 deletions
CHANGES.md
backend
resources/app
scripts
src/app
test/backend_tests
common/src/app/common
frontend/src/app
|
@ -83,6 +83,7 @@
|
|||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix webhook checkbox position [Taiga #8634](https://tree.taiga.io/project/penpot/issue/8634)
|
||||
- Fix wrong props on padding input [Taiga #8254](https://tree.taiga.io/project/penpot/issue/8254)
|
||||
- Fix fill collapsed options [Taiga #8351](https://tree.taiga.io/project/penpot/issue/8351)
|
||||
- Fix scroll on color picker modal [Taiga #8353](https://tree.taiga.io/project/penpot/issue/8353)
|
||||
|
|
|
@ -36,4 +36,7 @@
|
|||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"}
|
||||
{:id "flex-layout-playground"
|
||||
:name "Flex Layout Playground"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}]
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}
|
||||
{:id "welcome"
|
||||
:name "Welcome"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/welcome.penpot"}]
|
||||
|
|
|
@ -24,7 +24,7 @@ export PENPOT_FLAGS="\
|
|||
enable-rpc-climit \
|
||||
enable-rpc-rlimit \
|
||||
enable-soft-rpc-rlimit \
|
||||
enable-file-snapshot \
|
||||
enable-auto-file-snapshot \
|
||||
enable-webhooks \
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
|
|
|
@ -27,9 +27,11 @@
|
|||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.welcome-file :refer [create-welcome-file]]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def schema:password
|
||||
|
@ -241,6 +243,7 @@
|
|||
|
||||
params (d/without-nils params)
|
||||
token (tokens/generate (::setup/props cfg) params)]
|
||||
|
||||
(with-meta {:token token}
|
||||
{::audit/profile-id uuid/zero})))
|
||||
|
||||
|
@ -350,7 +353,7 @@
|
|||
:extra-data ptoken})))
|
||||
|
||||
(defn register-profile
|
||||
[{:keys [::db/conn] :as cfg} {:keys [token fullname theme] :as params}]
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token fullname theme] :as params}]
|
||||
(let [theme (when (= theme "light") theme)
|
||||
claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
|
||||
params (-> claims
|
||||
|
@ -380,8 +383,13 @@
|
|||
invitation (when-let [token (:invitation-token params)]
|
||||
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
|
||||
|
||||
props (audit/profile->props profile)]
|
||||
props (audit/profile->props profile)
|
||||
|
||||
create-welcome-file-when-needed
|
||||
(fn []
|
||||
(when (:create-welcome-file params)
|
||||
(let [cfg (dissoc cfg ::db/conn)]
|
||||
(wrk/submit! executor (create-welcome-file cfg profile)))))]
|
||||
(cond
|
||||
;; When profile is blocked, we just ignore it and return plain data
|
||||
(:is-blocked profile)
|
||||
|
@ -418,6 +426,7 @@
|
|||
(if (:is-active profile)
|
||||
(-> (profile/strip-private-attrs profile)
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-defer create-welcome-file-when-needed)
|
||||
(rph/with-meta
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "login"}
|
||||
|
@ -427,10 +436,12 @@
|
|||
(when-not (eml/has-reports? conn (:email profile))
|
||||
(send-email-verification! cfg profile))
|
||||
|
||||
(rph/with-meta {:email (:email profile)}
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "email-verification"}
|
||||
::audit/profile-id (:id profile)})))
|
||||
(-> {:email (:email profile)}
|
||||
(rph/with-defer create-welcome-file-when-needed)
|
||||
(rph/with-meta
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "email-verification"}
|
||||
::audit/profile-id (:id profile)}))))
|
||||
|
||||
:else
|
||||
(let [elapsed? (elapsed-verify-threshold? profile)
|
||||
|
@ -462,7 +473,8 @@
|
|||
[:map {:title "register-profile"}
|
||||
[:token schema:token]
|
||||
[:fullname [::sm/word-string {:max 100}]]
|
||||
[:theme {:optional true} [:string {:max 10}]]])
|
||||
[:theme {:optional true} [:string {:max 10}]]
|
||||
[:create-welcome-file {:optional true} :boolean]])
|
||||
|
||||
(sv/defmethod ::register-profile
|
||||
{::rpc/auth false
|
||||
|
|
|
@ -38,6 +38,20 @@
|
|||
[clojure.set :as set]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(declare ^:private get-lagged-changes)
|
||||
(declare ^:private send-notifications!)
|
||||
(declare ^:private update-file)
|
||||
(declare ^:private update-file*)
|
||||
(declare ^:private process-changes-and-validate)
|
||||
(declare ^:private take-snapshot?)
|
||||
(declare ^:private delete-old-snapshots!)
|
||||
|
||||
;; PUBLIC API; intended to be used outside of this module
|
||||
(declare update-file!)
|
||||
(declare update-file-data!)
|
||||
(declare persist-file!)
|
||||
(declare get-file)
|
||||
|
||||
;; --- SCHEMA
|
||||
|
||||
(def ^:private
|
||||
|
@ -97,41 +111,6 @@
|
|||
(or (contains? library-change-types type)
|
||||
(contains? file-change-types type)))
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.*, p.team_id
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?
|
||||
AND (f.deleted_at IS NULL OR
|
||||
f.deleted_at > now())
|
||||
FOR KEY SHARE")
|
||||
|
||||
(defn get-file
|
||||
[conn id]
|
||||
(let [file (db/exec-one! conn [sql:get-file id])]
|
||||
(when-not file
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint (format "file with id '%s' does not exists" id)))
|
||||
(update file :features db/decode-pgarray #{})))
|
||||
|
||||
(defn- wrap-with-pointer-map-context
|
||||
[f]
|
||||
(fn [cfg {:keys [id] :as file}]
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)
|
||||
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(let [result (f cfg file)]
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
result))))
|
||||
|
||||
(declare ^:private delete-old-snapshots!)
|
||||
(declare ^:private get-lagged-changes)
|
||||
(declare ^:private send-notifications!)
|
||||
(declare ^:private take-snapshot?)
|
||||
(declare ^:private update-file)
|
||||
(declare ^:private update-file*)
|
||||
(declare ^:private update-file-data)
|
||||
|
||||
;; If features are specified from params and the final feature
|
||||
;; set is different than the persisted one, update it on the
|
||||
;; database.
|
||||
|
@ -147,7 +126,8 @@
|
|||
::sm/result schema:update-file-result
|
||||
::doc/module :files
|
||||
::doc/added "1.17"}
|
||||
[cfg {:keys [::rpc/profile-id id] :as params}]
|
||||
[{:keys [::mtx/metrics] :as cfg}
|
||||
{:keys [::rpc/profile-id id changes changes-with-metadata] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
(db/xact-lock! conn id)
|
||||
|
@ -161,14 +141,30 @@
|
|||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
|
||||
params (assoc params
|
||||
:profile-id profile-id
|
||||
:features features
|
||||
:team team
|
||||
:file file)
|
||||
changes (if changes-with-metadata
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
(vec changes))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features)
|
||||
(assoc :team team)
|
||||
(assoc :file file)
|
||||
(assoc :changes changes))
|
||||
|
||||
cfg (assoc cfg ::timestamp (dt/now))
|
||||
|
||||
tpoint (dt/tpoint)]
|
||||
|
||||
|
||||
(when (> (:revn params)
|
||||
(:revn file))
|
||||
(ex/raise :type :validation
|
||||
:code :revn-conflict
|
||||
:hint "The incoming revision number is greater that stored version."
|
||||
:context {:incoming-revn (:revn params)
|
||||
:stored-revn (:revn file)}))
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
(when (not= features (:features team))
|
||||
|
@ -177,90 +173,126 @@
|
|||
{:features features}
|
||||
{:id (:id team)})))
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
(binding [l/*context* (some-> (meta params)
|
||||
(get :app.http/request)
|
||||
(errors/request->context))]
|
||||
(-> (update-file cfg params)
|
||||
(-> (update-file* cfg params)
|
||||
(rph/with-defer #(let [elapsed (tpoint)]
|
||||
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))))
|
||||
|
||||
(defn update-file
|
||||
[{:keys [::mtx/metrics] :as cfg}
|
||||
{:keys [file features changes changes-with-metadata] :as params}]
|
||||
(let [features (-> features
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union (:features file)))
|
||||
|
||||
update-fn (cond-> update-file*
|
||||
(contains? features "fdata/pointer-map")
|
||||
(wrap-with-pointer-map-context))
|
||||
|
||||
changes (if changes-with-metadata
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
(vec changes))]
|
||||
|
||||
(when (> (:revn params)
|
||||
(:revn file))
|
||||
(ex/raise :type :validation
|
||||
:code :revn-conflict
|
||||
:hint "The incoming revision number is greater that stored version."
|
||||
:context {:incoming-revn (:revn params)
|
||||
:stored-revn (:revn file)}))
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
(binding [cfeat/*current* features
|
||||
cfeat/*previous* (:features file)]
|
||||
(let [file (assoc file :features features)
|
||||
params (-> params
|
||||
(assoc :file file)
|
||||
(assoc :changes changes)
|
||||
(assoc ::created-at (dt/now)))]
|
||||
|
||||
(-> (update-fn cfg params)
|
||||
(vary-meta assoc ::audit/replace-props
|
||||
{:id (:id file)
|
||||
:name (:name file)
|
||||
:features (:features file)
|
||||
:project-id (:project-id file)
|
||||
:team-id (:team-id file)}))))))
|
||||
|
||||
(defn- update-file*
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg}
|
||||
{:keys [profile-id file changes session-id ::created-at skip-validate] :as params}]
|
||||
"Internal function, part of the update-file process, that encapsulates
|
||||
the changes application offload to a separated thread and emit all
|
||||
corresponding notifications.
|
||||
|
||||
Follow the inner implementation to `update-file-data!` function.
|
||||
|
||||
Only intended for internal use on this module."
|
||||
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
|
||||
{:keys [profile-id file features changes session-id skip-validate] :as params}]
|
||||
|
||||
(let [;; Retrieve the file data
|
||||
file (feat.fdata/resolve-file-data cfg file)
|
||||
file (feat.fdata/resolve-file-data cfg file)
|
||||
|
||||
file (assoc file :features
|
||||
(-> features
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union (:features file))))
|
||||
|
||||
;; Process the file data on separated thread for avoid to do
|
||||
;; the CPU intensive operation on vthread.
|
||||
file (px/invoke! executor
|
||||
(fn []
|
||||
(binding [cfeat/*current* features
|
||||
cfeat/*previous* (:features file)]
|
||||
(update-file-data! cfg file
|
||||
process-changes-and-validate
|
||||
changes skip-validate))))]
|
||||
|
||||
file (px/invoke! executor (partial update-file-data cfg file changes skip-validate))
|
||||
features (db/create-array conn "text" (:features file))]
|
||||
|
||||
;; NOTE: if file was offloaded, we need to touch the referenced
|
||||
;; storage object because on this update operation the data will
|
||||
;; be overwritted.
|
||||
(when (= "objects-storage" (:data-backend file))
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
|
||||
(sto/touch-object! storage (:data-ref-id file))))
|
||||
|
||||
(db/insert! conn :file-change
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:version (:version file)
|
||||
:label (::snapshot-label file)
|
||||
:data (::snapshot-data file)
|
||||
:features (db/create-array conn "text" (:features file))
|
||||
:changes (blob/encode changes)}
|
||||
{::db/return-keys false})
|
||||
(some->> (:data-ref-id file) (sto/touch-object! storage))))
|
||||
|
||||
;; TODO: move this to asynchronous task
|
||||
(when (::snapshot-data file)
|
||||
(delete-old-snapshots! cfg file))
|
||||
|
||||
(persist-file! cfg file)
|
||||
|
||||
(let [params (assoc params :file file)
|
||||
response {:revn (:revn file)
|
||||
:lagged (get-lagged-changes conn params)}
|
||||
features (db/create-array conn "text" (:features file))]
|
||||
|
||||
;; Insert change (xlog)
|
||||
(db/insert! conn :file-change
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
:profile-id profile-id
|
||||
:created-at timestamp
|
||||
:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:version (:version file)
|
||||
:features features
|
||||
:label (::snapshot-label file)
|
||||
:data (::snapshot-data file)
|
||||
:changes (blob/encode changes)}
|
||||
{::db/return-keys false})
|
||||
|
||||
;; Send asynchronous notifications
|
||||
(send-notifications! cfg params)
|
||||
|
||||
(vary-meta response assoc ::audit/replace-props
|
||||
{:id (:id file)
|
||||
:name (:name file)
|
||||
:features (:features file)
|
||||
:project-id (:project-id file)
|
||||
:team-id (:team-id file)}))))
|
||||
|
||||
(defn update-file!
|
||||
"A public api that allows apply a transformation to a file with all context setup."
|
||||
[cfg file-id update-fn & args]
|
||||
(let [file (get-file cfg file-id)
|
||||
file (apply update-file-data! cfg file update-fn args)]
|
||||
(persist-file! cfg file)))
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.*, p.team_id
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?
|
||||
AND (f.deleted_at IS NULL OR
|
||||
f.deleted_at > now())
|
||||
FOR KEY SHARE")
|
||||
|
||||
(defn get-file
|
||||
"Get not-decoded file, only decodes the features set."
|
||||
[conn id]
|
||||
(let [file (db/exec-one! conn [sql:get-file id])]
|
||||
(when-not file
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint (format "file with id '%s' does not exists" id)))
|
||||
(update file :features db/decode-pgarray #{})))
|
||||
|
||||
(defn persist-file!
|
||||
"Function responsible of persisting already encoded file. Should be
|
||||
used together with `get-file` and `update-file-data!`.
|
||||
|
||||
It also updates the project modified-at attr."
|
||||
[{:keys [::db/conn ::timestamp]} file]
|
||||
(let [features (db/create-array conn "text" (:features file))
|
||||
;; The timestamp can be nil because this function is also
|
||||
;; intended to be used outside of this module
|
||||
modified-at (or timestamp (dt/now))]
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at modified-at}
|
||||
{:id (:project-id file)}
|
||||
{::db/return-keys false})
|
||||
|
||||
(db/update! conn :file
|
||||
{:revn (:revn file)
|
||||
:data (:data file)
|
||||
|
@ -268,20 +300,95 @@
|
|||
:features features
|
||||
:data-backend nil
|
||||
:data-ref-id nil
|
||||
:modified-at created-at
|
||||
:modified-at modified-at
|
||||
:has-media-trimmed false}
|
||||
{:id (:id file)})
|
||||
{:id (:id file)}
|
||||
{::db/return-keys false})))
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at created-at}
|
||||
{:id (:project-id file)})
|
||||
(defn- update-file-data!
|
||||
"Perform a file data transformation in with all update context setup.
|
||||
|
||||
(let [params (assoc params :file file)]
|
||||
;; Send asynchronous notifications
|
||||
(send-notifications! cfg params)
|
||||
This function expected not-decoded file and transformation function. Returns
|
||||
an encoded file.
|
||||
|
||||
{:revn (:revn file)
|
||||
:lagged (get-lagged-changes conn params)})))
|
||||
This function is not responsible of saving the file. It only saves
|
||||
fdata/pointer-map modified fragments."
|
||||
|
||||
[cfg {:keys [id] :as file} update-fn & args]
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)
|
||||
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(let [file (update file :data (fn [data]
|
||||
(-> data
|
||||
(blob/decode)
|
||||
(assoc :id (:id file)))))
|
||||
|
||||
;; For avoid unnecesary overhead of creating multiple pointers
|
||||
;; and handly internally with objects map in their worst
|
||||
;; case (when probably all shapes and all pointers will be
|
||||
;; readed in any case), we just realize/resolve them before
|
||||
;; applying the migration to the file
|
||||
file (if (fmg/need-migration? file)
|
||||
(-> file
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))
|
||||
file)
|
||||
|
||||
file (apply update-fn cfg file args)
|
||||
|
||||
;; TODO: reuse operations if file is migrated
|
||||
;; TODO: move encoding to a separated thread
|
||||
file (if (take-snapshot? file)
|
||||
(let [tpoint (dt/tpoint)
|
||||
snapshot (-> (:data file)
|
||||
(feat.fdata/process-pointers deref)
|
||||
(feat.fdata/process-objects (partial into {}))
|
||||
(blob/encode))
|
||||
elapsed (tpoint)
|
||||
label (str "internal/snapshot/" (:revn file))]
|
||||
|
||||
(l/trc :hint "take snapshot"
|
||||
:file-id (str (:id file))
|
||||
:revn (:revn file)
|
||||
:label label
|
||||
:elapsed (dt/format-duration elapsed))
|
||||
|
||||
(-> file
|
||||
(assoc ::snapshot-data snapshot)
|
||||
(assoc ::snapshot-label label)))
|
||||
file)
|
||||
|
||||
file (cond-> file
|
||||
(contains? cfeat/*current* "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map)
|
||||
|
||||
(contains? cfeat/*current* "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map)
|
||||
|
||||
:always
|
||||
(update :data blob/encode))]
|
||||
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
|
||||
file)))
|
||||
|
||||
(defn- get-file-libraries
|
||||
"A helper for preload file libraries, mainly used for perform file
|
||||
semantical and structural validation"
|
||||
[{:keys [::db/conn] :as cfg} file]
|
||||
(->> (files/get-file-libraries conn (:id file))
|
||||
(into [file] (map (fn [{:keys [id]}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
||||
pmap/*tracked* nil]
|
||||
;; We do not resolve the objects maps here
|
||||
;; because there is a lower probability that all
|
||||
;; shapes needed to be loded into memory, so we
|
||||
;; leeave it on lazy status
|
||||
(-> (files/get-file cfg id :migrate? false)
|
||||
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))))))
|
||||
(d/index-by :id)))
|
||||
|
||||
(defn- soft-validate-file-schema!
|
||||
[file]
|
||||
|
@ -298,68 +405,19 @@
|
|||
(l/error :hint "file validation error"
|
||||
:cause cause))))
|
||||
|
||||
(defn- update-file-data
|
||||
[{:keys [::db/conn] :as cfg} file changes skip-validate]
|
||||
(let [file (update file :data (fn [data]
|
||||
(-> data
|
||||
(blob/decode)
|
||||
(assoc :id (:id file)))))
|
||||
;; For avoid unnecesary overhead of creating multiple pointers
|
||||
;; and handly internally with objects map in their worst
|
||||
;; case (when probably all shapes and all pointers will be
|
||||
;; readed in any case), we just realize/resolve them before
|
||||
;; applying the migration to the file
|
||||
file (if (fmg/need-migration? file)
|
||||
(-> file
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))
|
||||
file)
|
||||
|
||||
;; WARNING: this ruins performance; maybe we need to find
|
||||
(defn- process-changes-and-validate
|
||||
[cfg file changes skip-validate]
|
||||
(let [;; WARNING: this ruins performance; maybe we need to find
|
||||
;; some other way to do general validation
|
||||
libs (when (and (or (contains? cf/flags :file-validation)
|
||||
(contains? cf/flags :soft-file-validation))
|
||||
(not skip-validate))
|
||||
(->> (files/get-file-libraries conn (:id file))
|
||||
(into [file] (map (fn [{:keys [id]}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
||||
pmap/*tracked* nil]
|
||||
;; We do not resolve the objects maps here
|
||||
;; because there is a lower probability that all
|
||||
;; shapes needed to be loded into memory, so we
|
||||
;; leeave it on lazy status
|
||||
(-> (files/get-file cfg id :migrate? false)
|
||||
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))))))
|
||||
(d/index-by :id)))
|
||||
|
||||
(get-file-libraries cfg file))
|
||||
|
||||
file (-> (files/check-version! file)
|
||||
(update :revn inc)
|
||||
(update :data cpc/process-changes changes)
|
||||
(update :data d/without-nils))
|
||||
|
||||
file (if (take-snapshot? file)
|
||||
(let [tpoint (dt/tpoint)
|
||||
snapshot (-> (:data file)
|
||||
(feat.fdata/process-pointers deref)
|
||||
(feat.fdata/process-objects (partial into {}))
|
||||
(blob/encode))
|
||||
elapsed (tpoint)
|
||||
label (str "internal/snapshot/" (:revn file))]
|
||||
|
||||
(l/trc :hint "take snapshot"
|
||||
:file-id (str (:id file))
|
||||
:revn (:revn file)
|
||||
:label label
|
||||
:elapsed (dt/format-duration elapsed))
|
||||
|
||||
(-> file
|
||||
(assoc ::snapshot-data snapshot)
|
||||
(assoc ::snapshot-label label)))
|
||||
file)]
|
||||
(update :data d/without-nils))]
|
||||
|
||||
(binding [pmap/*tracked* nil]
|
||||
(when (contains? cf/flags :soft-file-validation)
|
||||
|
@ -376,15 +434,7 @@
|
|||
(not skip-validate))
|
||||
(val/validate-file-schema! file)))
|
||||
|
||||
(cond-> file
|
||||
(contains? cfeat/*current* "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map)
|
||||
|
||||
(contains? cfeat/*current* "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map)
|
||||
|
||||
:always
|
||||
(update :data blob/encode))))
|
||||
file))
|
||||
|
||||
(defn- take-snapshot?
|
||||
"Defines the rule when file `data` snapshot should be saved."
|
||||
|
@ -426,8 +476,7 @@
|
|||
result (db/exec-one! conn [sql:delete-snapshots id last-date])]
|
||||
(l/trc :hint "delete old snapshots" :file-id (str id) :total (db/get-update-count result)))))
|
||||
|
||||
(def ^:private
|
||||
sql:lagged-changes
|
||||
(def ^:private sql:lagged-changes
|
||||
"select s.id, s.revn, s.file_id,
|
||||
s.session_id, s.changes
|
||||
from file_change as s
|
||||
|
|
|
@ -396,8 +396,8 @@
|
|||
|
||||
;; --- COMMAND: Clone Template
|
||||
|
||||
(defn- clone-template
|
||||
[cfg {:keys [project-id ::rpc/profile-id] :as params} template]
|
||||
(defn clone-template
|
||||
[cfg {:keys [project-id profile-id] :as params} template]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
|
||||
;; NOTE: the importation process performs some operations that
|
||||
;; are not very friendly with virtual threads, and for avoid
|
||||
|
@ -416,6 +416,7 @@
|
|||
(doseq [file-id result]
|
||||
(let [props (assoc props :id file-id)
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/profile-id profile-id)
|
||||
(assoc ::audit/name "create-file")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))))
|
||||
|
@ -437,7 +438,8 @@
|
|||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}]
|
||||
(let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
|
||||
_ (teams/check-edition-permissions! pool profile-id (:team-id project))
|
||||
template (tmpl/get-template-stream cfg template-id)]
|
||||
template (tmpl/get-template-stream cfg template-id)
|
||||
params (assoc params :profile-id profile-id)]
|
||||
|
||||
(when-not template
|
||||
(ex/raise :type :not-found
|
||||
|
|
|
@ -360,27 +360,31 @@
|
|||
[:map {:title "update-profile-props"}
|
||||
[:props [:map-of :keyword :any]]]))
|
||||
|
||||
(defn update-profile-props
|
||||
[{:keys [::db/conn] :as cfg} profile-id props]
|
||||
(let [profile (get-profile conn profile-id ::sql/for-update true)
|
||||
props (reduce-kv (fn [props k v]
|
||||
;; We don't accept namespaced keys
|
||||
(if (simple-ident? k)
|
||||
(if (nil? v)
|
||||
(dissoc props k)
|
||||
(assoc props k v))
|
||||
props))
|
||||
(:props profile)
|
||||
props)]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id})
|
||||
|
||||
(filter-props props)))
|
||||
|
||||
(sv/defmethod ::update-profile-props
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema:update-profile-props}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id props]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (get-profile conn profile-id ::sql/for-update true)
|
||||
props (reduce-kv (fn [props k v]
|
||||
;; We don't accept namespaced keys
|
||||
(if (simple-ident? k)
|
||||
(if (nil? v)
|
||||
(dissoc props k)
|
||||
(assoc props k v))
|
||||
props))
|
||||
(:props profile)
|
||||
props)]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id})
|
||||
|
||||
(filter-props props))))
|
||||
[cfg {:keys [::rpc/profile-id props]}]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(update-profile-props cfg profile-id props))))
|
||||
|
||||
;; --- MUTATION: Delete Profile
|
||||
|
||||
|
|
64
backend/src/app/setup/welcome_file.clj
Normal file
64
backend/src/app/setup/welcome_file.clj
Normal file
|
@ -0,0 +1,64 @@
|
|||
;; 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.setup.welcome-file
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.files-update :as fupdate]
|
||||
[app.rpc.commands.management :as management]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.templates :as tmpl]
|
||||
[app.worker :as-alias wrk]))
|
||||
|
||||
(def ^:private page-id #uuid "2c6952ee-d00e-8160-8004-d2250b7210cb")
|
||||
(def ^:private shape-id #uuid "765e9f82-c44e-802e-8004-d72a10b7b445")
|
||||
|
||||
(def ^:private update-path
|
||||
[:data :pages-index page-id :objects shape-id
|
||||
:content :children 0 :children 0 :children 0])
|
||||
|
||||
(def ^:private sql:mark-file-object-thumbnails-deleted
|
||||
"UPDATE file_tagged_object_thumbnail
|
||||
SET deleted_at = now()
|
||||
WHERE file_id = ?")
|
||||
|
||||
(def ^:private sql:mark-file-thumbnail-deleted
|
||||
"UPDATE file_thumbnail
|
||||
SET deleted_at = now()
|
||||
WHERE file_id = ?")
|
||||
|
||||
(defn- update-welcome-shape
|
||||
[_ file name]
|
||||
(let [text (str "Welcome to Penpot, " name "!")]
|
||||
(-> file
|
||||
(update-in update-path assoc :text text)
|
||||
(update-in [:data :pages-index page-id :objects shape-id] assoc :name "Welcome to Penpot!")
|
||||
(update-in [:data :pages-index page-id :objects shape-id] dissoc :position-data))))
|
||||
|
||||
(defn create-welcome-file
|
||||
[cfg {:keys [id fullname] :as profile}]
|
||||
(try
|
||||
(let [cfg (dissoc cfg ::db/conn)
|
||||
params {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)}
|
||||
template-stream (tmpl/get-template-stream cfg "welcome")
|
||||
file-id (-> (management/clone-template cfg params template-stream)
|
||||
first)]
|
||||
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(fupdate/update-file! cfg file-id update-welcome-shape fullname)
|
||||
(profile/update-profile-props cfg id {:welcome-file-id file-id})
|
||||
(db/exec-one! conn [sql:mark-file-object-thumbnails-deleted file-id])
|
||||
(db/exec-one! conn [sql:mark-file-thumbnail-deleted file-id]))))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error on create welcome file " :cause cause))))
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
:enable-feature-fdata-pointer-map
|
||||
:enable-feature-fdata-objets-map
|
||||
:enable-feature-components-v2
|
||||
:enable-file-snapshot
|
||||
:enable-auto-file-snapshot
|
||||
:disable-file-validation])
|
||||
|
||||
(defn state-init
|
||||
|
@ -304,16 +304,18 @@
|
|||
([params] (update-file* *system* params))
|
||||
([system {:keys [file-id changes session-id profile-id revn]
|
||||
:or {session-id (uuid/next) revn 0}}]
|
||||
(db/tx-run! system (fn [{:keys [::db/conn] :as system}]
|
||||
(let [file (files.update/get-file conn file-id)]
|
||||
(files.update/update-file system
|
||||
(-> system
|
||||
(assoc ::files.update/timestamp (dt/now))
|
||||
(db/tx-run! (fn [{:keys [::db/conn] :as system}]
|
||||
(let [file (files.update/get-file conn file-id)]
|
||||
(#'files.update/update-file* system
|
||||
{:id file-id
|
||||
:revn revn
|
||||
:file file
|
||||
:features (:features file)
|
||||
:changes changes
|
||||
:session-id session-id
|
||||
:profile-id profile-id}))))))
|
||||
:profile-id profile-id})))))))
|
||||
|
||||
(declare command!)
|
||||
|
||||
|
|
|
@ -190,10 +190,9 @@
|
|||
[:type [:= :del-color]]
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
;; DEPRECATED: remove before 2.3
|
||||
[:add-recent-color
|
||||
[:map {:title "AddRecentColorChange"}
|
||||
[:type [:= :add-recent-color]]
|
||||
[:color ::ctc/recent-color]]]
|
||||
[:map {:title "AddRecentColorChange"}]]
|
||||
|
||||
[:add-media
|
||||
[:map {:title "AddMediaChange"}
|
||||
|
@ -656,18 +655,10 @@
|
|||
[data {:keys [id]}]
|
||||
(ctcl/delete-color data id))
|
||||
|
||||
;; DEPRECATED: remove before 2.3
|
||||
(defmethod process-change :add-recent-color
|
||||
[data {:keys [color]}]
|
||||
;; Moves the color to the top of the list and then truncates up to 15
|
||||
(update
|
||||
data
|
||||
:recent-colors
|
||||
(fn [rc]
|
||||
(let [rc (->> rc (d/removev (partial ctc/eq-recent-color? color)))
|
||||
rc (-> rc (conj color))]
|
||||
(cond-> rc
|
||||
(> (count rc) 15)
|
||||
(subvec 1))))))
|
||||
[data _]
|
||||
data)
|
||||
|
||||
;; -- Media
|
||||
|
||||
|
|
|
@ -607,13 +607,6 @@
|
|||
(reduce resize-parent changes all-parents)))
|
||||
|
||||
;; Library changes
|
||||
|
||||
(defn add-recent-color
|
||||
[changes color]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-recent-color :color color})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn add-color
|
||||
[changes color]
|
||||
(-> changes
|
||||
|
|
|
@ -107,17 +107,16 @@
|
|||
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
||||
|
||||
(sm/register! ::rgb-color type:rgb-color)
|
||||
|
||||
(sm/register! ::color schema:color)
|
||||
(sm/register! ::gradient schema:gradient)
|
||||
(sm/register! ::image-color schema:image-color)
|
||||
(sm/register! ::recent-color schema:recent-color)
|
||||
|
||||
(def check-color!
|
||||
(sm/check-fn schema:color))
|
||||
(def valid-color?
|
||||
(sm/lazy-validator schema:color))
|
||||
|
||||
(def check-recent-color!
|
||||
(sm/check-fn schema:recent-color))
|
||||
(def valid-recent-color?
|
||||
(sm/lazy-validator schema:recent-color))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
|
@ -392,13 +391,22 @@
|
|||
|
||||
(process-shape-colors shape sync-color)))
|
||||
|
||||
(defn eq-recent-color?
|
||||
(defn- eq-recent-color?
|
||||
[c1 c2]
|
||||
(or (= c1 c2)
|
||||
(and (some? (:color c1))
|
||||
(some? (:color c2))
|
||||
(= (:color c1) (:color c2)))))
|
||||
|
||||
(defn add-recent-color
|
||||
"Moves the color to the top of the list and then truncates up to 15"
|
||||
[state file-id color]
|
||||
(update state file-id (fn [colors]
|
||||
(let [colors (d/removev (partial eq-recent-color? color) colors)
|
||||
colors (conj colors color)]
|
||||
(cond-> colors
|
||||
(> (count colors) 15)
|
||||
(subvec 1))))))
|
||||
|
||||
(defn stroke->color-att
|
||||
[stroke file-id shared-libs]
|
||||
|
|
|
@ -21,10 +21,12 @@
|
|||
[app.main.repo :as rp]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.storage :as s]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(declare update-profile-props)
|
||||
|
||||
;; --- SCHEMAS
|
||||
|
||||
(def ^:private
|
||||
|
@ -49,14 +51,14 @@
|
|||
|
||||
(defn get-current-team-id
|
||||
[profile]
|
||||
(let [team-id (::current-team-id @storage)]
|
||||
(let [team-id (::current-team-id @s/storage)]
|
||||
(or team-id (:default-team-id profile))))
|
||||
|
||||
(defn set-current-team!
|
||||
[team-id]
|
||||
(if (nil? team-id)
|
||||
(swap! storage dissoc ::current-team-id)
|
||||
(swap! storage assoc ::current-team-id team-id)))
|
||||
(swap! s/storage dissoc ::current-team-id)
|
||||
(swap! s/storage assoc ::current-team-id team-id)))
|
||||
|
||||
;; --- EVENT: fetch-teams
|
||||
|
||||
|
@ -76,9 +78,9 @@
|
|||
;; if not, dissoc it from storage.
|
||||
|
||||
(let [ids (into #{} (map :id) teams)]
|
||||
(when-let [ctid (::current-team-id @storage)]
|
||||
(when-let [ctid (::current-team-id @s/storage)]
|
||||
(when-not (contains? ids ctid)
|
||||
(swap! storage dissoc ::current-team-id)))))))
|
||||
(swap! s/storage dissoc ::current-team-id)))))))
|
||||
|
||||
(defn fetch-teams
|
||||
[]
|
||||
|
@ -129,10 +131,10 @@
|
|||
(effect [_ state _]
|
||||
(let [profile (:profile state)
|
||||
email (:email profile)
|
||||
previous-profile (:profile @storage)
|
||||
previous-profile (:profile @s/storage)
|
||||
previous-email (:email previous-profile)]
|
||||
(when profile
|
||||
(swap! storage assoc :profile profile)
|
||||
(swap! s/storage assoc :profile profile)
|
||||
(i18n/set-locale! (:lang profile))
|
||||
(when (not= previous-email email)
|
||||
(set-current-team! nil)))))))
|
||||
|
@ -152,9 +154,15 @@
|
|||
profile. The profile can proceed from standard login or from
|
||||
accepting invitation, or third party auth signup or singin."
|
||||
[profile]
|
||||
(letfn [(get-redirect-event []
|
||||
(let [team-id (get-current-team-id profile)]
|
||||
(rt/nav' :dashboard-projects {:team-id team-id})))]
|
||||
(letfn [(get-redirect-events []
|
||||
(let [team-id (get-current-team-id profile)
|
||||
welcome-file-id (get-in profile [:props :welcome-file-id])]
|
||||
(if (some? welcome-file-id)
|
||||
(rx/of
|
||||
(rt/nav' :workspace {:project-id (:default-project-id profile)
|
||||
:file-id welcome-file-id})
|
||||
(update-profile-props {:welcome-file-id nil}))
|
||||
(rx/of (rt/nav' :dashboard-projects {:team-id team-id})))))]
|
||||
|
||||
(ptk/reify ::logged-in
|
||||
ev/Event
|
||||
|
@ -171,10 +179,11 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(when (is-authenticated? profile)
|
||||
(->> (rx/of (profile-fetched profile)
|
||||
(fetch-teams)
|
||||
(get-redirect-event)
|
||||
(ws/initialize))
|
||||
(->> (rx/concat
|
||||
(rx/of (profile-fetched profile)
|
||||
(fetch-teams)
|
||||
(ws/initialize))
|
||||
(get-redirect-events))
|
||||
(rx/observe-on :async)))))))
|
||||
|
||||
(declare login-from-register)
|
||||
|
@ -311,7 +320,7 @@
|
|||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
;; We prefer to keek some stuff in the storage like the current-team-id and the profile
|
||||
(set-current-team! nil)))))
|
||||
(swap! s/storage (constantly {}))))))
|
||||
|
||||
(defn logout
|
||||
([] (logout {}))
|
||||
|
|
|
@ -79,6 +79,7 @@
|
|||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
|
@ -335,6 +336,7 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state
|
||||
:recent-colors (:recent-colors @storage)
|
||||
:workspace-ready? false
|
||||
:current-file-id file-id
|
||||
:current-project-id project-id
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :as s]
|
||||
[app.util.time :as dt]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -132,16 +133,21 @@
|
|||
|
||||
(defn add-recent-color
|
||||
[color]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid recent color map"
|
||||
(ctc/check-recent-color! color))
|
||||
(ctc/valid-recent-color? color))
|
||||
|
||||
(ptk/reify ::add-recent-color
|
||||
ptk/WatchEvent
|
||||
(watch [it _ _]
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-recent-color color))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [file-id (:current-file-id state)]
|
||||
(update state :recent-colors ctc/add-recent-color file-id color)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [recent-colors (:recent-colors state)]
|
||||
(swap! s/storage assoc :recent-colors recent-colors)))))
|
||||
|
||||
(def clear-color-for-rename
|
||||
(ptk/reify ::clear-color-for-rename
|
||||
|
@ -168,8 +174,11 @@
|
|||
|
||||
(dm/assert!
|
||||
"expected valid parameters"
|
||||
(and (ctc/check-color! color)
|
||||
(uuid? file-id)))
|
||||
(ctc/valid-color? color))
|
||||
|
||||
(dm/assert!
|
||||
"expected file-id"
|
||||
(uuid? file-id))
|
||||
|
||||
(ptk/reify ::update-color
|
||||
ptk/WatchEvent
|
||||
|
|
|
@ -236,9 +236,10 @@
|
|||
=))
|
||||
|
||||
(def workspace-recent-colors
|
||||
(l/derived (fn [data]
|
||||
(get data :recent-colors []))
|
||||
workspace-data))
|
||||
(l/derived (fn [state]
|
||||
(when-let [file-id (:current-file-id state)]
|
||||
(dm/get-in state [:recent-colors file-id])))
|
||||
st/state))
|
||||
|
||||
(def workspace-recent-fonts
|
||||
(l/derived (fn [data]
|
||||
|
|
|
@ -44,7 +44,30 @@
|
|||
(mf/defc main-page
|
||||
{::mf/props :obj}
|
||||
[{:keys [route profile]}]
|
||||
(let [{:keys [data params]} route]
|
||||
(let [{:keys [data params]} route
|
||||
props (get profile :props)
|
||||
show-question-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-questions)))
|
||||
|
||||
show-newsletter-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :newsletter-updates))
|
||||
(contains? props :onboarding-questions))
|
||||
|
||||
show-team-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-team-id))
|
||||
(contains? props :newsletter-updates))
|
||||
|
||||
show-release-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(:onboarding-viewed props)
|
||||
(not= (:release-notes-viewed props) (:main cf/version))
|
||||
(not= "0.0" (:main cf/version)))]
|
||||
[:& (mf/provider ctx/current-route) {:value route}
|
||||
(case (:name data)
|
||||
(:auth-login
|
||||
|
@ -84,42 +107,19 @@
|
|||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
|
||||
(when-let [props (get profile :props)]
|
||||
(let [show-question-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-questions)))
|
||||
|
||||
show-newsletter-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :newsletter-updates))
|
||||
(contains? props :onboarding-questions))
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-team-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-team-id))
|
||||
(contains? props :newsletter-updates))
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
|
||||
show-release-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(:onboarding-viewed props)
|
||||
(not= (:release-notes-viewed props) (:main cf/version))
|
||||
(not= "0.0" (:main cf/version)))]
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal {:go-to-team? true}]
|
||||
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal]
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}])))
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}])
|
||||
|
||||
[:& dashboard-page {:route route :profile profile}]]
|
||||
:viewer
|
||||
|
@ -154,6 +154,20 @@
|
|||
page-id (some-> params :query :page-id uuid)
|
||||
layout (some-> params :query :layout keyword)]
|
||||
[:? {}
|
||||
(when (cf/external-feature-flag "onboarding-03" "test")
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal {:go-to-team? false}]
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}]))
|
||||
|
||||
[:& workspace-page {:project-id project-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
|
|
|
@ -39,7 +39,8 @@
|
|||
form (fm/use-form :schema schema:register-form
|
||||
:initial initial)
|
||||
|
||||
submitted? (mf/use-state false)
|
||||
submitted?
|
||||
(mf/use-state false)
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
|
@ -176,7 +177,9 @@
|
|||
::mf/private true}
|
||||
[{:keys [params on-success-callback]}]
|
||||
(let [form (fm/use-form :schema schema:register-validate-form :initial params)
|
||||
submitted? (mf/use-state false)
|
||||
|
||||
submitted?
|
||||
(mf/use-state false)
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
|
@ -208,7 +211,13 @@
|
|||
(mf/deps on-success on-error)
|
||||
(fn [form _]
|
||||
(reset! submitted? true)
|
||||
(let [params (:clean-data @form)]
|
||||
(let [create-welcome-file?
|
||||
(cf/external-feature-flag "onboarding-03" "test")
|
||||
|
||||
params
|
||||
(cond-> (:clean-data @form)
|
||||
create-welcome-file? (assoc :create-welcome-file true))]
|
||||
|
||||
(->> (rp/cmd! :register-profile params)
|
||||
(rx/finalize #(reset! submitted? false))
|
||||
(rx/subs! on-success on-error)))))]
|
||||
|
|
|
@ -519,8 +519,10 @@
|
|||
@include bodySmallTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.custom-input-checkbox {
|
||||
// TODO: This fix is temporary, the error is caused by the
|
||||
// cascading order of the compiled css files.
|
||||
// https://tree.taiga.io/project/penpot/task/8658
|
||||
.custom-input-checkbox.custom-input-checkbox {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
|
|
@ -168,7 +168,9 @@
|
|||
[{:keys [default-project-id profile project-id team-id]}]
|
||||
(let [templates (mf/deref builtin-templates)
|
||||
templates (mf/with-memo [templates]
|
||||
(filterv #(not= (:id %) "tutorial-for-beginners") templates))
|
||||
(filterv #(and
|
||||
(not= (:id %) "welcome")
|
||||
(not= (:id %) "tutorial-for-beginners")) templates))
|
||||
|
||||
route (mf/deref refs/route)
|
||||
route-name (get-in route [:data :name])
|
||||
|
|
|
@ -294,19 +294,21 @@
|
|||
`key` for new values."
|
||||
[key default]
|
||||
(let [id (mf/use-id)
|
||||
state (mf/use-state (get @storage key default))
|
||||
state* (mf/use-state #(get @storage key default))
|
||||
state (deref state*)
|
||||
stream (mf/with-memo [id]
|
||||
(->> mbc/stream
|
||||
(rx/filter #(not= (:id %) id))
|
||||
(rx/filter #(= (:type %) key))
|
||||
(rx/map deref)))]
|
||||
|
||||
(mf/with-effect [@state key id]
|
||||
(mbc/emit! id key @state)
|
||||
(swap! storage assoc key @state))
|
||||
(mf/with-effect [state key id]
|
||||
(mbc/emit! id key state)
|
||||
(swap! storage assoc key state))
|
||||
|
||||
(use-stream stream (partial reset! state))
|
||||
state))
|
||||
(use-stream stream (partial reset! state*))
|
||||
|
||||
state*))
|
||||
|
||||
(defonce ^:private intersection-subject (rx/subject))
|
||||
(defonce ^:private intersection-observer
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.hooks.resize
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.logging :as log]
|
||||
|
@ -20,6 +21,15 @@
|
|||
|
||||
(def last-resize-type nil)
|
||||
|
||||
(defn- get-initial-state
|
||||
[initial file-id key]
|
||||
(let [saved (dm/get-in @storage [::state file-id key])]
|
||||
(d/nilv saved initial)))
|
||||
|
||||
(defn- update-persistent-state
|
||||
[data file-id key size]
|
||||
(update-in data [::state file-id] assoc key size))
|
||||
|
||||
(defn set-resize-type! [type]
|
||||
(set! last-resize-type type))
|
||||
|
||||
|
@ -28,26 +38,28 @@
|
|||
(use-resize-hook key initial min-val max-val axis negate? resize-type nil))
|
||||
|
||||
([key initial min-val max-val axis negate? resize-type on-change-size]
|
||||
(let [current-file-id (mf/use-ctx ctx/current-file-id)
|
||||
size-state (mf/use-state (or (get-in @storage [::saved-resize current-file-id key]) initial))
|
||||
parent-ref (mf/use-ref nil)
|
||||
(let [file-id (mf/use-ctx ctx/current-file-id)
|
||||
|
||||
dragging-ref (mf/use-ref false)
|
||||
current-size* (mf/use-state #(get-initial-state initial file-id key))
|
||||
current-size (deref current-size*)
|
||||
|
||||
parent-ref (mf/use-ref nil)
|
||||
dragging-ref (mf/use-ref false)
|
||||
start-size-ref (mf/use-ref nil)
|
||||
start-ref (mf/use-ref nil)
|
||||
start-ref (mf/use-ref nil)
|
||||
|
||||
on-pointer-down
|
||||
(mf/use-callback
|
||||
(mf/deps @size-state)
|
||||
(mf/use-fn
|
||||
(mf/deps current-size)
|
||||
(fn [event]
|
||||
(dom/capture-pointer event)
|
||||
(mf/set-ref-val! start-size-ref @size-state)
|
||||
(mf/set-ref-val! start-size-ref current-size)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(mf/set-ref-val! start-ref (dom/get-client-position event))
|
||||
(set! last-resize-type resize-type)))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
(mf/set-ref-val! start-size-ref nil)
|
||||
|
@ -56,40 +68,39 @@
|
|||
(set! last-resize-type nil)))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-callback
|
||||
(mf/deps min-val max-val negate?)
|
||||
(mf/use-fn
|
||||
(mf/deps min-val max-val negate? file-id key)
|
||||
(fn [event]
|
||||
(when (mf/ref-val dragging-ref)
|
||||
(let [start (mf/ref-val start-ref)
|
||||
pos (dom/get-client-position event)
|
||||
pos (dom/get-client-position event)
|
||||
delta (-> (gpt/to-vec start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
|
||||
start-size (mf/ref-val start-size-ref)
|
||||
new-size (-> (+ start-size delta) (max min-val) (min max-val))]
|
||||
(reset! size-state new-size)
|
||||
(swap! storage assoc-in [::saved-resize current-file-id key] new-size)
|
||||
(when on-change-size (on-change-size new-size))))))
|
||||
(reset! current-size* new-size)
|
||||
(swap! storage update-persistent-state file-id key new-size)))))
|
||||
|
||||
set-size
|
||||
(mf/use-callback
|
||||
(mf/deps on-change-size)
|
||||
(mf/use-fn
|
||||
(mf/deps on-change-size file-id key)
|
||||
(fn [new-size]
|
||||
(let [new-size (mth/clamp new-size min-val max-val)]
|
||||
(reset! size-state new-size)
|
||||
(swap! storage assoc-in [::saved-resize current-file-id key] new-size)
|
||||
(when on-change-size (on-change-size new-size)))))]
|
||||
(reset! current-size* new-size)
|
||||
(swap! storage update-persistent-state file-id key new-size))))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(when on-change-size (on-change-size @size-state))))
|
||||
(mf/with-effect [on-change-size current-size]
|
||||
(when on-change-size
|
||||
(on-change-size current-size)))
|
||||
|
||||
{:on-pointer-down on-pointer-down
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:on-pointer-move on-pointer-move
|
||||
:parent-ref parent-ref
|
||||
:set-size set-size
|
||||
:size @size-state})))
|
||||
:size current-size})))
|
||||
|
||||
(defn use-resize-observer
|
||||
[callback]
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
|
||||
(mf/defc team-form-step-2
|
||||
{::mf/props :obj}
|
||||
[{:keys [name on-back]}]
|
||||
[{:keys [name on-back go-to-team?]}]
|
||||
(let [initial (mf/use-memo
|
||||
#(do {:role "editor"
|
||||
:name name}))
|
||||
|
@ -85,7 +85,8 @@
|
|||
(let [team-id (:id response)]
|
||||
(st/emit! (du/update-profile-props {:onboarding-team-id team-id
|
||||
:onboarding-viewed true})
|
||||
(rt/nav :dashboard-projects {:team-id team-id})))))
|
||||
(when go-to-team?
|
||||
(rt/nav :dashboard-projects {:team-id team-id}))))))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
|
@ -240,7 +241,7 @@
|
|||
|
||||
(mf/defc onboarding-team-modal
|
||||
{::mf/props :obj}
|
||||
[]
|
||||
[{:keys [go-to-team?]}]
|
||||
(let [name* (mf/use-state nil)
|
||||
name (deref name*)
|
||||
|
||||
|
@ -262,6 +263,6 @@
|
|||
[:& left-sidebar]
|
||||
[:div {:class (stl/css :separator)}]
|
||||
(if name
|
||||
[:& team-form-step-2 {:name name :on-back on-back}]
|
||||
[:& team-form-step-2 {:name name :on-back on-back :go-to-team? go-to-team?}]
|
||||
[:& team-form-step-1 {:on-submit on-submit}])]]))
|
||||
|
||||
|
|
|
@ -6,42 +6,80 @@
|
|||
|
||||
(ns app.util.storage
|
||||
(:require
|
||||
["lodash/debounce" :as ldebounce]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.transit :as t]
|
||||
[app.util.globals :as g]
|
||||
[app.util.timers :as tm]))
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn- persist
|
||||
[storage prev curr]
|
||||
(run! (fn [key]
|
||||
(let [prev* (get prev key)
|
||||
curr* (get curr key)]
|
||||
(when (not= curr* prev*)
|
||||
(tm/schedule-on-idle
|
||||
#(if (some? curr*)
|
||||
(.setItem ^js storage (t/encode-str key) (t/encode-str curr*))
|
||||
(.removeItem ^js storage (t/encode-str key)))))))
|
||||
;; Using ex/ignoring because can receive a DOMException like this when
|
||||
;; importing the code as a library: Failed to read the 'localStorage'
|
||||
;; property from 'Window': Storage is disabled inside 'data:' URLs.
|
||||
(defonce ^:private local-storage
|
||||
(ex/ignoring (unchecked-get g/global "localStorage")))
|
||||
|
||||
(into #{} (concat (keys curr)
|
||||
(keys prev)))))
|
||||
(defn- encode-key
|
||||
[k]
|
||||
(assert (keyword? k) "key must be keyword")
|
||||
(let [kns (namespace k)
|
||||
kn (name k)]
|
||||
(str "penpot:" kns "/" kn)))
|
||||
|
||||
(defn- decode-key
|
||||
[k]
|
||||
(when (str/starts-with? k "penpot:")
|
||||
(let [k (subs k 7)]
|
||||
(if (str/starts-with? k "/")
|
||||
(keyword (subs k 1))
|
||||
(let [[kns kn] (str/split k "/" 2)]
|
||||
(keyword kns kn))))))
|
||||
|
||||
(defn- lookup-by-index
|
||||
[result index]
|
||||
(try
|
||||
(let [key (.key ^js local-storage index)
|
||||
key' (decode-key key)]
|
||||
(if key'
|
||||
(let [val (.getItem ^js local-storage key)]
|
||||
(assoc! result key' (t/decode-str val)))
|
||||
result))
|
||||
(catch :default _
|
||||
result)))
|
||||
|
||||
(defn- load
|
||||
[storage]
|
||||
(when storage
|
||||
(let [len (.-length ^js storage)]
|
||||
(reduce (fn [res index]
|
||||
(let [key (.key ^js storage index)
|
||||
val (.getItem ^js storage key)]
|
||||
(try
|
||||
(assoc res (t/decode-str key) (t/decode-str val))
|
||||
(catch :default _e
|
||||
res))))
|
||||
{}
|
||||
(range len)))))
|
||||
[]
|
||||
(when (some? local-storage)
|
||||
(let [length (.-length ^js local-storage)]
|
||||
(loop [index 0
|
||||
result (transient {})]
|
||||
(if (< index length)
|
||||
(recur (inc index)
|
||||
(lookup-by-index result index))
|
||||
(persistent! result))))))
|
||||
|
||||
;; Using ex/ignoring because can receive a DOMException like this when importing the code as a library:
|
||||
;; Failed to read the 'localStorage' property from 'Window': Storage is disabled inside 'data:' URLs.
|
||||
(defonce storage (atom (load (ex/ignoring (unchecked-get g/global "localStorage")))))
|
||||
(defonce ^:private latest-state (load))
|
||||
|
||||
(add-watch storage :persistence #(persist js/localStorage %3 %4))
|
||||
(defn- on-change*
|
||||
[curr-state]
|
||||
(let [prev-state latest-state]
|
||||
(try
|
||||
(run! (fn [key]
|
||||
(let [prev-val (get prev-state key)
|
||||
curr-val (get curr-state key)]
|
||||
(when-not (identical? curr-val prev-val)
|
||||
(if (some? curr-val)
|
||||
(.setItem ^js local-storage (encode-key key) (t/encode-str curr-val))
|
||||
(.removeItem ^js local-storage (encode-key key))))))
|
||||
(into #{} (concat (keys curr-state)
|
||||
(keys prev-state))))
|
||||
(finally
|
||||
(set! latest-state curr-state)))))
|
||||
|
||||
(defonce on-change
|
||||
(ldebounce on-change* 2000 #js {:leading false :trailing true}))
|
||||
|
||||
|
||||
(defonce storage (atom latest-state))
|
||||
(add-watch storage :persistence
|
||||
(fn [_ _ _ curr-state]
|
||||
(on-change curr-state)))
|
||||
|
|
Loading…
Add table
Reference in a new issue