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

This commit is contained in:
Andrey Antukh 2024-01-05 09:46:53 +01:00
commit 9aff12f3c6
290 changed files with 15286 additions and 31285 deletions

1
.gitignore vendored
View File

@ -23,6 +23,7 @@
/*.jpg
/*.md
/*.png
/*.svg
/*.sql
/*.txt
/*.yml

View File

@ -26,7 +26,7 @@
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.894"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.909"}
metosin/reitit-core {:mvn/version "0.6.0"}
nrepl/nrepl {:mvn/version "1.1.0"}
cider/cider-nrepl {:mvn/version "0.43.1"}

View File

@ -8,6 +8,7 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
[app.common.fressian :as fres]
[app.common.geom.matrix :as gmt]
[app.common.logging :as l]
@ -136,3 +137,12 @@
(add-tap #(locking debug-tap
(prn "tap debug:" %)))
1))
(defn calculate-frames
[{:keys [data]}]
(->> (vals (:pages-index data))
(mapcat (comp vals :objects))
(filter cfh/is-direct-child-of-root?)
(filter cfh/frame-shape?)
(count)))

View File

@ -207,6 +207,7 @@
(s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string)
(s/def ::svgo-max-procs ::us/integer)
(s/def ::config
(s/keys :opt-un [::secret-key
@ -326,7 +327,9 @@
::telemetry-uri
::telemetry-referer
::telemetry-with-taiga
::tenant]))
::tenant
::svgo-max-procs]))
(def default-flags
[:enable-backend-api-doc

View File

@ -19,6 +19,7 @@
[app.util.json :as json]
[app.util.time :as dt]
[clojure.java.io :as io]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[next.jdbc :as jdbc]
@ -239,6 +240,10 @@
(ex/raise :type :internal
:code :unable-resolve-pool))))
(defn get-update-count
[result]
(:next.jdbc/update-count result))
(defn get-connection
[cfg-or-conn]
(if (connection? cfg-or-conn)
@ -265,48 +270,120 @@
:code :unable-resolve-connectable
:hint "expected conn, pool or system")))
(def ^:private params-mapping
{::return-keys? :return-keys
::return-keys :return-keys})
(defn rename-opts
[opts]
(set/rename-keys opts params-mapping))
(def ^:private default-insert-opts
{:builder-fn sql/as-kebab-maps
:return-keys true})
(def ^:private default-opts
{:builder-fn sql/as-kebab-maps})
(defn exec!
([ds sv]
(-> (get-connectable ds)
(jdbc/execute! sv default-opts)))
([ds sv] (exec! ds sv nil))
([ds sv opts]
(-> (get-connectable ds)
(jdbc/execute! sv (into default-opts (sql/adapt-opts opts))))))
(let [conn (get-connectable ds)
opts (if (empty? opts)
default-opts
(into default-opts (rename-opts opts)))]
(jdbc/execute! conn sv opts))))
(defn exec-one!
([ds sv]
(-> (get-connectable ds)
(jdbc/execute-one! sv default-opts)))
([ds sv] (exec-one! ds sv nil))
([ds sv opts]
(-> (get-connectable ds)
(jdbc/execute-one! sv (into default-opts (sql/adapt-opts opts))))))
(let [conn (get-connectable ds)
opts (if (empty? opts)
default-opts
(into default-opts (rename-opts opts)))]
(jdbc/execute-one! conn sv opts))))
(defn insert!
[ds table params & {:as opts :keys [::return-keys?] :or {return-keys? true}}]
(-> (get-connectable ds)
(exec-one! (sql/insert table params opts)
(assoc opts ::return-keys? return-keys?))))
"A helper that builds an insert sql statement and executes it. By
default returns the inserted row with all the field; you can delimit
the returned columns with the `::columns` option."
[ds table params & {:as opts}]
(let [conn (get-connectable ds)
sql (sql/insert table params opts)
opts (if (empty? opts)
default-insert-opts
(into default-insert-opts (rename-opts opts)))]
(jdbc/execute-one! conn sql opts)))
(defn insert-multi!
[ds table cols rows & {:as opts :keys [::return-keys?] :or {return-keys? true}}]
(-> (get-connectable ds)
(exec! (sql/insert-multi table cols rows opts)
(assoc opts ::return-keys? return-keys?))))
(defn insert-many!
"An optimized version of `insert!` that perform insertion of multiple
values at once.
This expands to a single SQL statement with placeholders for every
value being inserted. For large data sets, this may exceed the limit
of sql string size and/or number of parameters."
[ds table cols rows & {:as opts}]
(let [conn (get-connectable ds)
sql (sql/insert-many table cols rows opts)
opts (if (empty? opts)
default-insert-opts
(into default-insert-opts (rename-opts opts)))
opts (update opts :return-keys boolean)]
(jdbc/execute! conn sql opts)))
(defn update!
[ds table params where & {:as opts :keys [::return-keys?] :or {return-keys? true}}]
(-> (get-connectable ds)
(exec-one! (sql/update table params where opts)
(assoc opts ::return-keys? return-keys?))))
"A helper that build an UPDATE SQL statement and executes it.
Given a connectable object, a table name, a hash map of columns and
values to set, and either a hash map of columns and values to search
on or a vector of a SQL where clause and parameters, perform an
update on the table.
By default returns an object with the number of affected rows; a
complete row can be returned if you pass `::return-keys` with `true`
or with a vector of columns.
Also it can be combined with the `::many` option if you perform an
update to multiple rows and you want all the affected rows to be
returned."
[ds table params where & {:as opts}]
(let [conn (get-connectable ds)
sql (sql/update table params where opts)
opts (if (empty? opts)
default-opts
(into default-opts (rename-opts opts)))
opts (update opts :return-keys boolean)]
(if (::many opts)
(jdbc/execute! conn sql opts)
(jdbc/execute-one! conn sql opts))))
(defn delete!
[ds table params & {:as opts :keys [::return-keys?] :or {return-keys? true}}]
(-> (get-connectable ds)
(exec-one! (sql/delete table params opts)
(assoc opts ::return-keys? return-keys?))))
"A helper that builds an DELETE SQL statement and executes it.
Given a connectable object, a table name, and either a hash map of columns
and values to search on or a vector of a SQL where clause and parameters,
perform a delete on the table.
By default returns an object with the number of affected rows; a
complete row can be returned if you pass `::return-keys` with `true`
or with a vector of columns.
Also it can be combined with the `::many` option if you perform an
update to multiple rows and you want all the affected rows to be
returned."
[ds table params & {:as opts}]
(let [conn (get-connectable ds)
sql (sql/delete table params opts)
opts (if (empty? opts)
default-opts
(into default-opts (rename-opts opts)))]
(if (::many opts)
(jdbc/execute! conn sql opts)
(jdbc/execute-one! conn sql opts))))
(defn query
[ds table params & {:as opts}]
(exec! ds (sql/select table params opts) opts))
(defn is-row-deleted?
[{:keys [deleted-at]}]
@ -320,7 +397,7 @@
[ds table params & {:as opts}]
(let [rows (exec! ds (sql/select table params opts))
rows (cond->> rows
(::remove-deleted? opts true)
(::remove-deleted opts true)
(remove is-row-deleted?))]
(first rows)))
@ -329,7 +406,7 @@
filters. Raises :not-found exception if no object is found."
[ds table params & {:as opts}]
(let [row (get* ds table params opts)]
(when (and (not row) (::check-deleted? opts true))
(when (and (not row) (::check-deleted opts true))
(ex/raise :type :not-found
:code :object-not-found
:table table
@ -341,14 +418,29 @@
(-> (get-connectable ds)
(jdbc/plan sql sql/default-opts)))
(defn cursor
"Return a lazy seq of rows using server side cursors"
[conn query & {:keys [chunk-size] :or {chunk-size 25}}]
(let [cname (str (gensym "cursor_"))
fquery [(str "FETCH " chunk-size " FROM " cname)]]
;; declare cursor
(exec-one! conn
(if (vector? query)
(into [(str "DECLARE " cname " CURSOR FOR " (nth query 0))]
(rest query))
[(str "DECLARE " cname " CURSOR FOR " query)]))
;; return a lazy seq
((fn fetch-more []
(lazy-seq
(when-let [chunk (seq (exec! conn fquery))]
(concat chunk (fetch-more))))))))
(defn get-by-id
[ds table id & {:as opts}]
(get ds table {:id id} opts))
(defn query
[ds table params & {:as opts}]
(exec! ds (sql/select table params opts)))
(defn pgobject?
([v]
(instance? PGobject v))
@ -548,11 +640,6 @@
(.setType "jsonb")
(.setValue (json/encode-str data)))))
(defn get-update-count
[result]
(:next.jdbc/update-count result))
;; --- Locks
(def ^:private siphash-state

View File

@ -8,7 +8,6 @@
(:refer-clojure :exclude [update])
(:require
[app.db :as-alias db]
[clojure.set :as set]
[clojure.string :as str]
[next.jdbc.optional :as jdbc-opt]
[next.jdbc.sql.builder :as sql]))
@ -20,14 +19,6 @@
{:table-fn snake-case
:column-fn snake-case})
(def params-mapping
{::db/return-keys? :return-keys
::db/columns :columns})
(defn adapt-opts
[opts]
(set/rename-keys opts params-mapping))
(defn as-kebab-maps
[rs opts]
(jdbc-opt/as-unqualified-modified-maps rs (assoc opts :label-fn kebab-case)))
@ -42,7 +33,7 @@
(assoc :suffix "ON CONFLICT DO NOTHING"))]
(sql/for-insert table key-map opts))))
(defn insert-multi
(defn insert-many
[table cols rows opts]
(let [opts (merge default-opts opts)]
(sql/for-insert-multi table cols rows opts)))
@ -53,11 +44,9 @@
([table where-params opts]
(let [opts (merge default-opts opts)
opts (cond-> opts
(::db/columns opts) (assoc :columns (::db/columns opts))
(::db/for-update? opts) (assoc :suffix "FOR UPDATE")
(::db/for-share? opts) (assoc :suffix "FOR KEY SHARE")
(:for-update opts) (assoc :suffix "FOR UPDATE")
(:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))]
(::columns opts) (assoc :columns (::columns opts))
(::for-update opts) (assoc :suffix "FOR UPDATE")
(::for-share opts) (assoc :suffix "FOR KEY SHARE"))]
(sql/for-query table where-params opts))))
(defn update
@ -65,11 +54,9 @@
(update table key-map where-params nil))
([table key-map where-params opts]
(let [opts (into default-opts opts)
opts (if-let [columns (::db/columns opts)]
(let [columns (if (seq columns)
(sql/as-cols columns opts)
"*")]
(assoc opts :suffix (str "RETURNING " columns)))
keys (::db/return-keys opts)
opts (if (vector? keys)
(assoc opts :suffix (str "RETURNING " (sql/as-cols keys opts)))
opts)]
(sql/for-update table key-map where-params opts))))
@ -77,5 +64,9 @@
([table where-params]
(delete table where-params nil))
([table where-params opts]
(let [opts (merge default-opts opts)]
(let [opts (merge default-opts opts)
keys (::db/return-keys opts)
opts (if (vector? keys)
(assoc opts :suffix (str "RETURNING " (sql/as-cols keys opts)))
opts)]
(sql/for-delete table where-params opts))))

View File

@ -39,27 +39,54 @@
[app.rpc.commands.media :as cmd.media]
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.svgo :as svgo]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.time :as dt]
[buddy.core.codecs :as bc]
[cuerdas.core :as str]
[datoteka.io :as io]
[promesa.exec :as px]
[promesa.exec.semaphore :as ps]
[promesa.util :as pu]))
[promesa.core :as p]))
(def ^:dynamic *system* nil)
(def ^:dynamic *stats* nil)
(def ^:dynamic *file-stats* nil)
(def ^:dynamic *team-stats* nil)
(def ^:dynamic *semaphore* nil)
(def ^:dynamic *skip-on-error* true)
(def ^:dynamic *stats*
"A dynamic var for setting up state for collect stats globally."
nil)
(def ^:dynamic *skip-on-error*
"A dynamic var for setting up the default error behavior."
true)
(def ^:dynamic ^:private *system*
"An internal var for making the current `system` available to all
internal functions without the need to explicitly pass it top down."
nil)
(def ^:dynamic ^:private *max-procs*
"A dynamic variable that can optionally indicates the maxumum number
of concurrent graphics migration processes."
nil)
(def ^:dynamic ^:private *file-stats*
"An internal dynamic var for collect stats by file."
nil)
(def ^:dynamic ^:private *team-stats*
"An internal dynamic var for collect stats by team."
nil)
(def grid-gap 50)
(def frame-gap 200)
(def max-group-size 50)
(defn decode-row
[{:keys [features data] :as row}]
(cond-> row
(some? features)
(assoc :features (db/decode-pgarray features #{}))
(some? data)
(assoc :data (blob/decode data))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE PREPARATION BEFORE MIGRATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -220,19 +247,17 @@
(fn [file-data]
;; Transform component and copy heads to frames, and set the
;; frame-id of its childrens
(letfn [(fix-container
[container]
(letfn [(fix-container [container]
(update container :objects update-vals fix-shape))
(fix-shape
[shape]
(fix-shape [shape]
(if (or (nil? (:parent-id shape)) (ctk/instance-head? shape))
(assoc shape
:type :frame ; Old groups must be converted
:fills (or (:fills shape) []) ; to frames and conform to spec
:hide-in-viewer (or (:hide-in-viewer shape) true)
:rx (or (:rx shape) 0)
:ry (or (:ry shape) 0))
:type :frame ; Old groups must be converted
:fills (or (:fills shape) []) ; to frames and conform to spec
:hide-in-viewer (or (:hide-in-viewer shape) true)
:rx (or (:rx shape) 0)
:ry (or (:ry shape) 0))
shape))]
(-> file-data
(update :pages-index update-vals fix-container)
@ -310,10 +335,10 @@
(defn- get-asset-groups
[assets generic-name]
(let [; Group by first element of the path.
(let [;; Group by first element of the path.
groups (d/group-by #(first (cfh/split-path (:path %))) assets)
; Split large groups in chunks of max-group-size elements
;; Split large groups in chunks of max-group-size elements
groups (loop [groups (seq groups)
result {}]
(if (empty? groups)
@ -334,15 +359,14 @@
result
splits)))))))
; Sort assets in each group by path
;; Sort assets in each group by path
groups (update-vals groups (fn [assets]
(sort-by (fn [{:keys [path name]}]
(str/lower (cfh/merge-path-item path name)))
assets)))
assets)))]
; Sort groups by name
groups (into (sorted-map) groups)]
groups))
;; Sort groups by name
(into (sorted-map) groups)))
(defn- create-frame
[name position width height]
@ -612,14 +636,11 @@
(defn- create-shapes-for-svg
[{:keys [id] :as mobj} file-id objects frame-id position]
(let [svg-text (get-svg-content id)
optimizer (::csvg/optimizer *system*)
svg-text (csvg/optimize optimizer svg-text)
svg-data (-> (csvg/parse svg-text)
(assoc :name (:name mobj))
(collect-and-persist-images file-id))]
(let [svg-text (get-svg-content id)
svg-text (svgo/optimize *system* svg-text)
svg-data (-> (csvg/parse svg-text)
(assoc :name (:name mobj))
(collect-and-persist-images file-id))]
(sbuilder/create-svg-shapes svg-data position objects frame-id frame-id #{} false)))
@ -678,9 +699,7 @@
(defn- create-media-grid
[fdata page-id frame-id grid media-group]
(let [factory (px/thread-factory :virtual true)
executor (px/fixed-executor :parallelism 10 :factory factory)
process (fn [mobj position]
(let [process (fn [mobj position]
(let [position (gpt/add position (gpt/point grid-gap grid-gap))
tp1 (dt/tpoint)]
(try
@ -690,7 +709,6 @@
:file-id (str (:id fdata))
:id (str (:id mobj))
:cause cause)
(if-not *skip-on-error*
(throw cause)
nil))
@ -699,21 +717,24 @@
:file-id (str (:id fdata))
:media-id (str (:id mobj))
:elapsed (dt/format-duration (tp1)))))))]
(try
(->> (d/zip media-group grid)
(map (fn [[mobj position]]
(sse/tap {:type :migration-progress
:section :graphics
:name (:name mobj)})
(px/submit! executor (partial process mobj position))))
(reduce (fn [fdata promise]
(if-let [changes (deref promise)]
(-> (assoc-in fdata [:options :components-v2] true)
(cp/process-changes changes false))
fdata))
fdata))
(finally
(pu/close! executor)))))
(->> (d/zip media-group grid)
(partition-all (or *max-procs* 1))
(mapcat (fn [partition]
(->> partition
(map (fn [[mobj position]]
(sse/tap {:type :migration-progress
:section :graphics
:name (:name mobj)})
(p/vthread (process mobj position))))
(doall)
(map deref)
(doall))))
(filter some?)
(reduce (fn [fdata changes]
(-> (assoc-in fdata [:options :components-v2] true)
(cp/process-changes changes false)))
fdata))))
(defn- migrate-graphics
[fdata]
@ -759,6 +780,11 @@
(create-media-grid fdata page-id (:id frame) grid assets)
(gpt/add position (gpt/point 0 (+ height (* 2 grid-gap) frame-gap))))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PRIVATE HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- migrate-fdata
[fdata libs]
(let [migrated? (dm/get-in fdata [:options :components-v2])]
@ -771,11 +797,22 @@
(defn- get-file
[system id]
(binding [pmap/*load-fn* (partial fdata/load-pointer system id)]
(-> (files/get-file system id :migrate? false)
(-> (db/get system :file {:id id}
{::db/remove-deleted false
::db/check-deleted false})
(decode-row)
(update :data assoc :id id)
(update :data fdata/process-pointers deref)
(fmg/migrate-file))))
(defn- get-team
[system team-id]
(-> (db/get system :team {:id team-id}
{::db/remove-deleted false
::db/check-deleted false})
(decode-row)))
(defn- validate-file!
[file libs throw-on-validate?]
(try
@ -791,7 +828,8 @@
(let [file (get-file system id)
libs (->> (files/get-file-libraries conn id)
(into [file] (comp (map :id) (map (partial get-file system))))
(into [file] (comp (map :id)
(map (partial get-file system))))
(d/index-by :id))
file (-> file
@ -816,18 +854,39 @@
{:data (blob/encode (:data file))
:features (db/create-array conn "text" (:features file))
:revn (:revn file)}
{:id (:id file)}
{::db/return-keys? false})
{:id (:id file)})
(dissoc file :data)))
(def ^:private sql:get-and-lock-team-files
"SELECT f.id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
WHERE p.team_id = ?
FOR UPDATE")
(defn- get-and-lock-files
[conn team-id]
(->> (db/cursor conn [sql:get-and-lock-team-files team-id])
(map :id)))
(defn- update-team-features!
[conn team-id features]
(let [features (db/create-array conn "text" features)]
(db/update! conn :team
{:features features}
{:id team-id})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn migrate-file!
[system file-id & {:keys [validate? throw-on-validate?]}]
(let [tpoint (dt/tpoint)
file-id (if (string? file-id)
(parse-uuid file-id)
file-id)]
(binding [*file-stats* (atom {})]
[system file-id & {:keys [validate? throw-on-validate? max-procs]}]
(let [tpoint (dt/tpoint)]
(binding [*file-stats* (atom {})
*max-procs* max-procs]
(try
(l/dbg :hint "migrate:file:start" :file-id (str file-id))
@ -839,7 +898,6 @@
(process-file system file-id
:validate? validate?
:throw-on-validate? throw-on-validate?)))))
(finally
(let [elapsed (tpoint)
components (get @*file-stats* :processed/components 0)
@ -855,73 +913,51 @@
(some-> *team-stats* (swap! update :processed/files (fnil inc 0)))))))))
(defn migrate-team!
[system team-id & {:keys [validate? throw-on-validate?]}]
(let [tpoint (dt/tpoint)
team-id (if (string? team-id)
(parse-uuid team-id)
team-id)]
(l/dbg :hint "migrate:team:start" :team-id (dm/str team-id))
[system team-id & {:keys [validate? throw-on-validate? max-procs]}]
(l/dbg :hint "migrate:team:start"
:team-id (dm/str team-id))
(let [tpoint (dt/tpoint)
migrate-file
(fn [system file-id]
(migrate-file! system file-id
:max-procs max-procs
:validate? validate?
:throw-on-validate? throw-on-validate?))
migrate-team
(fn [{:keys [::db/conn] :as system} {:keys [id features] :as team}]
(let [features (-> features
(disj "ephimeral/v2-migration")
(conj "components/v2")
(conj "layout/grid")
(conj "styles/v2"))]
(run! (partial migrate-file system)
(get-and-lock-files conn id))
(update-team-features! conn id features)))]
(binding [*team-stats* (atom {})]
(try
;; We execute this out of transaction because we want this
;; change to be visible to all other sessions before starting
;; the migration
(let [sql (str "UPDATE team SET features = "
" array_append(features, 'ephimeral/v2-migration') "
" WHERE id = ?")]
(db/exec-one! system [sql team-id]))
(db/tx-run! system
(fn [{:keys [::db/conn] :as system}]
;; Lock the team
(db/exec-one! conn ["SET idle_in_transaction_session_timeout = 0"])
(let [{:keys [features] :as team} (-> (db/get conn :team {:id team-id})
(update :features db/decode-pgarray #{}))]
(if (contains? features "components/v2")
(l/dbg :hint "team already migrated")
(let [sql (str/concat
"SELECT f.id FROM file AS f "
" JOIN project AS p ON (p.id = f.project_id) "
"WHERE p.team_id = ? AND f.deleted_at IS NULL AND p.deleted_at IS NULL "
"FOR UPDATE")]
(doseq [file-id (->> (db/exec! conn [sql team-id])
(map :id))]
(migrate-file! system file-id
:validate? validate?
:throw-on-validate? throw-on-validate?))
(let [features (-> features
(disj "ephimeral/v2-migration")
(conj "components/v2")
(conj "layout/grid")
(conj "styles/v2"))]
(db/update! conn :team
{:features (db/create-array conn "text" features)}
{:id team-id})))))))
(db/tx-run! system (fn [system]
(db/exec-one! system ["SET idle_in_transaction_session_timeout = 0"])
(let [team (get-team system team-id)]
(if (contains? (:features team) "components/v2")
(l/inf :hint "team already migrated")
(migrate-team system team)))))
(finally
(some-> *semaphore* ps/release!)
(let [elapsed (tpoint)]
(let [elapsed (tpoint)
components (get @*team-stats* :processed/components 0)
graphics (get @*team-stats* :processed/graphics 0)
files (get @*team-stats* :processed/files 0)]
(some-> *stats* (swap! update :processed/teams (fnil inc 0)))
;; We execute this out of transaction because we want this
;; change to be visible to all other sessions before starting
;; the migration
(let [sql (str "UPDATE team SET features = "
" array_remove(features, 'ephimeral/v2-migration') "
" WHERE id = ?")]
(db/exec-one! system [sql team-id]))
(let [components (get @*team-stats* :processed/components 0)
graphics (get @*team-stats* :processed/graphics 0)
files (get @*team-stats* :processed/files 0)]
(l/dbg :hint "migrate:team:end"
:team-id (dm/str team-id)
:files files
:components components
:graphics graphics
:elapsed (dt/format-duration elapsed)))))))))
(l/dbg :hint "migrate:team:end"
:team-id (dm/str team-id)
:files files
:components components
:graphics graphics
:elapsed (dt/format-duration elapsed))))))))

View File

@ -11,6 +11,7 @@
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.util.blob :as blob]
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]))
@ -38,8 +39,8 @@
[system file-id id]
(let [{:keys [content]} (db/get system :file-data-fragment
{:id id :file-id file-id}
{::db/columns [:content]
::db/check-deleted? false})]
{::sql/columns [:content]
::db/check-deleted false})]
(when-not content
(ex/raise :type :internal
:code :fragment-not-found

View File

@ -133,7 +133,7 @@
[_ {:keys [::db/pool] :as cfg}]
(cond
(db/read-only? pool)
(l/warn :hint "audit: disabled (db is read-only)")
(l/warn :hint "audit disabled (db is read-only)")
:else
cfg))
@ -187,8 +187,7 @@
false)}))
(defn- handle-event!
[conn-or-pool event]
(us/verify! ::event event)
[cfg event]
(let [params {:id (uuid/next)
:name (::name event)
:type (::type event)
@ -201,19 +200,22 @@
;; NOTE: this operation may cause primary key conflicts on inserts
;; because of the timestamp precission (two concurrent requests), in
;; this case we just retry the operation.
(rtry/with-retry {::rtry/when rtry/conflict-exception?
::rtry/max-retries 6
::rtry/label "persist-audit-log"
::db/conn (dm/check db/connection? conn-or-pool)}
(let [now (dt/now)]
(db/insert! conn-or-pool :audit-log
(-> params
(update :props db/tjson)
(update :context db/tjson)
(update :ip-addr db/inet)
(assoc :created-at now)
(assoc :tracked-at now)
(assoc :source "backend"))))))
(let [cfg (-> cfg
(assoc ::rtry/when rtry/conflict-exception?)
(assoc ::rtry/max-retries 6)
(assoc ::rtry/label "persist-audit-log"))
params (-> params
(update :props db/tjson)
(update :context db/tjson)
(update :ip-addr db/inet)
(assoc :source "backend"))]
(rtry/invoke cfg (fn [cfg]
(let [tnow (dt/now)
params (-> params
(assoc :created-at tnow)
(assoc :tracked-at tnow))]
(db/insert! cfg :audit-log params))))))
(when (and (contains? cf/flags :webhooks)
(::webhooks/event? event))
@ -226,7 +228,7 @@
:else label)
dedupe? (boolean (and batch-key batch-timeout))]
(wrk/submit! ::wrk/conn conn-or-pool
(wrk/submit! ::wrk/conn (::db/conn cfg)
::wrk/task :process-webhook-event
::wrk/queue :webhooks
::wrk/max-retries 0
@ -243,12 +245,12 @@
(defn submit!
"Submit audit event to the collector."
[cfg params]
(let [conn (or (::db/conn cfg) (::db/pool cfg))]
(us/assert! ::db/pool-or-conn conn)
(try
(handle-event! conn (d/without-nils params))
(catch Throwable cause
(l/error :hint "audit: unexpected error processing event" :cause cause)))))
(try
(let [event (d/without-nils params)]
(us/verify! ::event event)
(db/tx-run! cfg handle-event! event))
(catch Throwable cause
(l/error :hint "unexpected error processing event" :cause cause))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TASK: ARCHIVE

View File

@ -111,9 +111,11 @@
" where id=?")
err
(:id whook)]
res (db/exec-one! pool sql {::db/return-keys? true})]
res (db/exec-one! pool sql {::db/return-keys true})]
(when (>= (:error-count res) max-errors)
(db/update! pool :webhook {:is-active false} {:id (:id whook)})))
(db/update! pool :webhook
{:is-active false}
{:id (:id whook)})))
(db/update! pool :webhook
{:updated-at (dt/now)

View File

@ -10,7 +10,6 @@
[app.auth.oidc :as-alias oidc]
[app.auth.oidc.providers :as-alias oidc.providers]
[app.common.logging :as l]
[app.common.svg :as csvg]
[app.config :as cf]
[app.db :as-alias db]
[app.email :as-alias email]
@ -34,7 +33,10 @@
[app.srepl :as-alias srepl]
[app.storage :as-alias sto]
[app.storage.fs :as-alias sto.fs]
[app.storage.gc-deleted :as-alias sto.gc-deleted]
[app.storage.gc-touched :as-alias sto.gc-touched]
[app.storage.s3 :as-alias sto.s3]
[app.svgo :as-alias svgo]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[cider.nrepl :refer [cider-nrepl-handler]]
@ -202,11 +204,11 @@
:app.storage.tmp/cleaner
{::wrk/executor (ig/ref ::wrk/executor)}
::sto/gc-deleted-task
::sto.gc-deleted/handler
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
::sto/gc-touched-task
::sto.gc-touched/handler
{::db/pool (ig/ref ::db/pool)}
::http.client/client
@ -314,7 +316,7 @@
::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref ::mbus/msgbus)
::rds/redis (ig/ref ::rds/redis)
::csvg/optimizer (ig/ref ::csvg/optimizer)
::svgo/optimizer (ig/ref ::svgo/optimizer)
::rpc/climit (ig/ref ::rpc/climit)
::rpc/rlimit (ig/ref ::rpc/rlimit)
@ -337,12 +339,13 @@
::wrk/tasks
{:sendmail (ig/ref ::email/handler)
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
:orphan-teams-gc (ig/ref :app.tasks.orphan-teams-gc/handler)
:file-gc (ig/ref :app.tasks.file-gc/handler)
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
:storage-gc-deleted (ig/ref ::sto/gc-deleted-task)
:storage-gc-touched (ig/ref ::sto/gc-touched-task)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
:telemetry (ig/ref :app.tasks.telemetry/handler)
:storage-gc-deleted (ig/ref ::sto.gc-deleted/handler)
:storage-gc-touched (ig/ref ::sto.gc-touched/handler)
:session-gc (ig/ref ::session.tasks/gc)
:audit-log-archive (ig/ref ::audit.tasks/archive)
:audit-log-gc (ig/ref ::audit.tasks/gc)
@ -373,6 +376,9 @@
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.orphan-teams-gc/handler
{::db/pool (ig/ref ::db/pool)}
:app.tasks.file-gc/handler
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
@ -403,8 +409,9 @@
;; module requires the migrations to run before initialize.
::migrations (ig/ref :app.migrations/migrations)}
::csvg/optimizer
{}
::svgo/optimizer
{::wrk/executor (ig/ref ::wrk/executor)
::svgo/max-procs (cf/get :svgo-max-procs)}
::audit.tasks/archive
{::props (ig/ref ::setup/props)
@ -458,6 +465,9 @@
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :objects-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :orphan-teams-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :storage-gc-deleted}

View File

@ -337,7 +337,40 @@
:fn (mg/resource "app/migrations/sql/0106-mod-team-table.sql")}
{:name "0107-mod-file-tagged-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0107-mod-file-tagged-object-thumbnail-table.sql")}])
:fn (mg/resource "app/migrations/sql/0107-mod-file-tagged-object-thumbnail-table.sql")}
{:name "0107-add-deletion-protection-trigger-function"
:fn (mg/resource "app/migrations/sql/0107-add-deletion-protection-trigger-function.sql")}
{:name "0108-mod-file-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0108-mod-file-thumbnail-table.sql")}
{:name "0109-mod-file-tagged-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0109-mod-file-tagged-object-thumbnail-table.sql")}
{:name "0110-mod-file-media-object-table"
:fn (mg/resource "app/migrations/sql/0110-mod-file-media-object-table.sql")}
{:name "0111-mod-file-data-fragment-table"
:fn (mg/resource "app/migrations/sql/0111-mod-file-data-fragment-table.sql")}
{:name "0112-mod-profile-table"
:fn (mg/resource "app/migrations/sql/0112-mod-profile-table.sql")}
{:name "0113-mod-team-font-variant-table"
:fn (mg/resource "app/migrations/sql/0113-mod-team-font-variant-table.sql")}
{:name "0114-mod-team-table"
:fn (mg/resource "app/migrations/sql/0114-mod-team-table.sql")}
{:name "0115-mod-project-table"
:fn (mg/resource "app/migrations/sql/0115-mod-project-table.sql")}
{:name "0116-mod-file-table"
:fn (mg/resource "app/migrations/sql/0116-mod-file-table.sql")}
{:name "0117-mod-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0117-mod-file-object-thumbnail-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@ -0,0 +1,8 @@
CREATE OR REPLACE FUNCTION raise_deletion_protection()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'unable to proceed to delete row on "%"', TG_TABLE_NAME
USING HINT = 'disable deletion protection with "SET rules.deletion_protection TO off"';
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1,25 @@
--- Add missing index for deleted_at column, we include all related
--- columns because we expect the index to be small and expect use
--- index-only scans.
CREATE INDEX IF NOT EXISTS file_thumbnail__deleted_at__idx
ON file_thumbnail (deleted_at, file_id, revn, media_id)
WHERE deleted_at IS NOT NULL;
--- Add missing for media_id column, used mainly for refs checking
CREATE INDEX IF NOT EXISTS file_thumbnail__media_id__idx ON file_thumbnail (media_id);
--- Remove CASCADE from media_id and file_id foreign constraint
ALTER TABLE file_thumbnail
DROP CONSTRAINT file_thumbnail_file_id_fkey,
ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE;
ALTER TABLE file_thumbnail
DROP CONSTRAINT file_thumbnail_media_id_fkey,
ADD FOREIGN KEY (media_id) REFERENCES storage_object(id) DEFERRABLE;
--- Add deletion protection
CREATE OR REPLACE TRIGGER deletion_protection__tgr
BEFORE DELETE ON file_thumbnail FOR EACH STATEMENT
WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR
(current_setting('rules.deletion_protection', true) IS NULL))
EXECUTE PROCEDURE raise_deletion_protection();

View File

@ -0,0 +1,26 @@
ALTER TABLE file_tagged_object_thumbnail
ADD COLUMN updated_at timestamptz NULL,
ADD COLUMN deleted_at timestamptz NULL;
--- Add index for deleted_at column, we include all related columns
--- because we expect the index to be small and expect use index-only
--- scans.
CREATE INDEX IF NOT EXISTS file_tagged_object_thumbnail__deleted_at__idx
ON file_tagged_object_thumbnail (deleted_at, file_id, object_id, media_id)
WHERE deleted_at IS NOT NULL;
--- Remove CASCADE from media_id and file_id foreign constraint
ALTER TABLE file_tagged_object_thumbnail
DROP CONSTRAINT file_tagged_object_thumbnail_media_id_fkey,
ADD FOREIGN KEY (media_id) REFERENCES storage_object(id) DEFERRABLE;
ALTER TABLE file_tagged_object_thumbnail
DROP CONSTRAINT file_tagged_object_thumbnail_file_id_fkey,
ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE;
--- Add deletion protection
CREATE OR REPLACE TRIGGER deletion_protection__tgr
BEFORE DELETE ON file_tagged_object_thumbnail FOR EACH STATEMENT
WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR
(current_setting('rules.deletion_protection', true) IS NULL))
EXECUTE PROCEDURE raise_deletion_protection();

View File

@ -0,0 +1,27 @@
--- Fix legacy naming
ALTER INDEX media_object_pkey RENAME TO file_media_object_pkey;
ALTER INDEX media_object__file_id__idx RENAME TO file_media_object__file_id__idx;
--- Create index for the deleted_at column
CREATE INDEX IF NOT EXISTS file_media_object__deleted_at__idx
ON file_media_object (deleted_at, id, media_id)
WHERE deleted_at IS NOT NULL;
--- Drop now unnecesary trigger because this will be handled by the
--- application code
DROP TRIGGER file_media_object__on_delete__tgr ON file_media_object;
DROP FUNCTION on_delete_file_media_object ( ) CASCADE;
DROP TRIGGER file_media_object__on_insert__tgr ON file_media_object;
DROP FUNCTION on_media_object_insert () CASCADE;
--- Remove CASCADE from file FOREIGN KEY
ALTER TABLE file_media_object
DROP CONSTRAINT file_media_object_file_id_fkey,
ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE;
--- Add deletion protection
CREATE OR REPLACE TRIGGER deletion_protection__tgr
BEFORE DELETE ON file_media_object FOR EACH STATEMENT
WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR
(current_setting('rules.deletion_protection', true) IS NULL))
EXECUTE PROCEDURE raise_deletion_protection();

View File

@ -0,0 +1,9 @@
ALTER TABLE file_data_fragment
ADD COLUMN deleted_at timestamptz NULL;
--- Add index for deleted_at column, we include all related columns
--- because we expect the index to be small and expect use index-only
--- scans.
CREATE INDEX IF NOT EXISTS file_data_fragment__deleted_at__idx
ON file_data_fragment (deleted_at, file_id, id)
WHERE deleted_at IS NOT NULL;

View File

@ -0,0 +1,15 @@
ALTER TABLE profile
DROP CONSTRAINT profile_photo_id_fkey,
ADD FOREIGN KEY (photo_id) REFERENCES storage_object(id) DEFERRABLE,
DROP CONSTRAINT profile_default_project_id_fkey,
ADD FOREIGN KEY (default_project_id) REFERENCES project(id) DEFERRABLE,
DROP CONSTRAINT profile_default_team_id_fkey,
ADD FOREIGN KEY (default_team_id) REFERENCES team(id) DEFERRABLE;
--- Add deletion protection
CREATE OR REPLACE TRIGGER deletion_protection__tgr
BEFORE DELETE ON profile FOR EACH STATEMENT
WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR
(current_setting('rules.deletion_protection', true) IS NULL))
EXECUTE PROCEDURE raise_deletion_protection();

View File

@ -0,0 +1,20 @@
--- Remove ON DELETE SET NULL from foreign constraint on
--- storage_object table
ALTER TABLE team_font_variant
DROP CONSTRAINT team_font_variant_otf_file_id_fkey,
ADD FOREIGN KEY (otf_file_id) REFERENCES storage_object(id) DEFERRABLE,
DROP CONSTRAINT team_font_variant_ttf_file_id_fkey,
ADD FOREIGN KEY (ttf_file_id) REFERENCES storage_object(id) DEFERRABLE,
DROP CONSTRAINT team_font_variant_woff1_file_id_fkey,
ADD FOREIGN KEY (woff1_file_id) REFERENCES storage_object(id) DEFERRABLE,
DROP CONSTRAINT team_font_variant_woff2_file_id_fkey,
ADD FOREIGN KEY (woff2_file_id) REFERENCES storage_object(id) DEFERRABLE,
DROP CONSTRAINT team_font_variant_team_id_fkey,
ADD FOREIGN KEY (team_id) REFERENCES team(id) DEFERRABLE;
--- Add deletion protection
CREATE OR REPLACE TRIGGER deletion_protection__tgr
BEFORE DELETE ON team_font_variant FOR EACH STATEMENT
WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR
(current_setting('rules.deletion_protection', true) IS NULL))
EXECUTE PROCEDURE raise_deletion_protection();

View File

@ -0,0 +1,10 @@
--- Add deletion protection
CREATE OR REPLACE TRIGGER deletion_protection__tgr
BEFORE DELETE ON team FOR EACH STATEMENT
WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR
(current_setting('rules.deletion_protection', true) IS NULL))
EXECUTE PROCEDURE raise_deletion_protection();
ALTER TABLE team
DROP CONSTRAINT team_photo_id_fkey,
ADD FOREIGN KEY (photo_id) REFERENCES storage_object(id) DEFERRABLE;

View File

@ -0,0 +1,3 @@
ALTER TABLE project
DROP CONSTRAINT project_team_id_fkey,
ADD FOREIGN KEY (team_id) REFERENCES team(id) DEFERRABLE;

View File

@ -0,0 +1,3 @@
ALTER TABLE file
DROP CONSTRAINT file_project_id_fkey,
ADD FOREIGN KEY (project_id) REFERENCES project(id) DEFERRABLE;

View File

@ -0,0 +1,12 @@
ALTER TABLE file_object_thumbnail
DROP CONSTRAINT file_object_thumbnail_file_id_fkey,
ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE,
DROP CONSTRAINT file_object_thumbnail_media_id_fkey,
ADD FOREIGN KEY (media_id) REFERENCES storage_object(id) DEFERRABLE;
--- Mark all related storage_object row as touched
-- UPDATE storage_object SET touched_at = now()
-- WHERE id IN (SELECT DISTINCT media_id
-- FROM file_object_thumbnail
-- WHERE media_id IS NOT NULL)
-- AND touched_at IS NULL;

View File

@ -48,7 +48,7 @@
(map event->row))
events (sequence xform events)]
(when (seq events)
(db/insert-multi! pool :audit-log event-columns events))))
(db/insert-many! pool :audit-log event-columns events))))
(def schema:event
[:map {:title "Event"}

View File

@ -54,7 +54,9 @@
:hint "the current account does not have password")
(let [result (profile/verify-password cfg password (:password profile))]
(when (:update result)
(l/trace :hint "updating profile password" :id (:id profile) :email (:email profile))
(l/trc :hint "updating profile password"
:id (str (:id profile))
:email (:email profile))
(profile/update-profile-password! conn (assoc profile :password password)))
(:valid result))))
@ -131,7 +133,8 @@
(update-password [conn profile-id]
(let [pwd (profile/derive-password cfg password)]
(db/update! conn :profile {:password pwd} {:id profile-id})))]
(db/update! conn :profile {:password pwd} {:id profile-id})
nil))]
(db/with-atomic [conn pool]
(->> (validate-token token)
@ -301,7 +304,8 @@
(-> (db/update! conn :profile
{:default-team-id (:id team)
:default-project-id (:default-project-id team)}
{:id id})
{:id id}
{::db/return-keys true})
(profile/decode-row))))

View File

@ -317,7 +317,7 @@
[cfg file-id]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
(some-> (db/get* conn :file {:id file-id} {::db/remove-deleted? false})
(some-> (db/get* conn :file {:id file-id} {::db/remove-deleted false})
(files/decode-row)
(update :data feat.fdata/process-pointers deref))))))
@ -593,6 +593,7 @@
(declare lookup-index)
(declare update-index)
(declare relink-media)
(declare relink-colors)
(declare relink-shapes)
(defmulti read-import ::version)
@ -663,6 +664,7 @@
(case feature
"components/v2"
(feat.compv2/migrate-file! options file-id
:max-procs 2
:validate? validate?
:throw-on-validate? true)
@ -723,6 +725,7 @@
(update :pages-index relink-shapes)
(update :components relink-shapes)
(update :media relink-media)
(update :colors relink-colors)
(d/without-nils))))))
@ -997,6 +1000,17 @@
media
media))
(defn- relink-colors
"A function responsible of process the :colors attr of file data and
remap the old ids with the new ones."
[colors]
(reduce-kv (fn [res k v]
(if (:image v)
(update-in res [k :image :id] lookup-index)
res))
colors
colors))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HIGH LEVEL API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -12,6 +12,7 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.db.sql :as sql]
[app.features.fdata :as feat.fdata]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
@ -62,8 +63,8 @@
(decode-row)))
(defn- get-comment
[conn comment-id & {:keys [for-update?]}]
(db/get-by-id conn :comment comment-id {:for-update for-update?}))
[conn comment-id & {:as opts}]
(db/get-by-id conn :comment comment-id opts))
(defn- get-next-seqn
[conn file-id]
@ -309,23 +310,21 @@
::quotes/project-id project-id
::quotes/file-id file-id}))
(rtry/with-retry {::rtry/when rtry/conflict-exception?
::rtry/max-retries 3
::rtry/label "create-comment-thread"
::db/conn conn}
(create-comment-thread conn
{:created-at request-at
:profile-id profile-id
:file-id file-id
:page-id page-id
:page-name page-name
:position position
:content content
:frame-id frame-id}))))))
(-> cfg
(assoc ::rtry/when rtry/conflict-exception?)
(assoc ::rtry/label "create-comment-thread")
(rtry/invoke create-comment-thread {:created-at request-at
:profile-id profile-id
:file-id file-id
:page-id page-id
:page-name page-name
:position position
:content content
:frame-id frame-id}))))))
(defn- create-comment-thread
[conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
[{:keys [::db/conn]} {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
(let [;; NOTE: we take the next seq number from a separate query because the whole
;; operation can be retried on conflict, and in this case the new seq shold be
;; retrieved from the database.
@ -377,7 +376,7 @@
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(upsert-comment-thread-status! conn profile-id id))))
@ -394,7 +393,7 @@
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(db/update! conn :comment-thread
{:is-resolved is-resolved}
@ -417,7 +416,7 @@
[cfg {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content]}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true)
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
(files/check-comment-permissions! conn profile-id (:id file) share-id)
@ -473,8 +472,8 @@
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::db/for-update? true)
{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)]
(let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::sql/for-update true)
{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
@ -506,7 +505,7 @@
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id]}]
(db/with-atomic [conn pool]
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(when-not (= owner-id profile-id)
(ex/raise :type :validation
@ -526,14 +525,14 @@
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::db/for-update? true)
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::sql/for-update true)
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(when-not (= owner-id profile-id)
(ex/raise :type :validation
:code :not-allowed))
(db/delete! conn :comment {:id id}))))
(db/delete! conn :comment {:id id})
nil)))
;; --- COMMAND: Update comment thread position
@ -546,7 +545,7 @@
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}]
(db/with-atomic [conn pool]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(db/update! conn :comment-thread
{:modified-at request-at
@ -566,7 +565,7 @@
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}]
(db/with-atomic [conn pool]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(db/update! conn :comment-thread
{:modified-at request-at

View File

@ -20,6 +20,7 @@
[app.common.types.file :as ctf]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
@ -238,8 +239,7 @@
(db/update! conn :file
{:data (blob/encode (:data file))
:features (db/create-array conn "text" (:features file))}
{:id id}
{::db/return-keys? false})
{:id id})
(when (contains? (:features file) "fdata/pointer-map")
(feat.fdata/persist-pointers! cfg id)))
@ -262,9 +262,9 @@
(when (some? project-id)
{:project-id project-id}))
file (-> (db/get conn :file params
{::db/check-deleted? (not include-deleted?)
::db/remove-deleted? (not include-deleted?)
::db/for-update? lock-for-update?})
{::db/check-deleted (not include-deleted?)
::db/remove-deleted (not include-deleted?)
::sql/for-update lock-for-update?})
(decode-row))]
(if migrate?
(migrate-file cfg file)
@ -516,7 +516,7 @@
ft.media_id
from file as f
inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn and ft.deleted_at is null)
where f.is_shared = true
and f.deleted_at is null
and p.deleted_at is null
@ -733,7 +733,8 @@
(db/update! conn :file
{:name name
:modified-at (dt/now)}
{:id id}))
{:id id}
{::db/return-keys true}))
(sv/defmethod ::rename-file
{::doc/added "1.17"
@ -860,9 +861,7 @@
(let [file (assoc file :is-shared true)]
(db/update! conn :file
{:is-shared true}
{:id id}
::db/return-keys? false)
{:id id})
file)
:else
@ -899,7 +898,7 @@
(db/update! conn :file
{:deleted-at (dt/now)}
{:id file-id}
{::db/columns [:id :name :is-shared :project-id :created-at :modified-at]}))
{::db/return-keys [:id :name :is-shared :project-id :created-at :modified-at]}))
(def ^:private
schema:delete-file
@ -998,8 +997,8 @@
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)
(unlink-file-from-library conn params)))
(unlink-file-from-library conn params)
nil))
;; --- MUTATION COMMAND: update-sync
@ -1008,7 +1007,8 @@
(db/update! conn :file-library-rel
{:synced-at (dt/now)}
{:file-id file-id
:library-file-id library-id}))
:library-file-id library-id}
{::db/return-keys true}))
(def ^:private schema:update-file-library-sync-status
[:map {:title "update-file-library-sync-status"}
@ -1031,7 +1031,8 @@
[conn {:keys [file-id date] :as params}]
(db/update! conn :file
{:ignore-sync-until date}
{:id file-id}))
{:id file-id}
{::db/return-keys true}))
(s/def ::ignore-file-library-sync-status
(s/keys :req [::rpc/profile-id]

View File

@ -17,6 +17,7 @@
[app.common.types.shape-tree :as ctt]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
@ -27,6 +28,7 @@
[app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc]
[app.rpc.retry :as rtry]
[app.storage :as sto]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
@ -46,7 +48,7 @@
(let [sql (str/concat
"select object_id, media_id, tag "
" from file_tagged_object_thumbnail"
" where file_id=? and tag=?")
" where file_id=? and tag=? and deleted_at is null")
res (db/exec! conn [sql file-id tag])]
(->> res
(d/index-by :object-id (fn [row]
@ -58,7 +60,7 @@
(let [sql (str/concat
"select object_id, media_id, tag "
" from file_tagged_object_thumbnail"
" where file_id=?")
" where file_id=? and deleted_at is null")
res (db/exec! conn [sql file-id])]
(->> res
(d/index-by :object-id (fn [row]
@ -69,7 +71,7 @@
(let [sql (str/concat
"select object_id, media_id, tag "
" from file_tagged_object_thumbnail"
" where file_id=? and object_id = ANY(?)")
" where file_id=? and object_id = ANY(?) and deleted_at is null")
ids (db/create-array conn "text" (seq object-ids))
res (db/exec! conn [sql file-id ids])]
@ -226,34 +228,54 @@
;; MUTATION COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- MUTATION COMMAND: create-file-object-thumbnail
(def ^:private sql:create-object-thumbnail
"insert into file_tagged_object_thumbnail(file_id, object_id, media_id, tag)
values (?, ?, ?, ?)
on conflict(file_id, tag, object_id) do
update set media_id = ?
returning *;")
;; MUTATION COMMAND: create-file-object-thumbnail
(defn- create-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} file-id object-id media tag]
(let [path (:path media)
(let [thumb (db/get* conn :file-tagged-object-thumbnail
{:file-id file-id
:object-id object-id
:tag tag}
{::db/remove-deleted false
::sql/for-update true})
path (:path media)
mtype (:mtype media)
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
tnow (dt/now)
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? true
::sto/touched-at (dt/now)
::sto/touched-at tnow
:content-type mtype
:bucket "file-object-thumbnail"})]
(db/exec-one! conn [sql:create-object-thumbnail file-id object-id
(:id media) tag (:id media)])))
(if (some? thumb)
(do
;; We mark the old media id as touched if it does not matches
(when (not= (:id media) (:media-id thumb))
(sto/touch-object! storage (:media-id thumb)))
(db/update! conn :file-tagged-object-thumbnail
{:media-id (:id media)
:deleted-at nil
:updated-at tnow}
{:file-id file-id
:object-id object-id
:tag tag}))
(db/insert! conn :file-tagged-object-thumbnail
{:file-id file-id
:object-id object-id
:created-at tnow
:updated-at tnow
:tag tag
:media-id (:id media)}))))
(def schema:create-file-object-thumbnail
(def ^:private
schema:create-file-object-thumbnail
[:map {:title "create-file-object-thumbnail"}
[:file-id ::sm/uuid]
[:object-id :string]
@ -268,32 +290,36 @@
::audit/skip true
::sm/params schema:create-file-object-thumbnail}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media tag]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(media/validate-media-type! media)
(media/validate-media-size! media)
[cfg {:keys [::rpc/profile-id file-id object-id media tag]}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id)
(media/validate-media-type! media)
(media/validate-media-size! media)
(when-not (db/read-only? conn)
(-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(create-file-object-thumbnail! file-id object-id media (or tag "frame"))))))
(when-not (db/read-only? conn)
(let [cfg (-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::rtry/when rtry/conflict-exception?)
(assoc ::rtry/max-retries 5)
(assoc ::rtry/label "create-file-object-thumbnail"))]
(rtry/invoke cfg create-file-object-thumbnail!
file-id object-id media (or tag "frame")))))))
;; --- MUTATION COMMAND: delete-file-object-thumbnail
(defn- delete-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} file-id object-id]
(when-let [{:keys [media-id]} (db/get* conn :file-tagged-object-thumbnail
{:file-id file-id
:object-id object-id}
{::db/for-update? true})]
(when-let [{:keys [media-id tag]} (db/get* conn :file-tagged-object-thumbnail
{:file-id file-id
:object-id object-id}
{::sql/for-update true})]
(sto/touch-object! storage media-id)
(db/delete! conn :file-tagged-object-thumbnail
(db/update! conn :file-tagged-object-thumbnail
{:deleted-at (dt/now)}
{:file-id file-id
:object-id object-id})
nil))
:object-id object-id
:tag tag})))
(s/def ::delete-file-object-thumbnail
(s/keys :req [::rpc/profile-id]
@ -302,29 +328,21 @@
(sv/defmethod ::delete-file-object-thumbnail
{::doc/added "1.19"
::doc/module :files
::doc/deprecated "1.20"
::climit/id :file-thumbnail-ops
::climit/key-fn ::rpc/profile-id
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(delete-file-object-thumbnail! file-id object-id))
nil)))
[cfg {:keys [::rpc/profile-id file-id object-id]}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(-> cfg
(update ::sto/storage media/configure-assets-storage conn)
(delete-file-object-thumbnail! file-id object-id))
nil))))
;; --- MUTATION COMMAND: create-file-thumbnail
(def ^:private sql:create-file-thumbnail
"insert into file_thumbnail (file_id, revn, media_id, props)
values (?, ?, ?, ?::jsonb)
on conflict(file_id, revn) do
update set media_id=?, props=?, updated_at=now();")
(defn- create-file-thumbnail!
[{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}]
(media/validate-media-type! media)
@ -336,14 +354,42 @@
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
tnow (dt/now)
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? false
::sto/deduplicate? true
::sto/touched-at tnow
:content-type mtype
:bucket "file-thumbnail"})]
(db/exec-one! conn [sql:create-file-thumbnail file-id revn
(:id media) props
(:id media) props])
:bucket "file-thumbnail"})
thumb (db/get* conn :file-thumbnail
{:file-id file-id
:revn revn}
{::db/remove-deleted false
::sql/for-update true})]
(if (some? thumb)
(do
;; We mark the old media id as touched if it does not match
(when (not= (:id media) (:media-id thumb))
(sto/touch-object! storage (:media-id thumb)))
(db/update! conn :file-thumbnail
{:media-id (:id media)
:deleted-at nil
:updated-at tnow
:props props}
{:file-id file-id
:revn revn}))
(db/insert! conn :file-thumbnail
{:file-id file-id
:revn revn
:created-at tnow
:updated-at tnow
:props props
:media-id (:id media)}))
media))
(sv/defmethod ::create-file-thumbnail
@ -359,13 +405,14 @@
[:revn :int]
[:media ::media/upload]]}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [media (-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(create-file-thumbnail! params))]
{:uri (files/resolve-public-uri (:id media))}))))
[cfg {:keys [::rpc/profile-id file-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [cfg (-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::rtry/when rtry/conflict-exception?)
(assoc ::rtry/max-retries 5)
(assoc ::rtry/label "create-thumbnail"))
media (rtry/invoke cfg create-file-thumbnail! params)]
{:uri (files/resolve-public-uri (:id media))})))))

View File

@ -250,7 +250,8 @@
:features (db/create-array conn "text" (:features file))
:data (when (take-snapshot? file)
(:data file))
:changes (blob/encode changes)})
:changes (blob/encode changes)}
{::db/return-keys false})
(db/update! conn :file
{:revn (:revn file)
@ -305,7 +306,7 @@
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
pmap/*tracked* nil]
(-> (files/get-file cfg id :migrate? false)
(feat.fdata/process-pointers deref) ; ensure all pointers resolved
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
(fmg/migrate-file))))))
(d/index-by :id)))

View File

@ -8,9 +8,10 @@
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
@ -25,39 +26,27 @@
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]))
[app.worker :as-alias wrk]))
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"})
(s/def ::data (s/map-of ::us/string any?))
(s/def ::file-id ::us/uuid)
(s/def ::font-id ::us/uuid)
(s/def ::id ::us/uuid)
(s/def ::name ::us/not-empty-string)
(s/def ::project-id ::us/uuid)
(s/def ::share-id ::us/uuid)
(s/def ::style valid-style)
(s/def ::team-id ::us/uuid)
(s/def ::weight valid-weight)
;; --- QUERY: Get font variants
(s/def ::get-font-variants
(s/and
(s/keys :req [::rpc/profile-id]
:opt-un [::team-id
::file-id
::project-id
::share-id])
(fn [o]
(or (contains? o :team-id)
(contains? o :file-id)
(contains? o :project-id)))))
(def ^:private
schema:get-font-variants
[:schema {:title "get-font-variants"}
[:and
[:map
[:team-id {:optional true} ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:project-id {:optional true} ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]]
[::sm/contains-any #{:team-id :file-id :project-id}]]])
(sv/defmethod ::get-font-variants
{::doc/added "1.18"}
{::doc/added "1.18"
::sm/params schema:get-font-variants}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id share-id] :as params}]
(dm/with-open [conn (db/open pool)]
(cond
@ -87,28 +76,31 @@
(declare create-font-variant)
(s/def ::create-font-variant
(s/keys :req [::rpc/profile-id]
:req-un [::team-id
::data
::font-id
::font-family
::font-weight
::font-style]))
(def ^:private schema:create-font-variant
[:map {:title "create-font-variant"}
[:team-id ::sm/uuid]
[:data [:map-of :string :any]]
[:font-id ::sm/uuid]
[:font-family :string]
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
[:font-style [::sm/one-of {:format "string"} valid-style]]])
(sv/defmethod ::create-font-variant
{::doc/added "1.18"
::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(teams/check-edition-permissions! pool profile-id team-id)
(quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(create-font-variant cfg (assoc params :profile-id profile-id))))
::webhooks/event? true
::sm/params schema:create-font-variant}
[cfg {:keys [::rpc/profile-id team-id] :as params}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(teams/check-edition-permissions! conn profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(create-font-variant cfg (assoc params :profile-id profile-id))))))
(defn create-font-variant
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [data] :as params}]
[{:keys [::sto/storage ::db/conn] :as cfg} {:keys [data] :as params}]
(letfn [(generate-missing! [data]
(let [data (media/run {:cmd :generate-fonts :input data})]
(when (and (not (contains? data "font/otf"))
@ -136,6 +128,7 @@
ttf-params (prepare-font data "font/ttf")
wf1-params (prepare-font data "font/woff")
wf2-params (prepare-font data "font/woff2")]
(cond-> {}
(some? otf-params)
(assoc :otf (sto/put-object! storage otf-params))
@ -147,7 +140,7 @@
(assoc :woff2 (sto/put-object! storage wf2-params)))))
(insert-font-variant! [{:keys [woff1 woff2 otf ttf]}]
(db/insert! pool :team-font-variant
(db/insert! conn :team-font-variant
{:id (uuid/next)
:team-id (:team-id params)
:font-id (:font-id params)
@ -168,63 +161,105 @@
;; --- UPDATE FONT FAMILY
(s/def ::update-font
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::id ::name]))
(def ^:private
schema:update-font
[:map {:title "update-font"}
[:team-id ::sm/uuid]
[:id ::sm/uuid]
[:name :string]])
(sv/defmethod ::update-font
{::doc/added "1.18"
::webhooks/event? true}
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id id name]}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(rph/with-meta
(db/update! conn :team-font-variant
{:font-family name}
{:font-id id
:team-id team-id})
{::audit/replace-props {:id id
:name name
:team-id team-id
:profile-id profile-id}})))
::webhooks/event? true
::sm/params schema:update-font}
[cfg {:keys [::rpc/profile-id team-id id name]}]
(db/tx-run! cfg
(fn [{:keys [::db/conn]}]
(teams/check-edition-permissions! conn profile-id team-id)
(db/update! conn :team-font-variant
{:font-family name}
{:font-id id
:team-id team-id})
(rph/with-meta (rph/wrap nil)
{::audit/replace-props {:id id
:name name
:team-id team-id
:profile-id profile-id}}))))
;; --- DELETE FONT
(s/def ::delete-font
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::id]))
(def ^:private
schema:delete-font
[:map {:title "delete-font"}
[:team-id ::sm/uuid]
[:id ::sm/uuid]])
(sv/defmethod ::delete-font
{::doc/added "1.18"
::webhooks/event? true}
[{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(let [font (db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:font-id id :team-id team-id})]
(rph/with-meta (rph/wrap)
{::audit/props {:id id
:team-id team-id
:name (:font-family font)
:profile-id profile-id}}))))
::webhooks/event? true
::sm/params schema:delete-font}
[cfg {:keys [::rpc/profile-id id team-id]}]
(db/tx-run! cfg
(fn [{:keys [::db/conn ::sto/storage] :as cfg}]
(teams/check-edition-permissions! conn profile-id team-id)
(let [fonts (db/query conn :team-font-variant
{:team-id team-id
:font-id id
:deleted-at nil}
{::sql/for-update true})
storage (media/configure-assets-storage storage conn)
tnow (dt/now)]
(when-not (seq fonts)
(ex/raise :type :not-found
:code :object-not-found))
(doseq [font fonts]
(db/update! conn :team-font-variant
{:deleted-at tnow}
{:id (:id font)})
(some->> (:woff1-file-id font) (sto/touch-object! storage))
(some->> (:woff2-file-id font) (sto/touch-object! storage))
(some->> (:ttf-file-id font) (sto/touch-object! storage))
(some->> (:otf-file-id font) (sto/touch-object! storage)))
(rph/with-meta (rph/wrap)
{::audit/props {:id id
:team-id team-id
:name (:font-family (peek fonts))
:profile-id profile-id}})))))
;; --- DELETE FONT VARIANT
(s/def ::delete-font-variant
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::id]))
(def ^:private schema:delete-font-variant
[:map {:title "delete-font-variant"}
[:team-id ::sm/uuid]
[:id ::sm/uuid]])
(sv/defmethod ::delete-font-variant
{::doc/added "1.18"
::webhooks/event? true}
[{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(let [variant (db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:id id :team-id team-id})]
(rph/with-meta (rph/wrap)
{::audit/props {:font-family (:font-family variant)
:font-id (:font-id variant)}}))))
::webhooks/event? true
::sm/params schema:delete-font-variant}
[cfg {:keys [::rpc/profile-id id team-id]}]
(db/tx-run! cfg
(fn [{:keys [::db/conn ::sto/storage] :as cfg}]
(teams/check-edition-permissions! conn profile-id team-id)
(let [variant (db/get conn :team-font-variant
{:id id :team-id team-id}
{::sql/for-update true})
storage (media/configure-assets-storage storage conn)]
(db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:id (:id variant)})
(some->> (:woff1-file-id variant) (sto/touch-object! storage))
(some->> (:woff2-file-id variant) (sto/touch-object! storage))
(some->> (:ttf-file-id variant) (sto/touch-object! storage))
(some->> (:otf-file-id variant) (sto/touch-object! storage))
(rph/with-meta (rph/wrap)
{::audit/props {:font-family (:font-family variant)
:font-id (:font-id variant)}})))))

View File

@ -215,7 +215,7 @@
(-> file
(update :features #(db/create-array conn "text" %))
(update :data blob/encode))
{::db/return-keys? false})
{::db/return-keys false})
;; The file profile creation is optional, so when no profile is
;; present (when this function is called from profile less
@ -231,10 +231,10 @@
{::db/return-keys? false}))
(doseq [params flibs]
(db/insert! conn :file-library-rel params ::db/return-keys? false))
(db/insert! conn :file-library-rel params ::db/return-keys false))
(doseq [params fmeds]
(db/insert! conn :file-media-object params ::db/return-keys? false))
(db/insert! conn :file-media-object params ::db/return-keys false))
file))

View File

@ -23,6 +23,7 @@
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
@ -153,6 +154,11 @@
thumb (when-let [params (::thumb result)]
(sto/put-object! storage params))]
(db/update! conn :file
{:modified-at (dt/now)
:has-media-trimmed false}
{:id file-id})
(db/exec-one! conn [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name

View File

@ -13,6 +13,7 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.email :as eml]
[app.http.session :as session]
[app.loggers.audit :as audit]
@ -99,7 +100,7 @@
;; NOTE: we need to retrieve the profile independently if we use
;; it or not for explicit locking and avoid concurrent updates of
;; the same row/object.
(let [profile (-> (db/get-by-id conn :profile profile-id ::db/for-update? true)
(let [profile (-> (db/get-by-id conn :profile profile-id ::sql/for-update true)
(decode-row))
;; Update the profile map with direct params
@ -164,7 +165,7 @@
(defn- validate-password!
[{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}]
(let [profile (db/get-by-id conn :profile profile-id ::db/for-update? true)]
(let [profile (db/get-by-id conn :profile profile-id ::sql/for-update true)]
(when (and (not= (:password profile) "!")
(not (:valid (verify-password cfg old-password (:password profile)))))
(ex/raise :type :validation
@ -176,7 +177,8 @@
(when-not (db/read-only? conn)
(db/update! conn :profile
{:password (auth/derive-password password)}
{:id id})))
{:id id})
nil))
;; --- MUTATION: Update Photo
@ -202,7 +204,7 @@
(defn update-profile-photo
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}]
(let [photo (upload-photo cfg params)
profile (db/get-by-id pool :profile profile-id ::db/for-update? true)]
profile (db/get-by-id pool :profile profile-id ::sql/for-update true)]
;; Schedule deletion of old photo
(when-let [id (:photo-id profile)]
@ -329,7 +331,7 @@
::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 ::db/for-update? true)
(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)

View File

@ -9,6 +9,7 @@
[app.common.data.macros :as dm]
[app.common.spec :as us]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as webhooks]
[app.rpc :as-alias rpc]
@ -233,7 +234,7 @@
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id)
(let [project (db/get-by-id conn :project id ::db/for-update? true)]
(let [project (db/get-by-id conn :project id ::sql/for-update true)]
(db/update! conn :project
{:name name}
{:id id})
@ -259,7 +260,8 @@
(check-edition-permissions! conn profile-id id)
(let [project (db/update! conn :project
{:deleted-at (dt/now)}
{:id id :is-default false})]
{:id id :is-default false}
{::db/return-keys true})]
(rph/with-meta (rph/wrap)
{::audit/props {:team-id (:team-id project)
:name (:name project)

View File

@ -963,5 +963,6 @@
(let [invitation (db/delete! conn :team-invitation
{:team-id team-id
:email-to (str/lower email)})]
:email-to (str/lower email)}
{::db/return-keys true})]
(rph/wrap nil {::audit/props {:invitation-id (:id invitation)}})))))

View File

@ -95,7 +95,8 @@
:mtype mtype
:error-code nil
:error-count 0}
{:id id})
{:id id}
{::db/return-keys true})
(decode-row)))
(sv/defmethod ::create-webhook

View File

@ -18,46 +18,47 @@
(and (instance? PSQLException e)
(= "23505" (.getSQLState ^PSQLException e))))
(def ^:private always-false (constantly false))
(def ^:private always-false
(constantly false))
(defn wrap-retry
[_ f {:keys [::matches ::sv/name] :or {matches always-false} :as mdata}]
[_ f {:keys [::sv/name] :as mdata}]
(when (::enabled mdata)
(l/debug :hint "wrapping retry" :name name))
(if-let [max-retries (::max-retries mdata)]
(fn [cfg params]
((fn run [retry]
(try
(f cfg params)
(catch Throwable cause
(if (matches cause)
(let [current-retry (inc retry)]
(l/trace :hint "running retry algorithm" :retry current-retry)
(if (<= current-retry max-retries)
(run current-retry)
(throw cause)))
(throw cause))))) 1))
(if (::enabled mdata)
(let [max-retries (get mdata ::max-retries 3)
matches? (get mdata ::when always-false)]
(l/dbg :hint "wrapping retry" :name name :max-retries max-retries)
(fn [cfg params]
((fn recursive-invoke [retry]
(try
(f cfg params)
(catch Throwable cause
(if (matches? cause)
(let [current-retry (inc retry)]
(l/wrn :hint "retrying operation" :retry current-retry :service name)
(if (<= current-retry max-retries)
(recursive-invoke current-retry)
(throw cause)))
(throw cause))))) 1)))
f))
(defmacro with-retry
[{:keys [::when ::max-retries ::label ::db/conn] :or {max-retries 3}} & body]
`(let [conn# ~conn]
(assert (or (nil? conn#) (db/connection? conn#)) "invalid database connection")
(loop [tnum# 1]
(let [result# (let [sp# (some-> conn# db/savepoint)]
(try
(let [result# (do ~@body)]
(some->> sp# (db/release! conn#))
result#)
(catch Throwable cause#
(some->> sp# (db/rollback! conn#))
(if (and (~when cause#) (<= tnum# ~max-retries))
::retry
(throw cause#)))))]
(if (= ::retry result#)
(do
(l/warn :hint "retrying operation" :label ~label :retry tnum#)
(recur (inc tnum#)))
result#)))))
(defn invoke
[{:keys [::db/conn ::max-retries] :or {max-retries 3} :as cfg} f & args]
(assert (db/connection? conn) "invalid database connection")
(loop [rnum 1]
(let [match? (get cfg ::when always-false)
result (let [spoint (db/savepoint conn)]
(try
(let [result (apply f cfg args)]
(db/release! conn spoint)
result)
(catch Throwable cause
(db/rollback! conn spoint)
(if (and (match? cause) (<= rnum max-retries))
::retry
(throw cause)))))]
(if (= ::retry result)
(let [label (get cfg ::label "anonymous")]
(l/warn :hint "retrying operation" :label label :retry rnum)
(recur (inc rnum)))
result))))

View File

@ -65,9 +65,8 @@
(let [res (db/update! conn :profile
params
{:email email
:deleted-at nil}
{::db/return-keys? false})]
(pos? (:next.jdbc/update-count res))))))))
:deleted-at nil})]
(pos? (db/get-update-count res))))))))
(defmethod exec-command :delete-profile
[{:keys [email soft]}]
@ -82,12 +81,10 @@
(let [res (if soft
(db/update! conn :profile
{:deleted-at (dt/now)}
{:email email :deleted-at nil}
{::db/return-keys? false})
{:email email :deleted-at nil})
(db/delete! conn :profile
{:email email}
{::db/return-keys? false}))]
(pos? (:next.jdbc/update-count res))))))
{:email email}))]
(pos? (db/get-update-count res))))))
(defmethod exec-command :search-profile
[{:keys [email]}]

View File

@ -6,8 +6,6 @@
(ns app.srepl.components-v2
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.db :as db]
@ -19,6 +17,13 @@
[promesa.exec.semaphore :as ps]
[promesa.util :as pu]))
(def ^:dynamic *scope* nil)
(def ^:dynamic *semaphore* nil)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PRIVATE HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- print-stats!
[stats]
(->> stats
@ -87,210 +92,228 @@
res (db/exec-one! pool [sql])]
(:count res)))
(defn migrate-file!
[system file-id & {:keys [rollback?] :or {rollback? true}}]
(l/dbg :hint "migrate:start")
(let [tpoint (dt/tpoint)]
(try
(binding [feat/*stats* (atom {})]
(defn- mark-team-migration!
[{:keys [::db/pool]} team-id]
;; We execute this out of transaction because we want this
;; change to be visible to all other sessions before starting
;; the migration
(let [sql (str "UPDATE team SET features = "
" array_append(features, 'ephimeral/v2-migration') "
" WHERE id = ?")]
(db/exec-one! pool [sql team-id])))
(defn- unmark-team-migration!
[{:keys [::db/pool]} team-id]
;; We execute this out of transaction because we want this
;; change to be visible to all other sessions before starting
;; the migration
(let [sql (str "UPDATE team SET features = "
" array_remove(features, 'ephimeral/v2-migration') "
" WHERE id = ?")]
(db/exec-one! pool [sql team-id])))
(def ^:private sql:get-teams
"SELECT id, features
FROM team
WHERE deleted_at IS NULL
ORDER BY created_at ASC")
(defn- get-teams
[conn]
(->> (db/cursor conn sql:get-teams)
(map feat/decode-row)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn migrate-file!
[system file-id & {:keys [rollback? max-procs]
:or {rollback? true}}]
(l/dbg :hint "migrate:start" :rollback rollback?)
(let [tpoint (dt/tpoint)
file-id (if (string? file-id)
(parse-uuid file-id)
file-id)]
(binding [feat/*stats* (atom {})]
(try
(-> (assoc system ::db/rollback rollback?)
(feat/migrate-file! file-id))
(feat/migrate-file! file-id :max-procs max-procs))
(-> (deref feat/*stats*)
(assoc :elapsed (dt/format-duration (tpoint)))))
(assoc :elapsed (dt/format-duration (tpoint))))
(catch Throwable cause
(l/wrn :hint "migrate:error" :cause cause))
(catch Throwable cause
(l/wrn :hint "migrate:error" :cause cause))
(finally
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end" :elapsed elapsed))))))
(defn migrate-files!
[{:keys [::db/pool] :as system}
& {:keys [chunk-size max-jobs max-items start-at preset rollback? skip-on-error validate?]
:or {chunk-size 10
skip-on-error true
max-jobs 10
max-items Long/MAX_VALUE
preset :shutdown-on-failure
rollback? true
validate? false}}]
(letfn [(get-chunk [cursor]
(let [sql (str/concat
"SELECT id, created_at FROM file "
" WHERE created_at < ? AND deleted_at IS NULL "
" ORDER BY created_at desc LIMIT ?")
rows (db/exec! pool [sql cursor chunk-size])]
[(some->> rows peek :created-at) (seq rows)]))
(get-candidates []
(->> (d/iteration get-chunk
:vf second
:kf first
:initk (or start-at (dt/now)))
(take max-items)
(map :id)))]
(l/dbg :hint "migrate:start")
(let [fsem (ps/create :permits max-jobs)
total (get-total-files pool)
stats (atom {:files/total total})
tpoint (dt/tpoint)]
(add-watch stats :progress-report (report-progress-files tpoint))
(binding [feat/*stats* stats
feat/*semaphore* fsem
feat/*skip-on-error* skip-on-error]
(try
(pu/with-open [scope (px/structured-task-scope :preset preset :factory :virtual)]
(run! (fn [file-id]
(ps/acquire! feat/*semaphore*)
(px/submit! scope (fn []
(-> (assoc system ::db/rollback rollback?)
(feat/migrate-file! file-id
:validate? validate?
:throw-on-validate? (not skip-on-error))))))
(get-candidates))
(p/await! scope))
(-> (deref feat/*stats*)
(assoc :elapsed (dt/format-duration (tpoint))))
(catch Throwable cause
(l/dbg :hint "migrate:error" :cause cause))
(finally
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end" :elapsed elapsed))))))))
(finally
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed)))))))
(defn migrate-team!
[{:keys [::db/pool] :as system} team-id
& {:keys [rollback? skip-on-error validate?]
:or {rollback? true skip-on-error true validate? false}}]
(l/dbg :hint "migrate:start")
[{:keys [::db/pool] :as system} team-id & {:keys [rollback? skip-on-error validate? max-procs]
:or {rollback? true
skip-on-error true
validate? false
max-procs 1 }
:as opts}]
(let [total (get-total-files pool :team-id team-id)
stats (atom {:total/files total})
tpoint (dt/tpoint)]
(l/dbg :hint "migrate:start" :rollback rollback?)
(let [team-id (if (string? team-id)
(parse-uuid team-id)
team-id)
total (get-total-files pool :team-id team-id)
stats (atom {:total/files total})
tpoint (dt/tpoint)]
(add-watch stats :progress-report (report-progress-files tpoint))
(try
(binding [feat/*stats* stats
feat/*skip-on-error* skip-on-error]
(binding [feat/*stats* stats
feat/*skip-on-error* skip-on-error]
(try
(mark-team-migration! system team-id)
(-> (assoc system ::db/rollback rollback?)
(feat/migrate-team! team-id
:max-procs max-procs
:validate? validate?
:throw-on-validate? (not skip-on-error)))
(print-stats!
(-> (deref feat/*stats*)
(dissoc :total/files)
(assoc :elapsed (dt/format-duration (tpoint))))))
(assoc :elapsed (dt/format-duration (tpoint)))))
(catch Throwable cause
(l/dbg :hint "migrate:error" :cause cause))
(catch Throwable cause
(l/dbg :hint "migrate:error" :cause cause))
(finally
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end" :elapsed elapsed))))))
(finally
(unmark-team-migration! system team-id)
(defn default-on-end
[stats]
(print-stats!
(-> stats
(update :elapsed/total dt/format-duration)
(dissoc :total/teams))))
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed)))))))
(defn migrate-teams!
[{:keys [::db/pool] :as system}
& {:keys [chunk-size max-jobs max-items start-at
rollback? validate? preset skip-on-error
max-time on-start on-progress on-error on-end]
:or {chunk-size 10000
validate? false
rollback? true
skip-on-error true
on-end default-on-end
preset :shutdown-on-failure
max-jobs Integer/MAX_VALUE
max-items Long/MAX_VALUE}}]
"A REPL helper for migrate all teams.
(letfn [(get-chunk [cursor]
(let [sql (str/concat
"SELECT id, created_at, features FROM team "
" WHERE created_at < ? AND deleted_at IS NULL "
" ORDER BY created_at desc LIMIT ?")
rows (db/exec! pool [sql cursor chunk-size])]
[(some->> rows peek :created-at) (seq rows)]))
This function starts multiple concurrent team migration processes
until thw maximum number of jobs is reached which by default has the
value of `1`. This is controled with the `:max-jobs` option.
(get-candidates []
(->> (d/iteration get-chunk
:vf second
:kf first
:initk (or start-at (dt/now)))
(map #(update % :features db/decode-pgarray #{}))
(remove #(contains? (:features %) "ephimeral/v2-migration"))
(take max-items)
(map :id)))
Each tram migration process also can start multiple procs for
graphics migration, the total of that procs is controled with the
`:max-procs` option.
(migrate-team [team-id]
(try
(-> (assoc system ::db/rollback rollback?)
(feat/migrate-team! team-id
:validate? validate?
:throw-on-validate? (not skip-on-error)))
(catch Throwable cause
(l/err :hint "unexpected error on processing team" :team-id (dm/str team-id) :cause cause))))
Internally, the graphics migration process uses SVGO module which by
default has a limited number of maximum concurent
operations (globally), ensure setting up correct number with
PENPOT_SVGO_MAX_PROCS environment variable."
(process-team [scope tpoint mtime team-id]
(ps/acquire! feat/*semaphore*)
(let [ts (tpoint)]
(if (and mtime (neg? (compare mtime ts)))
(l/inf :hint "max time constraint reached" :elapsed (dt/format-duration ts))
(px/submit! scope (partial migrate-team team-id)))))]
[{:keys [::db/pool] :as system} & {:keys [max-jobs max-procs max-items
rollback? validate? preset
skip-on-error max-time
on-start on-progress on-error on-end]
:or {validate? false
rollback? true
skip-on-error true
preset :shutdown-on-failure
max-jobs 1
max-procs 10
max-items Long/MAX_VALUE}
:as opts}]
(l/dbg :hint "migrate:start")
(let [total (get-total-teams pool)
stats (atom {:total/teams (min total max-items)})
(let [sem (ps/create :permits max-jobs)
total (get-total-teams pool)
stats (atom {:total/teams (min total max-items)})
tpoint (dt/tpoint)
mtime (some-> max-time dt/duration)]
tpoint (dt/tpoint)
mtime (some-> max-time dt/duration)
(when (fn? on-start)
(on-start {:total total :rollback rollback?}))
scope (px/structured-task-scope :preset preset :factory :virtual)
sjobs (ps/create :permits max-jobs)
(add-watch stats :progress-report (report-progress-teams tpoint on-progress))
migrate-team
(fn [{:keys [id features] :as team}]
(ps/acquire! sjobs)
(let [ts (tpoint)]
(cond
(and mtime (neg? (compare mtime ts)))
(do
(l/inf :hint "max time constraint reached"
:team-id (str id)
:elapsed (dt/format-duration ts))
(ps/release! sjobs)
(reduced nil))
(binding [feat/*stats* stats
feat/*semaphore* sem
feat/*skip-on-error* skip-on-error]
(or (contains? features "ephimeral/v2-migration")
(contains? features "components/v2"))
(do
(l/dbg :hint "skip team" :team-id (str id))
(ps/release! sjobs))
:else
(px/submit! scope (fn []
(try
(mark-team-migration! system id)
(-> (assoc system ::db/rollback rollback?)
(feat/migrate-team! id
:max-procs max-procs
:validate? validate?
:throw-on-validate? (not skip-on-error)))
(catch Throwable cause
(l/err :hint "unexpected error on processing team"
:team-id (str id)
:cause cause))
(finally
(ps/release! sjobs)
(unmark-team-migration! system id))))))))]
(l/dbg :hint "migrate:start"
:rollback rollback?
:total total
:max-jobs max-jobs
:max-procs max-procs
:max-items max-items)
(add-watch stats :progress-report (report-progress-teams tpoint on-progress))
(binding [feat/*stats* stats
feat/*skip-on-error* skip-on-error]
(try
(when (fn? on-start)
(on-start {:total total :rollback rollback?}))
(db/tx-run! system
(fn [{:keys [::db/conn]}]
(run! (partial migrate-team)
(->> (get-teams conn)
(take max-items)))))
(try
(pu/with-open [scope (px/structured-task-scope :preset preset
:factory :virtual)]
(loop [candidates (get-candidates)]
(when-let [team-id (first candidates)]
(when (process-team scope tpoint mtime team-id)
(recur (rest candidates)))))
(p/await! scope))
(when (fn? on-end)
(-> (deref stats)
(assoc :elapsed/total (tpoint))
(on-end)))
(catch Throwable cause
(l/dbg :hint "migrate:error" :cause cause)
(when (fn? on-error)
(on-error cause)))
(p/await! scope)
(finally
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end" :elapsed elapsed))))))))
(pu/close! scope)))
(if (fn? on-end)
(-> (deref stats)
(assoc :elapsed/total (tpoint))
(on-end))
(-> (deref stats)
(assoc :elapsed/total (tpoint))
(update :elapsed/total dt/format-duration)
(dissoc :total/teams)
(print-stats!)))
(catch Throwable cause
(l/dbg :hint "migrate:error" :cause cause)
(when (fn? on-error)
(on-error cause)))
(finally
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end"
:rollback rollback?
:elapsed elapsed)))))))

View File

@ -83,7 +83,7 @@
(into [file] (map (fn [{:keys [id]}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer system id)]
(-> (files/get-file system id :migrate? false)
(feat.fdata/process-pointers deref)
(update :data feat.fdata/process-pointers deref)
(pmg/migrate-file))))))
(d/index-by :id))]
(validate/validate-file file libs))))))
@ -101,7 +101,7 @@
(into [file] (map (fn [{:keys [id]}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer system id)]
(-> (files/get-file system id :migrate? false)
(feat.fdata/process-pointers deref)
(update :data feat.fdata/process-pointers deref)
(pmg/migrate-file))))))
(d/index-by :id))
errors (validate/validate-file file libs)

View File

@ -9,8 +9,6 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
@ -172,8 +170,7 @@
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
rs (db/update! pool-or-conn :storage-object
{:touched-at (dt/now)}
{:id id}
{::db/return-keys? false})]
{:id id})]
(pos? (db/get-update-count rs))))
(defn get-object-data
@ -222,231 +219,8 @@
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
res (db/update! pool-or-conn :storage-object
{:deleted-at (dt/now)}
{:id id}
{::db/return-keys? false})]
{:id id})]
(pos? (db/get-update-count res))))
(dm/export impl/resolve-backend)
(dm/export impl/calculate-hash)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Garbage Collection: Permanently delete objects
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; A task responsible to permanently delete already marked as deleted
;; storage files. The storage objects are practically never marked to
;; be deleted directly by the api call. The touched-gc is responsible
;; of collecting the usage of the object and mark it as deleted. Only
;; the TMP files are are created with expiration date in future.
(declare sql:retrieve-deleted-objects-chunk)
(defmethod ig/pre-init-spec ::gc-deleted-task [_]
(s/keys :req [::storage ::db/pool]))
(defmethod ig/prep-key ::gc-deleted-task
[_ cfg]
(assoc cfg ::min-age (dt/duration {:hours 2})))
(defmethod ig/init-key ::gc-deleted-task
[_ {:keys [::db/pool ::storage ::min-age]}]
(letfn [(get-to-delete-chunk [cursor]
(let [sql (str "select s.* "
" from storage_object as s "
" where s.deleted_at is not null "
" and s.deleted_at < ? "
" order by s.deleted_at desc "
" limit 25")
rows (db/exec! pool [sql cursor])]
[(some-> rows peek :deleted-at)
(some->> (seq rows) (d/group-by #(-> % :backend keyword) :id #{}) seq)]))
(get-to-delete-chunks [min-age]
(d/iteration get-to-delete-chunk
:initk (dt/minus (dt/now) min-age)
:vf second
:kf first))
(delete-in-bulk! [backend-id ids]
(try
(db/with-atomic [conn pool]
(let [sql "delete from storage_object where id = ANY(?)"
ids' (db/create-array conn "uuid" ids)
total (-> (db/exec-one! conn [sql ids'])
(db/get-update-count))]
(-> (impl/resolve-backend storage backend-id)
(impl/del-objects-in-bulk ids))
(doseq [id ids]
(l/dbg :hint "gc-deleted: permanently delete storage object" :backend backend-id :id id))
total))
(catch Throwable cause
(l/err :hint "gc-deleted: unexpected error on bulk deletion"
:ids (vec ids)
:cause cause)
0)))]
(fn [params]
(let [min-age (or (some-> params :min-age dt/duration) min-age)]
(loop [total 0
chunks (get-to-delete-chunks min-age)]
(if-let [[backend-id ids] (first chunks)]
(let [deleted (delete-in-bulk! backend-id ids)]
(recur (+ total deleted)
(rest chunks)))
(do
(l/inf :hint "gc-deleted: task finished"
:min-age (dt/format-duration min-age)
:total total)
{:deleted total})))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Garbage Collection: Analyze touched objects
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; This task is part of the garbage collection process of storage
;; objects and is responsible on analyzing the touched objects and
;; mark them for deletion if corresponds.
;;
;; For example: when file_media_object is deleted, the depending
;; storage_object are marked as touched. This means that some files
;; that depend on a concrete storage_object are no longer exists and
;; maybe this storage_object is no longer necessary and can be
;; eligible for elimination. This task periodically analyzes touched
;; objects and mark them as freeze (means that has other references
;; and the object is still valid) or deleted (no more references to
;; this object so is ready to be deleted).
(declare sql:retrieve-file-media-object-nrefs)
(declare sql:retrieve-file-object-thumbnail-nrefs)
(declare sql:retrieve-profile-nrefs)
(declare sql:retrieve-team-font-variant-nrefs)
(declare sql:retrieve-touched-objects-chunk)
(defmethod ig/pre-init-spec ::gc-touched-task [_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::gc-touched-task
[_ {:keys [::db/pool]}]
(letfn [(get-team-font-variant-nrefs [conn id]
(-> (db/exec-one! conn [sql:retrieve-team-font-variant-nrefs id id id id]) :nrefs))
(get-file-media-object-nrefs [conn id]
(-> (db/exec-one! conn [sql:retrieve-file-media-object-nrefs id id]) :nrefs))
(get-profile-nrefs [conn id]
(-> (db/exec-one! conn [sql:retrieve-profile-nrefs id id]) :nrefs))
(get-file-object-thumbnails [conn id]
(-> (db/exec-one! conn [sql:retrieve-file-object-thumbnail-nrefs id]) :nrefs))
(mark-freeze-in-bulk [conn ids]
(db/exec-one! conn ["update storage_object set touched_at=null where id = ANY(?)"
(db/create-array conn "uuid" ids)]))
(mark-delete-in-bulk [conn ids]
(db/exec-one! conn ["update storage_object set deleted_at=now(), touched_at=null where id = ANY(?)"
(db/create-array conn "uuid" ids)]))
;; NOTE: A getter that retrieves the key witch will be used
;; for group ids; previously we have no value, then we
;; introduced the `:reference` prop, and then it is renamed
;; to `:bucket` and now is string instead. This is
;; implemented in this way for backward comaptibilty.
;; NOTE: we use the "file-media-object" as default value for
;; backward compatibility because when we deploy it we can
;; have old backend instances running in the same time as
;; the new one and we can still have storage-objects created
;; without bucket value. And we know that if it does not
;; have value, it means :file-media-object.
(get-bucket [{:keys [metadata]}]
(or (some-> metadata :bucket)
(some-> metadata :reference d/name)
"file-media-object"))
(retrieve-touched-chunk [conn cursor]
(let [rows (->> (db/exec! conn [sql:retrieve-touched-objects-chunk cursor])
(mapv #(d/update-when % :metadata db/decode-transit-pgobject)))]
(when (seq rows)
[(-> rows peek :created-at)
(d/group-by get-bucket :id #{} rows)])))
(retrieve-touched [conn]
(d/iteration (partial retrieve-touched-chunk conn)
:initk (dt/now)
:vf second
:kf first))
(process-objects! [conn get-fn ids bucket]
(loop [to-freeze #{}
to-delete #{}
ids (seq ids)]
(if-let [id (first ids)]
(let [nrefs (get-fn conn id)]
(if (pos? nrefs)
(do
(l/debug :hint "gc-touched: processing storage object"
:id id :status "freeze"
:bucket bucket :refs nrefs)
(recur (conj to-freeze id) to-delete (rest ids)))
(do
(l/debug :hint "gc-touched: processing storage object"
:id id :status "delete"
:bucket bucket :refs nrefs)
(recur to-freeze (conj to-delete id) (rest ids)))))
(do
(some->> (seq to-freeze) (mark-freeze-in-bulk conn))
(some->> (seq to-delete) (mark-delete-in-bulk conn))
[(count to-freeze) (count to-delete)]))))]
(fn [_]
(db/with-atomic [conn pool]
(loop [to-freeze 0
to-delete 0
groups (retrieve-touched conn)]
(if-let [[bucket ids] (first groups)]
(let [[f d] (case bucket
"file-media-object" (process-objects! conn get-file-media-object-nrefs ids bucket)
"team-font-variant" (process-objects! conn get-team-font-variant-nrefs ids bucket)
"file-object-thumbnail" (process-objects! conn get-file-object-thumbnails ids bucket)
"profile" (process-objects! conn get-profile-nrefs ids bucket)
(ex/raise :type :internal
:code :unexpected-unknown-reference
:hint (dm/fmt "unknown reference %" bucket)))]
(recur (+ to-freeze (long f))
(+ to-delete (long d))
(rest groups)))
(do
(l/info :hint "gc-touched: task finished" :to-freeze to-freeze :to-delete to-delete)
{:freeze to-freeze :delete to-delete})))))))
(def sql:retrieve-touched-objects-chunk
"SELECT so.*
FROM storage_object AS so
WHERE so.touched_at IS NOT NULL
AND so.created_at < ?
ORDER by so.created_at DESC
LIMIT 500;")
(def sql:retrieve-file-media-object-nrefs
"select ((select count(*) from file_media_object where media_id = ?) +
(select count(*) from file_media_object where thumbnail_id = ?)) as nrefs")
(def sql:retrieve-file-object-thumbnail-nrefs
"select (select count(*) from file_tagged_object_thumbnail where media_id = ?) as nrefs")
(def sql:retrieve-team-font-variant-nrefs
"select ((select count(*) from team_font_variant where woff1_file_id = ?) +
(select count(*) from team_font_variant where woff2_file_id = ?) +
(select count(*) from team_font_variant where otf_file_id = ?) +
(select count(*) from team_font_variant where ttf_file_id = ?)) as nrefs")
(def sql:retrieve-profile-nrefs
"select ((select count(*) from profile where photo_id = ?) +
(select count(*) from team where photo_id = ?)) as nrefs")

View File

@ -0,0 +1,125 @@
;; 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.storage.gc-deleted
"A task responsible to permanently delete already marked as deleted
storage files. The storage objects are practically never marked to
be deleted directly by the api call.
The touched-gc is responsible of collecting the usage of the object
and mark it as deleted. Only the TMP files are are created with
expiration date in future."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.db :as db]
[app.storage :as-alias sto]
[app.storage.impl :as impl]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(def ^:private sql:lock-sobjects
"SELECT id FROM storage_object
WHERE id = ANY(?::uuid[])
FOR UPDATE
SKIP LOCKED")
(defn- lock-ids
"Perform a select before delete for proper object locking and
prevent concurrent operations and we proceed only with successfully
locked objects."
[conn ids]
(let [ids (db/create-array conn "uuid" ids)]
(->> (db/exec! conn [sql:lock-sobjects ids])
(into #{} (map :id))
(not-empty))))
(def ^:private sql:delete-sobjects
"DELETE FROM storage_object
WHERE id = ANY(?::uuid[])")
(defn- delete-sobjects!
[conn ids]
(let [ids (db/create-array conn "uuid" ids)]
(-> (db/exec-one! conn [sql:delete-sobjects ids])
(db/get-update-count))))
(defn- delete-in-bulk!
[cfg backend-id ids]
;; We run the deletion on a separate transaction. This is
;; because if some exception is raised inside procesing
;; one chunk, it does not affects the rest of the chunks.
(try
(db/tx-run! cfg
(fn [{:keys [::db/conn ::sto/storage]}]
(when-let [ids (lock-ids conn ids)]
(let [total (delete-sobjects! conn ids)]
(-> (impl/resolve-backend storage backend-id)
(impl/del-objects-in-bulk ids))
(doseq [id ids]
(l/dbg :hint "permanently delete storage object"
:id (str id)
:backend (name backend-id)))
total))))
(catch Throwable cause
(l/err :hint "unexpected error on bulk deletion"
:ids ids
:cause cause))))
(defn- group-by-backend
[items]
(d/group-by (comp keyword :backend) :id #{} items))
(def ^:private sql:get-deleted-sobjects
"SELECT s.* FROM storage_object AS s
WHERE s.deleted_at IS NOT NULL
AND s.deleted_at < now() - ?::interval
ORDER BY s.deleted_at ASC")
(defn- get-buckets
[conn min-age]
(let [age (db/interval min-age)]
(sequence
(comp (partition-all 25)
(mapcat group-by-backend))
(db/cursor conn [sql:get-deleted-sobjects age]))))
(defn- clean-deleted!
[{:keys [::db/conn ::min-age] :as cfg}]
(reduce (fn [total [backend-id ids]]
(let [deleted (delete-in-bulk! cfg backend-id ids)]
(+ total (or deleted 0))))
0
(get-buckets conn min-age)))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::sto/storage ::db/pool]))
(defmethod ig/prep-key ::handler
[_ cfg]
(assoc cfg ::min-age (dt/duration {:hours 2})))
(defmethod ig/init-key ::handler
[_ {:keys [::min-age] :as cfg}]
(fn [params]
(let [min-age (dt/duration (or (:min-age params) min-age))]
(db/tx-run! cfg (fn [cfg]
(let [cfg (assoc cfg ::min-age min-age)
total (clean-deleted! cfg)]
(l/inf :hint "task finished"
:min-age (dt/format-duration min-age)
:total total)
{:deleted total}))))))

View File

@ -0,0 +1,208 @@
;; 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.storage.gc-touched
"This task is part of the garbage collection process of storage
objects and is responsible on analyzing the touched objects and mark
them for deletion if corresponds.
For example: when file_media_object is deleted, the depending
storage_object are marked as touched. This means that some files
that depend on a concrete storage_object are no longer exists and
maybe this storage_object is no longer necessary and can be eligible
for elimination. This task periodically analyzes touched objects and
mark them as freeze (means that has other references and the object
is still valid) or deleted (no more references to this object so is
ready to be deleted)."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.db :as db]
[app.storage :as-alias sto]
[app.storage.impl :as impl]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(def ^:private sql:get-team-font-variant-nrefs
"SELECT ((SELECT count(*) FROM team_font_variant WHERE woff1_file_id = ?) +
(SELECT count(*) FROM team_font_variant WHERE woff2_file_id = ?) +
(SELECT count(*) FROM team_font_variant WHERE otf_file_id = ?) +
(SELECT count(*) FROM team_font_variant WHERE ttf_file_id = ?)) AS nrefs")
(defn- get-team-font-variant-nrefs
[conn id]
(-> (db/exec-one! conn [sql:get-team-font-variant-nrefs id id id id])
(get :nrefs)))
(def ^:private
sql:get-file-media-object-nrefs
"SELECT ((SELECT count(*) FROM file_media_object WHERE media_id = ?) +
(SELECT count(*) FROM file_media_object WHERE thumbnail_id = ?)) AS nrefs")
(defn- get-file-media-object-nrefs
[conn id]
(-> (db/exec-one! conn [sql:get-file-media-object-nrefs id id])
(get :nrefs)))
(def ^:private sql:get-profile-nrefs
"SELECT ((SELECT count(*) FROM profile WHERE photo_id = ?) +
(SELECT count(*) FROM team WHERE photo_id = ?)) AS nrefs")
(defn- get-profile-nrefs
[conn id]
(-> (db/exec-one! conn [sql:get-profile-nrefs id id])
(get :nrefs)))
(def ^:private
sql:get-file-object-thumbnail-nrefs
"SELECT (SELECT count(*) FROM file_tagged_object_thumbnail WHERE media_id = ?) AS nrefs")
(defn- get-file-object-thumbnails
[conn id]
(-> (db/exec-one! conn [sql:get-file-object-thumbnail-nrefs id])
(get :nrefs)))
(def ^:private
sql:get-file-thumbnail-nrefs
"SELECT (SELECT count(*) FROM file_thumbnail WHERE media_id = ?) AS nrefs")
(defn- get-file-thumbnails
[conn id]
(-> (db/exec-one! conn [sql:get-file-thumbnail-nrefs id])
(get :nrefs)))
(def ^:private sql:mark-freeze-in-bulk
"UPDATE storage_object
SET touched_at = NULL
WHERE id = ANY(?::uuid[])")
(defn- mark-freeze-in-bulk!
[conn ids]
(let [ids (db/create-array conn "uuid" ids)]
(db/exec-one! conn [sql:mark-freeze-in-bulk ids])))
(def ^:private sql:mark-delete-in-bulk
"UPDATE storage_object
SET deleted_at = now(),
touched_at = NULL
WHERE id = ANY(?::uuid[])")
(defn- mark-delete-in-bulk!
[conn ids]
(let [ids (db/create-array conn "uuid" ids)]
(db/exec-one! conn [sql:mark-delete-in-bulk ids])))
;; NOTE: A getter that retrieves the key which will be used for group
;; ids; previously we have no value, then we introduced the
;; `:reference` prop, and then it is renamed to `:bucket` and now is
;; string instead. This is implemented in this way for backward
;; comaptibilty.
;; NOTE: we use the "file-media-object" as default value for
;; backward compatibility because when we deploy it we can
;; have old backend instances running in the same time as
;; the new one and we can still have storage-objects created
;; without bucket value. And we know that if it does not
;; have value, it means :file-media-object.
(defn- lookup-bucket
[{:keys [metadata]}]
(or (some-> metadata :bucket)
(some-> metadata :reference d/name)
"file-media-object"))
(defn- process-objects!
[conn get-fn ids bucket]
(loop [to-freeze #{}
to-delete #{}
ids (seq ids)]
(if-let [id (first ids)]
(let [nrefs (get-fn conn id)]
(if (pos? nrefs)
(do
(l/debug :hint "processing object"
:id (str id)
:status "freeze"
:bucket bucket :refs nrefs)
(recur (conj to-freeze id) to-delete (rest ids)))
(do
(l/debug :hint "processing object"
:id (str id)
:status "delete"
:bucket bucket :refs nrefs)
(recur to-freeze (conj to-delete id) (rest ids)))))
(do
(some->> (seq to-freeze) (mark-freeze-in-bulk! conn))
(some->> (seq to-delete) (mark-delete-in-bulk! conn))
[(count to-freeze) (count to-delete)]))))
(defn- process-bucket!
[conn bucket ids]
(case bucket
"file-media-object" (process-objects! conn get-file-media-object-nrefs ids bucket)
"team-font-variant" (process-objects! conn get-team-font-variant-nrefs ids bucket)
"file-object-thumbnail" (process-objects! conn get-file-object-thumbnails ids bucket)
"file-thumbnail" (process-objects! conn get-file-thumbnails ids bucket)
"profile" (process-objects! conn get-profile-nrefs ids bucket)
(ex/raise :type :internal
:code :unexpected-unknown-reference
:hint (dm/fmt "unknown reference %" bucket))))
(def ^:private
sql:get-touched-storage-objects
"SELECT so.*
FROM storage_object AS so
WHERE so.touched_at IS NOT NULL
ORDER BY touched_at ASC
FOR UPDATE
SKIP LOCKED")
(defn- group-by-bucket
[row]
(d/group-by lookup-bucket :id #{} row))
(defn- get-buckets
[conn]
(sequence
(comp (map impl/decode-row)
(partition-all 25)
(mapcat group-by-bucket))
(db/cursor conn sql:get-touched-storage-objects)))
(defn- process-touched!
[{:keys [::db/conn]}]
(loop [buckets (get-buckets conn)
freezed 0
deleted 0]
(if-let [[bucket ids] (first buckets)]
(let [[nfo ndo] (process-bucket! conn bucket ids)]
(recur (rest buckets)
(+ freezed nfo)
(+ deleted ndo)))
(do
(l/inf :hint "task finished"
:to-freeze freezed
:to-delete deleted)
{:freeze freezed :delete deleted}))))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [_]
(db/tx-run! cfg process-touched!)))

View File

@ -9,7 +9,7 @@
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.db :as-alias db]
[app.db :as db]
[app.storage :as-alias sto]
[buddy.core.codecs :as bc]
[buddy.core.hash :as bh]
@ -22,6 +22,13 @@
java.nio.file.Path
java.util.UUID))
(defn decode-row
"Decode the storage-object row fields"
[{:keys [metadata] :as row}]
(cond-> row
(some? metadata)
(assoc :metadata (db/decode-transit-pgobject metadata))))
;; --- API Definition
(defmulti put-object (fn [cfg _ _] (::sto/type cfg)))

View File

@ -39,6 +39,7 @@
software.amazon.awssdk.core.async.AsyncRequestBody
software.amazon.awssdk.core.async.AsyncResponseTransformer
software.amazon.awssdk.core.client.config.ClientAsyncConfiguration
software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
software.amazon.awssdk.regions.Region
@ -169,32 +170,34 @@
(defn- build-s3-client
[{:keys [::region ::endpoint ::io-threads]}]
(let [aconfig (-> (ClientAsyncConfiguration/builder)
(.build))
(let [executor (px/resolve-executor :virtual)
aconfig (-> (ClientAsyncConfiguration/builder)
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
(.build))
sconfig (-> (S3Configuration/builder)
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
(.build))
sconfig (-> (S3Configuration/builder)
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
(.build))
thr-num (or io-threads (min 16 (px/get-available-processors)))
hclient (-> (NettyNioAsyncHttpClient/builder)
(.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder)
(.numberOfThreads (int thr-num))))
(.connectionAcquisitionTimeout default-timeout)
(.connectionTimeout default-timeout)
(.readTimeout default-timeout)
(.writeTimeout default-timeout)
(.build))
thr-num (or io-threads (min 16 (px/get-available-processors)))
hclient (-> (NettyNioAsyncHttpClient/builder)
(.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder)
(.numberOfThreads (int thr-num))))
(.connectionAcquisitionTimeout default-timeout)
(.connectionTimeout default-timeout)
(.readTimeout default-timeout)
(.writeTimeout default-timeout)
(.build))
client (let [builder (S3AsyncClient/builder)
builder (.serviceConfiguration ^S3AsyncClientBuilder builder ^S3Configuration sconfig)
builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig)
builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient)
builder (.region ^S3AsyncClientBuilder builder (lookup-region region))
builder (cond-> ^S3AsyncClientBuilder builder
(some? endpoint)
(.endpointOverride (URI. endpoint)))]
(.build ^S3AsyncClientBuilder builder))]
client (let [builder (S3AsyncClient/builder)
builder (.serviceConfiguration ^S3AsyncClientBuilder builder ^S3Configuration sconfig)
builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig)
builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient)
builder (.region ^S3AsyncClientBuilder builder (lookup-region region))
builder (cond-> ^S3AsyncClientBuilder builder
(some? endpoint)
(.endpointOverride (URI. endpoint)))]
(.build ^S3AsyncClientBuilder builder))]
(reify
clojure.lang.IDeref

65
backend/src/app/svgo.clj Normal file
View File

@ -0,0 +1,65 @@
;; 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.svgo
"A SVG Optimizer service"
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.jsrt :as jsrt]
[app.common.logging :as l]
[app.common.spec :as us]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.exec :as px]
[promesa.exec.bulkhead :as bh]
[promesa.exec.semaphore :as ps]
[promesa.util :as pu]))
(def ^:dynamic *semaphore*
"A dynamic variable that can optionally contain a traffic light to
appropriately delimit the use of resources, managed externally."
nil)
(defn optimize
[system data]
(dm/assert! "expect data to be a string" (string? data))
(letfn [(optimize-fn [pool]
(jsrt/run! pool
(fn [context]
(jsrt/set! context "svgData" data)
(jsrt/eval! context "penpotSvgo.optimize(svgData, {plugins: ['safeAndFastPreset']})"))))]
(try
(some-> *semaphore* ps/acquire!)
(let [{:keys [::jsrt/pool ::wrk/executor]} (::optimizer system)]
(dm/assert! "expect optimizer instance" (jsrt/pool? pool))
(px/invoke! executor (partial optimize-fn pool)))
(finally
(some-> *semaphore* ps/release!)))))
(s/def ::max-procs (s/nilable ::us/integer))
(defmethod ig/pre-init-spec ::optimizer [_]
(s/keys :req [::wrk/executor ::max-procs]))
(defmethod ig/prep-key ::optimizer
[_ cfg]
(merge {::max-procs 20} (d/without-nils cfg)))
(defmethod ig/init-key ::optimizer
[_ {:keys [::wrk/executor ::max-procs]}]
(l/inf :hint "initializing svg optimizer pool" :max-procs max-procs)
(let [init (jsrt/resource->source "app/common/svg/optimizer.js")
executor (bh/create :type :executor :executor executor :permits max-procs)]
{::jsrt/pool (jsrt/pool :init init)
::wrk/executor executor}))
(defmethod ig/halt-key! ::optimizer
[_ {:keys [::jsrt/pool]}]
(l/info :hint "stopping svg optimizer pool")
(pu/close! pool))

View File

@ -10,7 +10,6 @@
file is eligible to be garbage collected after some period of
inactivity (the default threshold is 72h)."
(:require
[app.common.data :as d]
[app.common.files.migrations :as pmg]
[app.common.logging :as l]
[app.common.thumbnails :as thc]
@ -30,7 +29,7 @@
[integrant.core :as ig]))
(declare ^:private get-candidates)
(declare ^:private process-file)
(declare ^:private clean-file!)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HANDLER
@ -44,67 +43,61 @@
(assoc cfg ::min-age cf/deletion-delay))
(defmethod ig/init-key ::handler
[_ {:keys [::db/pool] :as cfg}]
[_ cfg]
(fn [{:keys [file-id] :as params}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [min-age (dt/duration (or (:min-age params) (::min-age cfg)))
cfg (-> cfg
(update ::sto/storage media/configure-assets-storage conn)
(assoc ::file-id file-id)
(assoc ::min-age min-age))
(db/with-atomic [conn pool]
(let [min-age (dt/duration (or (:min-age params) (::min-age cfg)))
cfg (-> cfg
(update ::sto/storage media/configure-assets-storage conn)
(assoc ::db/conn conn)
(assoc ::file-id file-id)
(assoc ::min-age min-age))
total (reduce (fn [total file]
(clean-file! cfg file)
(inc total))
0
(get-candidates cfg))]
total (reduce (fn [total file]
(process-file cfg file)
(inc total))
0
(get-candidates cfg))]
(l/inf :hint "task finished"
:min-age (dt/format-duration min-age)
:processed total)
(l/info :hint "task finished" :min-age (dt/format-duration min-age) :processed total)
;; Allow optional rollback passed by params
(when (:rollback? params)
(db/rollback! conn))
;; Allow optional rollback passed by params
(when (:rollback? params)
(db/rollback! conn))
{:processed total}))))
{:processed total})))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IMPL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private
sql:get-candidates-chunk
"select f.id,
sql:get-candidates
"SELECT f.id,
f.data,
f.revn,
f.features,
f.modified_at
from file as f
where f.has_media_trimmed is false
and f.modified_at < now() - ?::interval
and f.modified_at < ?
order by f.modified_at desc
limit 1
for update skip locked")
FROM file AS f
WHERE f.has_media_trimmed IS false
AND f.modified_at < now() - ?::interval
ORDER BY f.modified_at DESC
FOR UPDATE
SKIP LOCKED")
(defn- get-candidates
[{:keys [::db/conn ::min-age ::file-id]}]
(if (uuid? file-id)
(do
(l/warn :hint "explicit file id passed on params" :file-id file-id)
(l/warn :hint "explicit file id passed on params" :file-id (str file-id))
(->> (db/query conn :file {:id file-id})
(map #(update % :features db/decode-pgarray #{}))))
(let [interval (db/interval min-age)
get-chunk (fn [cursor]
(let [rows (db/exec! conn [sql:get-candidates-chunk interval cursor])]
[(some->> rows peek :modified-at)
(map #(update % :features db/decode-pgarray #{}) rows)]))]
(d/iteration get-chunk
:vf second
:kf first
:initk (dt/now)))))
(let [min-age (db/interval min-age)]
(->> (db/cursor conn [sql:get-candidates min-age] {:chunk-size 1})
(map #(update % :features db/decode-pgarray #{}))))))
(defn collect-used-media
"Given a fdata (file data), returns all media references."
@ -134,101 +127,93 @@
(into xform pages)
(into (keys (:media data))))))
(def ^:private sql:mark-file-media-object-deleted
"UPDATE file_media_object
SET deleted_at = now()
WHERE file_id = ? AND id != ALL(?::uuid[])
RETURNING id")
(defn- clean-file-media!
"Performs the garbage collection of file media objects."
[conn file-id data]
(let [used (collect-used-media data)
unused (->> (db/query conn :file-media-object {:file-id file-id})
(remove #(contains? used (:id %))))]
ids (db/create-array conn "uuid" used)
unused (->> (db/exec! conn [sql:mark-file-media-object-deleted file-id ids])
(into #{} (map :id)))]
(doseq [mobj unused]
(l/dbg :hint "delete file media object"
:id (:id mobj)
:media-id (:media-id mobj)
:thumbnail-id (:thumbnail-id mobj))
(doseq [id unused]
(l/trc :hint "mark deleted"
:rel "file-media-object"
:id (str id)
:file-id (str file-id)))
;; NOTE: deleting the file-media-object in the database
;; automatically marks as touched the referenced storage
;; objects. The touch mechanism is needed because many files can
;; point to the same storage objects and we can't just delete
;; them.
(db/delete! conn :file-media-object {:id (:id mobj)}))))
(count unused)))
(def ^:private sql:mark-file-object-thumbnails-deleted
"UPDATE file_tagged_object_thumbnail
SET deleted_at = now()
WHERE file_id = ? AND object_id != ALL(?::text[])
RETURNING object_id")
(defn- clean-file-object-thumbnails!
[{:keys [::db/conn ::sto/storage]} file-id data]
(let [stored (->> (db/query conn :file-tagged-object-thumbnail
{:file-id file-id}
{:columns [:object-id]})
(into #{} (map :object-id)))
[{:keys [::db/conn]} file-id data]
(let [using (->> (vals (:pages-index data))
(into #{} (comp
(mapcat (fn [{:keys [id objects]}]
(->> (ctt/get-frames objects)
(map #(assoc % :page-id id)))))
(mapcat (fn [{:keys [id page-id]}]
(list
(thc/fmt-object-id file-id page-id id "frame")
(thc/fmt-object-id file-id page-id id "component")))))))
using (into #{}
(comp
(mapcat (fn [{:keys [id objects]}]
(->> (ctt/get-frames objects)
(map #(assoc % :page-id id)))))
(mapcat (fn [{:keys [id page-id]}]
(list
(thc/fmt-object-id file-id page-id id "frame")
(thc/fmt-object-id file-id page-id id "component")))))
ids (db/create-array conn "text" using)
unused (->> (db/exec! conn [sql:mark-file-object-thumbnails-deleted file-id ids])
(into #{} (map :object-id)))]
(vals (:pages-index data)))
(doseq [object-id unused]
(l/trc :hint "mark deleted"
:rel "file-tagged-object-thumbnail"
:object-id object-id
:file-id (str file-id)))
unused (set/difference stored using)]
(count unused)))
(when (seq unused)
(let [sql (str "delete from file_tagged_object_thumbnail "
" where file_id=? and object_id=ANY(?)"
" returning media_id")
res (db/exec! conn [sql file-id (db/create-array conn "text" unused)])]
(l/dbg :hint "delete file object thumbnails"
:file-id (str file-id)
:total (count res))
(doseq [media-id (into #{} (keep :media-id) res)]
;; Mark as deleted the storage object related with the
;; photo-id field.
(l/trc :hint "touch file object thumbnail storage object" :id (str media-id))
(sto/touch-object! storage media-id))))))
(def ^:private sql:mark-file-thumbnails-deleted
"UPDATE file_thumbnail
SET deleted_at = now()
WHERE file_id = ? AND revn < ?
RETURNING revn")
(defn- clean-file-thumbnails!
[{:keys [::db/conn ::sto/storage]} file-id revn]
(let [sql (str "delete from file_thumbnail "
" where file_id=? and revn < ? "
" returning media_id")
res (db/exec! conn [sql file-id revn])]
[{:keys [::db/conn]} file-id revn]
(let [unused (->> (db/exec! conn [sql:mark-file-thumbnails-deleted file-id revn])
(into #{} (map :revn)))]
(when (seq res)
(l/dbg :hint "delete file thumbnails"
:file-id (str file-id)
:total (count res))
(doseq [revn unused]
(l/trc :hint "mark deleted"
:rel "file-thumbnail"
:revn revn
:file-id (str file-id)))
(doseq [media-id (into #{} (keep :media-id) res)]
;; Mark as deleted the storage object related with the
;; media-id field.
(l/trc :hint "delete file thumbnail storage object" :id (str media-id))
(sto/del-object! storage media-id)))))
(count unused)))
(def ^:private
sql:get-files-for-library
"select f.data, f.modified_at
from file as f
left join file_library_rel as fl on (fl.file_id = f.id)
where fl.library_file_id = ?
and f.modified_at < ?
and f.deleted_at is null
order by f.modified_at desc
limit 1")
(def ^:private sql:get-files-for-library
"SELECT f.id, f.data, f.modified_at
FROM file AS f
LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id)
WHERE fl.library_file_id = ?
AND f.deleted_at IS null
ORDER BY f.modified_at ASC")
(defn- clean-deleted-components!
"Performs the garbage collection of unreferenced deleted components."
[conn file-id data]
(letfn [(get-files-chunk [cursor]
(let [rows (db/exec! conn [sql:get-files-for-library file-id cursor])]
[(some-> rows peek :modified-at)
(map (comp blob/decode :data) rows)]))
(get-used-components [fdata components]
[{:keys [::db/conn] :as cfg} file-id data]
(letfn [(get-used-components [fdata components]
;; Find which of the components are used in the file.
(into #{}
(filter #(ctf/used-in? fdata file-id % :component))
@ -246,65 +231,85 @@
files-data))]
(let [deleted (into #{} (ctkl/deleted-components-seq data))
unused (->> (d/iteration get-files-chunk :vf second :kf first :initk (dt/now))
unused (->> (db/cursor conn [sql:get-files-for-library file-id] {:chunk-size 1})
(map (fn [{:keys [id data] :as file}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(-> (blob/decode data)
(feat.fdata/process-pointers deref)))))
(cons data)
(get-unused-components deleted)
(mapv :id))]
(when (seq unused)
(l/dbg :hint "clean deleted components" :total (count unused))
(doseq [id unused]
(l/trc :hint "delete component" :component-id (str id) :file-id (str file-id)))
(let [data (reduce ctkl/delete-component data unused)]
(db/update! conn :file
{:data (blob/encode data)}
{:id file-id}))))))
(when-let [data (some->> (seq unused)
(reduce ctkl/delete-component data)
(blob/encode))]
(db/update! conn :file
{:data data}
{:id file-id}))
(count unused))))
(def ^:private sql:get-changes
"SELECT id, data FROM file_change
WHERE file_id = ? AND data IS NOT NULL
ORDER BY created_at ASC")
(def ^:private sql:mark-deleted-data-fragments
"UPDATE file_data_fragment
SET deleted_at = now()
WHERE file_id = ?
AND id != ALL(?::uuid[])
RETURNING id")
(defn- clean-data-fragments!
[conn file-id data]
(letfn [(get-pointers-chunk [cursor]
(let [sql (str "select id, data, created_at "
" from file_change "
" where file_id = ? "
" and data is not null "
" and created_at < ? "
" order by created_at desc "
" limit 1;")
rows (db/exec! conn [sql file-id cursor])]
[(some-> rows peek :created-at)
(mapcat (comp feat.fdata/get-used-pointer-ids blob/decode :data) rows)]))]
(let [used (->> (db/cursor conn [sql:get-changes file-id])
(into (feat.fdata/get-used-pointer-ids data)
(comp (map :data)
(map blob/decode)
(mapcat feat.fdata/get-used-pointer-ids))))
(let [used (into (feat.fdata/get-used-pointer-ids data)
(d/iteration get-pointers-chunk
:vf second
:kf first
:initk (dt/now)))
unused (let [ids (db/create-array conn "uuid" used)]
(->> (db/exec! conn [sql:mark-deleted-data-fragments file-id ids])
(into #{} (map :id))))]
sql (str "select id from file_data_fragment "
" where file_id = ? AND id != ALL(?::uuid[])")
used (db/create-array conn "uuid" used)
rows (db/exec! conn [sql file-id used])]
(doseq [id unused]
(l/trc :hint "mark deleted"
:rel "file-data-fragment"
:id (str id)
:file-id (str file-id)))
(doseq [fragment-id (map :id rows)]
(l/trc :hint "remove unused file data fragment" :id (str fragment-id))
(db/delete! conn :file-data-fragment {:id fragment-id :file-id file-id})))))
(count unused)))
(defn- process-file
[{:keys [::db/conn] :as cfg} {:keys [id data revn modified-at features] :as file}]
(l/dbg :hint "processing file" :file-id (str id) :modified-at modified-at)
(defn- clean-file!
[{:keys [::db/conn] :as cfg} {:keys [id data revn modified-at] :as file}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
pmap/*tracked* (pmap/create-tracked)]
(let [data (-> (blob/decode data)
(assoc :id id)
(pmg/migrate-data))]
(pmg/migrate-data))
(clean-file-media! conn id data)
(clean-file-object-thumbnails! cfg id data)
(clean-file-thumbnails! cfg id revn)
(clean-deleted-components! conn id data)
nfm (clean-file-media! conn id data)
nfot (clean-file-object-thumbnails! cfg id data)
nft (clean-file-thumbnails! cfg id revn)
nc (clean-deleted-components! cfg id data)
ndf (clean-data-fragments! conn id data)]
(when (contains? features "fdata/pointer-map")
(clean-data-fragments! conn id data))
(l/dbg :hint "file clened"
:file-id (str id)
:modified-at (dt/format-instant modified-at)
:media-objects nfm
:thumbnails nft
:object-thumbnails nfot
:components nc
:data-fragments ndf)
;; Mark file as trimmed
(db/update! conn :file

View File

@ -8,7 +8,6 @@
"A maintenance task that performs a general purpose garbage collection
of deleted or unreachable objects."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.config :as cf]
[app.db :as db]
@ -18,12 +17,15 @@
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(declare ^:private delete-profiles!)
(declare ^:private delete-teams!)
(declare ^:private delete-fonts!)
(declare ^:private delete-projects!)
(declare ^:private delete-file-data-fragments!)
(declare ^:private delete-file-media-objects!)
(declare ^:private delete-file-object-thumbnails!)
(declare ^:private delete-file-thumbnails!)
(declare ^:private delete-files!)
(declare ^:private delete-orphan-teams!)
(declare ^:private delete-fonts!)
(declare ^:private delete-profiles!)
(declare ^:private delete-projects!)
(declare ^:private delete-teams!)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool ::sto/storage]))
@ -33,211 +35,306 @@
(assoc cfg ::min-age cf/deletion-delay))
(defmethod ig/init-key ::handler
[_ {:keys [::db/pool ::sto/storage] :as cfg}]
[_ cfg]
(fn [params]
(db/with-atomic [conn pool]
(let [min-age (or (:min-age params) (::min-age cfg))
_ (l/info :hint "gc started"
:min-age (dt/format-duration min-age)
:rollback? (boolean (:rollback? params)))
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
;; Disable deletion protection for the current transaction
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
storage (media/configure-assets-storage storage conn)
cfg (-> cfg
(assoc ::min-age (db/interval min-age))
(assoc ::conn conn)
(assoc ::storage storage))
(let [min-age (dt/duration (or (:min-age params) (::min-age cfg)))
cfg (-> cfg
(assoc ::min-age (db/interval min-age))
(update ::sto/storage media/configure-assets-storage conn))
htotal (+ (delete-profiles! cfg)
(delete-teams! cfg)
(delete-projects! cfg)
(delete-files! cfg)
(delete-fonts! cfg))
stotal (delete-orphan-teams! cfg)]
total (reduce + 0
[(delete-profiles! cfg)
(delete-teams! cfg)
(delete-fonts! cfg)
(delete-projects! cfg)
(delete-files! cfg)
(delete-file-thumbnails! cfg)
(delete-file-object-thumbnails! cfg)
(delete-file-data-fragments! cfg)
(delete-file-media-objects! cfg)])]
(l/info :hint "gc finished"
:deleted htotal
:orphans stotal
:rollback? (boolean (:rollback? params)))
(l/info :hint "task finished"
:deleted total
:rollback? (boolean (:rollback? params)))
(when (:rollback? params)
(db/rollback! conn))
(when (:rollback? params)
(db/rollback! conn))
{:processed (+ stotal htotal)
:orphans stotal}))))
{:processed total})))))
(def ^:private sql:get-profiles-chunk
"select id, photo_id, created_at from profile
where deleted_at is not null
and deleted_at < now() - ?::interval
and created_at < ?
order by created_at desc
limit 10
for update skip locked")
(def ^:private sql:get-profiles
"SELECT id, photo_id FROM profile
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
FOR UPDATE
SKIP LOCKED")
(defn- delete-profiles!
[{:keys [::conn ::min-age ::storage] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-profiles-chunk min-age cursor])]
[(some->> rows peek :created-at) rows]))
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-profiles min-age])
(reduce (fn [total {:keys [id photo-id]}]
(l/trc :hint "permanently delete" :rel "profile" :id (str id))
(process-profile [total {:keys [id photo-id]}]
(l/debug :hint "permanently delete profile" :id (str id))
;; Mark as deleted the storage object
(some->> photo-id (sto/touch-object! storage))
;; Mark as deleted the storage object related with the
;; photo-id field.
(some->> photo-id (sto/touch-object! storage))
;; And finally, permanently delete the profile. The
;; relevant objects will be deleted using DELETE
;; CASCADE database triggers. This may leave orphan
;; teams, but there is a special task for deleting
;; orphaned teams.
(db/delete! conn :profile {:id id})
;; And finally, permanently delete the profile.
(db/delete! conn :profile {:id id})
(inc total))
0)))
(inc total))]
(->> (d/iteration get-chunk :vf second :kf first :initk (dt/now))
(reduce process-profile 0))))
(def ^:private sql:get-teams-chunk
"select id, photo_id, created_at from team
where deleted_at is not null
and deleted_at < now() - ?::interval
and created_at < ?
order by created_at desc
limit 10
for update skip locked")
(def ^:private sql:get-teams
"SELECT deleted_at, id, photo_id FROM team
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
FOR UPDATE
SKIP LOCKED")
(defn- delete-teams!
[{:keys [::conn ::min-age ::storage] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-teams-chunk min-age cursor])]
[(some->> rows peek :created-at) rows]))
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(process-team [total {:keys [id photo-id]}]
(l/debug :hint "permanently delete team" :id (str id))
(->> (db/cursor conn [sql:get-teams min-age])
(reduce (fn [total {:keys [id photo-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "team"
:id (str id)
:deleted-at (dt/format-instant deleted-at))
;; Mark as deleted the storage object related with the
;; photo-id field.
(some->> photo-id (sto/touch-object! storage))
;; Mark as deleted the storage object
(some->> photo-id (sto/touch-object! storage))
;; And finally, permanently delete the team.
(db/delete! conn :team {:id id})
;; And finally, permanently delete the team.
(db/delete! conn :team {:id id})
(inc total))]
;; Mark for deletion in cascade
(db/update! conn :team-font-variant
{:deleted-at deleted-at}
{:team-id id})
(->> (d/iteration get-chunk :vf second :kf first :initk (dt/now))
(reduce process-team 0))))
(db/update! conn :project
{:deleted-at deleted-at}
{:team-id id})
(def ^:private sql:get-orphan-teams-chunk
"select t.id, t.created_at
from team as t
left join team_profile_rel as tpr
on (t.id = tpr.team_id)
where tpr.profile_id is null
and t.created_at < ?
order by t.created_at desc
limit 10
for update of t skip locked;")
(inc total))
0)))
(defn- delete-orphan-teams!
"Find all orphan teams (with no members and mark them for
deletion (soft delete)."
[{:keys [::conn] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-orphan-teams-chunk cursor])]
[(some->> rows peek :created-at) rows]))
(process-team [total {:keys [id]}]
(let [result (db/update! conn :team
{:deleted-at (dt/now)}
{:id id :deleted-at nil}
{::db/return-keys? false})
count (db/get-update-count result)]
(when (pos? count)
(l/debug :hint "mark team for deletion" :id (str id)))
(+ total count)))]
(->> (d/iteration get-chunk :vf second :kf first :initk (dt/now))
(reduce process-team 0))))
(def ^:private sql:get-fonts-chunk
"select id, created_at, woff1_file_id, woff2_file_id, otf_file_id, ttf_file_id
from team_font_variant
where deleted_at is not null
and deleted_at < now() - ?::interval
and created_at < ?
order by created_at desc
limit 10
for update skip locked")
(def ^:private sql:get-fonts
"SELECT id, team_id, deleted_at, woff1_file_id, woff2_file_id, otf_file_id, ttf_file_id
FROM team_font_variant
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
FOR UPDATE
SKIP LOCKED")
(defn- delete-fonts!
[{:keys [::conn ::min-age ::storage] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-fonts-chunk min-age cursor])]
[(some->> rows peek :created-at) rows]))
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-fonts min-age])
(reduce (fn [total {:keys [id team-id deleted-at] :as font}]
(l/trc :hint "permanently delete"
:rel "team-font-variant"
:id (str id)
:team-id (str team-id)
:deleted-at (dt/format-instant deleted-at))
(process-font [total {:keys [id] :as font}]
(l/debug :hint "permanently delete font variant" :id (str id))
;; Mark as deleted the all related storage objects
(some->> (:woff1-file-id font) (sto/touch-object! storage))
(some->> (:woff2-file-id font) (sto/touch-object! storage))
(some->> (:otf-file-id font) (sto/touch-object! storage))
(some->> (:ttf-file-id font) (sto/touch-object! storage))
;; Mark as deleted the all related storage objects
(some->> (:woff1-file-id font) (sto/touch-object! storage))
(some->> (:woff2-file-id font) (sto/touch-object! storage))
(some->> (:otf-file-id font) (sto/touch-object! storage))
(some->> (:ttf-file-id font) (sto/touch-object! storage))
;; And finally, permanently delete the team font variant
(db/delete! conn :team-font-variant {:id id})
;; And finally, permanently delete the team font variant
(db/delete! conn :team-font-variant {:id id})
(inc total))
0)))
(inc total))]
(->> (d/iteration get-chunk :vf second :kf first :initk (dt/now))
(reduce process-font 0))))
(def ^:private sql:get-projects-chunk
"select id, created_at
from project
where deleted_at is not null
and deleted_at < now() - ?::interval
and created_at < ?
order by created_at desc
limit 10
for update skip locked")
(def ^:private sql:get-projects
"SELECT id, deleted_at, team_id
FROM project
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
FOR UPDATE
SKIP LOCKED")
(defn- delete-projects!
[{:keys [::conn ::min-age] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-projects-chunk min-age cursor])]
[(some->> rows peek :created-at) rows]))
[{:keys [::db/conn ::min-age] :as cfg}]
(->> (db/cursor conn [sql:get-projects min-age])
(reduce (fn [total {:keys [id team-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "project"
:id (str id)
:team-id (str team-id)
:deleted-at (dt/format-instant deleted-at))
(process-project [total {:keys [id]}]
(l/debug :hint "permanently delete project" :id (str id))
;; And finally, permanently delete the project.
(db/delete! conn :project {:id id})
;; And finally, permanently delete the project.
(db/delete! conn :project {:id id})
(inc total))]
;; Mark files to be deleted
(db/update! conn :file
{:deleted-at deleted-at}
{:project-id id})
(->> (d/iteration get-chunk :vf second :kf first :initk (dt/now))
(reduce process-project 0))))
(inc total))
0)))
(def ^:private sql:get-files-chunk
"select id, created_at
from file
where deleted_at is not null
and deleted_at < now() - ?::interval
and created_at < ?
order by created_at desc
limit 10
for update skip locked")
(def ^:private sql:get-files
"SELECT id, deleted_at, project_id
FROM file
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
FOR UPDATE
SKIP LOCKED")
(defn- delete-files!
[{:keys [::conn ::min-age] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-files-chunk min-age cursor])]
[(some->> rows peek :created-at) rows]))
[{:keys [::db/conn ::min-age] :as cfg}]
(->> (db/cursor conn [sql:get-files min-age])
(reduce (fn [total {:keys [id deleted-at project-id]}]
(l/trc :hint "permanently delete"
:rel "file"
:id (str id)
:project-id (str project-id)
:deleted-at (dt/format-instant deleted-at))
(process-file [total {:keys [id]}]
(l/debug :hint "permanently delete file" :id (str id))
;; And finally, permanently delete the file.
(db/delete! conn :file {:id id})
(inc total))]
;; And finally, permanently delete the file.
(db/delete! conn :file {:id id})
(->> (d/iteration get-chunk :vf second :kf first :initk (dt/now))
(reduce process-file 0))))
;; Mark file media objects to be deleted
(db/update! conn :file-media-object
{:deleted-at deleted-at}
{:file-id id})
;; Mark thumbnails to be deleted
(db/update! conn :file-thumbnail
{:deleted-at deleted-at}
{:file-id id})
(db/update! conn :file-tagged-object-thumbnail
{:deleted-at deleted-at}
{:file-id id})
(inc total))
0)))
(def ^:private sql:get-file-thumbnails
"SELECT file_id, revn, media_id, deleted_at
FROM file_thumbnail
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
FOR UPDATE
SKIP LOCKED")
(defn delete-file-thumbnails!
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-thumbnails min-age])
(reduce (fn [total {:keys [file-id revn media-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "file-thumbnail"
:file-id (str file-id)
:revn revn
:deleted-at (dt/format-instant deleted-at))
;; Mark as deleted the storage object
(some->> media-id (sto/touch-object! storage))
;; And finally, permanently delete the object
(db/delete! conn :file-thumbnail {:file-id file-id :revn revn})
(inc total))
0)))
(def ^:private sql:get-file-object-thumbnails
"SELECT file_id, object_id, media_id, deleted_at
FROM file_tagged_object_thumbnail
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
FOR UPDATE
SKIP LOCKED")
(defn delete-file-object-thumbnails!
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-object-thumbnails min-age])
(reduce (fn [total {:keys [file-id object-id media-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "file-tagged-object-thumbnail"
:file-id (str file-id)
:object-id object-id
:deleted-at (dt/format-instant deleted-at))
;; Mark as deleted the storage object
(some->> media-id (sto/touch-object! storage))
;; And finally, permanently delete the object
(db/delete! conn :file-tagged-object-thumbnail {:file-id file-id :object-id object-id})
(inc total))
0)))
(def ^:private sql:get-file-data-fragments
"SELECT file_id, id, deleted_at
FROM file_data_fragment
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
FOR UPDATE
SKIP LOCKED")
(defn- delete-file-data-fragments!
[{:keys [::db/conn ::min-age] :as cfg}]
(->> (db/cursor conn [sql:get-file-data-fragments min-age])
(reduce (fn [total {:keys [file-id id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "file-data-fragment"
:id (str id)
:file-id (str file-id)
:deleted-at (dt/format-instant deleted-at))
(db/delete! conn :file-data-fragment {:file-id file-id :id id})
(inc total))
0)))
(def ^:private sql:get-file-media-objects
"SELECT id, file_id, media_id, thumbnail_id, deleted_at
FROM file_media_object
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
FOR UPDATE
SKIP LOCKED")
(defn- delete-file-media-objects!
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-media-objects min-age])
(reduce (fn [total {:keys [id file-id deleted-at] :as fmo}]
(l/trc :hint "permanently delete"
:rel "file-media-object"
:id (str id)
:file-id (str file-id)
:deleted-at (dt/format-instant deleted-at))
;; Mark as deleted the all related storage objects
(some->> (:media-id fmo) (sto/touch-object! storage))
(some->> (:thumbnail-id fmo) (sto/touch-object! storage))
(db/delete! conn :file-media-object {:id id})
(inc total))
0)))

View File

@ -0,0 +1,59 @@
;; 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.tasks.orphan-teams-gc
"A maintenance task that performs orphan teams GC."
(:require
[app.common.logging :as l]
[app.db :as db]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(declare ^:private delete-orphan-teams!)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [params]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(l/inf :hint "gc started" :rollback? (boolean (:rollback? params)))
(let [total (delete-orphan-teams! cfg)]
(l/inf :hint "task finished"
:teams total
:rollback? (boolean (:rollback? params)))
(when (:rollback? params)
(db/rollback! conn))
{:processed total})))))
(def ^:private sql:get-orphan-teams
"SELECT t.id
FROM team AS t
LEFT JOIN team_profile_rel AS tpr
ON (t.id = tpr.team_id)
WHERE tpr.profile_id IS NULL
AND t.deleted_at IS NULL
ORDER BY t.created_at ASC
FOR UPDATE OF t
SKIP LOCKED")
(defn- delete-orphan-teams!
"Find all orphan teams (with no members) and mark them for
deletion (soft delete)."
[{:keys [::db/conn] :as cfg}]
(->> (db/cursor conn sql:get-orphan-teams)
(map :id)
(reduce (fn [total team-id]
(l/trc :hint "mark orphan team for deletion" :id (str team-id))
(db/update! conn :team
{:deleted-at (dt/now)}
{:id team-id})
(inc total))
0)))

View File

@ -42,8 +42,8 @@
(defmethod ig/init-key ::executor
[_ _]
(let [factory (px/thread-factory :prefix "penpot/default/")
executor (px/cached-executor :factory factory :keepalive 30000)]
(let [factory (px/thread-factory :prefix "penpot/default/")
executor (px/cached-executor :factory factory :keepalive 60000)]
(l/inf :hint "starting executor")
(reify
java.lang.AutoCloseable

View File

@ -175,12 +175,11 @@
" WHERE table_schema = 'public' "
" AND table_name != 'migrations';")]
(db/with-atomic [conn *pool*]
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
(let [result (->> (db/exec! conn [sql])
(map :table-name)
(remove #(= "task" %)))
sql (str "TRUNCATE "
(apply str (interpose ", " result))
" CASCADE;")]
(remove #(= "task" %)))]
(doseq [table result]
(db/exec! conn [(str "delete from " table ";")]))))
@ -433,11 +432,11 @@
(us/pretty-explain data))
(= :params-validation (:code data))
(app.common.pprint/pprint
(println
(sm/humanize-explain (::sm/explain data)))
(= :data-validation (:code data))
(app.common.pprint/pprint
(println
(sm/humanize-explain (::sm/explain data)))
(= :service-error (:type data))
@ -512,6 +511,10 @@
[sql]
(db/exec! *pool* sql))
(defn db-exec-one!
[sql]
(db/exec-one! *pool* sql))
(defn db-delete!
[& params]
(apply db/delete! *pool* params))

View File

@ -149,7 +149,7 @@
shape-id (uuid/random)]
;; Preventive file-gc
(let [res (th/run-task! "file-gc" {:min-age 0})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; Check the number of fragments before adding the page
@ -175,7 +175,7 @@
(t/is (= 2 (count rows))))
;; The file-gc should remove unused fragments
(let [res (th/run-task! "file-gc" {:min-age 0})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
@ -203,7 +203,7 @@
(t/is (= 3 (count rows))))
;; The file-gc should remove unused fragments
(let [res (th/run-task! "file-gc" {:min-age 0})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; Check the number of fragments; should be 3 because changes
@ -220,12 +220,23 @@
;; The file-gc should remove fragments related to changes
;; snapshots previously deleted.
(let [res (th/run-task! "file-gc" {:min-age 0})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; Check the number of fragments;
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
(t/is (= 2 (count rows)))))))
;; (pp/pprint rows)
(t/is (= 3 (count rows)))
(t/is (= 2 (count (remove (comp some? :deleted-at) rows)))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
(t/is (= 2 (count rows))))
)))
(t/deftest file-gc-task-with-thumbnails
(letfn [(add-file-media-object [& {:keys [profile-id file-id]}]
@ -301,17 +312,16 @@
;; freeze because of the deduplication (we have uploaded 2 times
;; the same files).
(let [task (:app.storage/gc-touched-task th/*system*)
res (task {:min-age (dt/duration 0)})]
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 2 (:freeze res)))
(t/is (= 0 (:delete res))))
;; run the file-gc task immediately without forced min-age
(let [res (th/run-task! "file-gc")]
(let [res (th/run-task! :file-gc)]
(t/is (= 0 (:processed res))))
;; run the task again
(let [res (th/run-task! "file-gc" {:min-age 0})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; retrieve file and check trimmed attribute
@ -319,8 +329,17 @@
(t/is (true? (:has-media-trimmed row))))
;; check file media objects
(let [rows (th/db-exec! ["select * from file_media_object where file_id = ?" (:id file)])]
(t/is (= 1 (count rows))))
(let [rows (th/db-query :file-media-object {:file-id (:id file)})]
(t/is (= 2 (count rows)))
(t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 2 (:processed res))))
;; check file media objects
(let [rows (th/db-query :file-media-object {:file-id (:id file)})]
(t/is (= 1 (count rows)))
(t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))
;; The underlying storage objects are still available.
(t/is (some? (sto/get-object storage (:media-id fmo2))))
@ -340,15 +359,16 @@
;; Now, we have deleted the usage of pointers to the
;; file-media-objects, if we paste file-gc, they should be marked
;; as deleted.
(let [task (:app.tasks.file-gc/handler th/*system*)
res (task {:min-age (dt/duration 0)})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; Now that file-gc have deleted the file-media-object usage,
;; lets execute the touched-gc task, we should see that two of
;; them are marked to be deleted.
(let [task (:app.storage/gc-touched-task th/*system*)
res (task {:min-age (dt/duration 0)})]
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 0 (:freeze res)))
(t/is (= 2 (:delete res))))
@ -457,11 +477,14 @@
:strokes [{:opacity 1 :stroke-image {:id (:id fmo5) :width 100 :height 100 :mtype "image/jpeg"}}]})}])
;; run the file-gc task immediately without forced min-age
(let [res (th/run-task! "file-gc")]
(let [res (th/run-task! :file-gc)]
(t/is (= 0 (:processed res))))
;; run the task again
(let [res (th/run-task! "file-gc" {:min-age 0})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; retrieve file and check trimmed attribute
@ -494,15 +517,16 @@
;; Now, we have deleted the usage of pointers to the
;; file-media-objects, if we paste file-gc, they should be marked
;; as deleted.
(let [task (:app.tasks.file-gc/handler th/*system*)
res (task {:min-age (dt/duration 0)})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 5 (:processed res))))
;; Now that file-gc have deleted the file-media-object usage,
;; lets execute the touched-gc task, we should see that two of
;; them are marked to be deleted.
(let [task (:app.storage/gc-touched-task th/*system*)
res (task {:min-age (dt/duration 0)})]
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 0 (:freeze res)))
(t/is (= 2 (:delete res))))
@ -515,7 +539,6 @@
(t/is (nil? (sto/get-object storage (:media-id fmo2))))
(t/is (nil? (sto/get-object storage (:media-id fmo1)))))))
(t/deftest file-gc-task-with-object-thumbnails
(letfn [(insert-file-object-thumbnail! [& {:keys [profile-id file-id page-id frame-id]}]
(let [object-id (thc/fmt-object-id file-id page-id frame-id "frame")
@ -609,16 +632,16 @@
;; because of the deduplication (we have uploaded 2 times the
;; same files).
(let [res (th/run-task! "storage-gc-touched" {:min-age (dt/duration 0)})]
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 1 (:freeze res)))
(t/is (= 0 (:delete res))))
;; run the file-gc task immediately without forced min-age
(let [res (th/run-task! "file-gc")]
(let [res (th/run-task! :file-gc)]
(t/is (= 0 (:processed res))))
;; run the task again
(let [res (th/run-task! "file-gc" {:min-age 0})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; retrieve file and check trimmed attribute
@ -648,22 +671,29 @@
:page-id page-id
:id frame-id-2}])
(let [res (th/run-task! "file-gc" {:min-age (dt/duration 0)})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
(let [rows (th/db-exec! ["select * from file_tagged_object_thumbnail where file_id = ?" file-id])]
;; (pp/pprint rows)
(t/is (= 1 (count rows)))
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})]
(t/is (= 2 (count rows)))
(t/is (= 1 (count (remove (comp some? :deleted-at) rows))))
(t/is (= (thc/fmt-object-id file-id page-id frame-id-1 "frame")
(-> rows first :object-id))))
;; Now that file-gc have deleted the object thumbnail lets
;; Now that file-gc have marked for deletion the object
;; thumbnail lets execute the objects-gc task which remove
;; the rows and mark as touched the storage object rows
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 2 (:processed res))))
;; Now that objects-gc have deleted the object thumbnail lets
;; execute the touched-gc task
(let [res (th/run-task! "storage-gc-touched" {:min-age (dt/duration 0)})]
(let [res (th/run-task! "storage-gc-touched" {:min-age 0})]
(t/is (= 1 (:freeze res))))
;; check file media objects
(let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])]
(let [rows (th/db-query :storage-object {:deleted-at nil})]
;; (pp/pprint rows)
(t/is (= 1 (count rows))))
@ -676,31 +706,32 @@
:page-id page-id
:id frame-id-1}])
(let [res (th/run-task! "file-gc" {:min-age (dt/duration 0)})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
(let [rows (th/db-exec! ["select * from file_tagged_object_thumbnail where file_id = ?" file-id])]
(t/is (= 0 (count rows))))
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})]
(t/is (= 1 (count rows)))
(t/is (= 0 (count (remove (comp some? :deleted-at) rows)))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
;; (pp/pprint res)
(t/is (= 1 (:processed res))))
;; We still have th storage objects in the table
(let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])]
(let [rows (th/db-query :storage-object {:deleted-at nil})]
;; (pp/pprint rows)
(t/is (= 1 (count rows))))
;; Now that file-gc have deleted the object thumbnail lets
;; execute the touched-gc task
(let [res (th/run-task! "storage-gc-touched" {:min-age (dt/duration 0)})]
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 1 (:delete res))))
;; check file media objects
(let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])]
(let [rows (th/db-query :storage-object {:deleted-at nil})]
;; (pp/pprint rows)
(t/is (= 0 (count rows)))))))
(t/deftest permissions-checks-creating-file
(let [profile1 (th/create-profile* 1)
profile2 (th/create-profile* 2)
@ -811,13 +842,12 @@
(t/is (th/ex-of-type? error :not-found))))
(t/deftest deletion
(let [task (:app.tasks.objects-gc/handler th/*system*)
profile1 (th/create-profile* 1)
(let [profile1 (th/create-profile* 1)
file (th/create-file* 1 {:project-id (:default-project-id profile1)
:profile-id (:id profile1)})]
;; file is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (task {:min-age (dt/duration 0)})]
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 0 (:processed result))))
;; query the list of files
@ -848,7 +878,7 @@
(t/is (= 0 (count result)))))
;; run permanent deletion (should be noop)
(let [result (task {:min-age (dt/duration {:minutes 1})})]
(let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})]
(t/is (= 0 (:processed result))))
;; query the list of file libraries of a after hard deletion
@ -862,7 +892,7 @@
(t/is (= 0 (count result)))))
;; run permanent deletion
(let [result (task {:min-age (dt/duration 0)})]
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
;; query the list of file libraries of a after hard deletion
@ -874,7 +904,8 @@
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))))
(t/is (= (:type error-data) :not-found))))
))
(t/deftest object-thumbnails-ops
@ -1075,7 +1106,7 @@
(th/sleep 300)
;; run the task
(let [res (th/run-task! "file-gc" {:min-age 0})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; check that object thumbnails are still here
@ -1104,13 +1135,19 @@
(t/is (= 2 (count res))))
;; run the task again
(let [res (th/run-task! "file-gc" {:min-age 0})]
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; check that the unknown frame thumbnail is deleted
(let [res (th/db-exec! ["select * from file_tagged_object_thumbnail"])]
(t/is (= 1 (count res)))))))
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
(t/is (= 2 (count rows)))
(t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 2 (:processed res))))
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
(t/is (= 1 (count rows)))))))
(t/deftest file-thumbnail-ops
(let [prof (th/create-profile* 1 {:is-active true})
@ -1155,12 +1192,19 @@
(t/testing "gc task"
;; make the file eligible for GC waiting 300ms (configured
;; timeout for testing)
(th/sleep 300)
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
(let [res (th/run-task! "file-gc" {:min-age 0})]
(let [rows (th/db-query :file-thumbnail {:file-id (:id file)})]
(t/is (= 2 (count rows)))
(t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
(let [rows (th/db-query :file-thumbnail {:file-id (:id file)})]
(t/is (= 1 (count rows)))))))

View File

@ -6,6 +6,7 @@
(ns backend-tests.rpc-file-thumbnails-test
(:require
[app.common.pprint :as pp]
[app.common.thumbnails :as thc]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
@ -114,9 +115,12 @@
;; Run the File GC task that should remove unused file object
;; thumbnails
(let [result (th/run-task! :file-gc {:min-age (dt/duration 0)})]
(let [result (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 2 (:processed result))))
;; check if row2 related thumbnail row still exists
(let [[row :as rows] (th/db-query :file-tagged-object-thumbnail
{:file-id (:id file)}
@ -136,19 +140,19 @@
(t/is (= 0 (:freeze res))))
;; check that storage object is still exists but is marked as deleted
(let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted? false})]
(let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted false})]
(t/is (some? (:deleted-at row))))
;; Run the storage gc deleted task, it should permanently delete
;; all storage objects related to the deleted thumbnails
(let [result (th/run-task! :storage-gc-deleted {:min-age (dt/duration 0)})]
(let [result (th/run-task! :storage-gc-deleted {:min-age 0})]
(t/is (= 1 (:deleted result))))
(t/is (nil? (sto/get-object storage (:media-id row1))))
(t/is (some? (sto/get-object storage (:media-id row2))))
;; check that storage object is still exists but is marked as deleted
(let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted? false})]
(let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted false})]
(t/is (nil? row))))))
(t/deftest create-file-thumbnail
@ -188,13 +192,12 @@
(let [[row1 row2 :as rows] (th/db-query :file-thumbnail
{:file-id (:id file)}
{:order-by [[:created-at :asc]]})]
{:order-by [[:revn :asc]]})]
(t/is (= 2 (count rows)))
(t/is (= (:file-id data1) (:file-id row1)))
(t/is (= (:revn data1) (:revn row1)))
(t/is (uuid? (:media-id row1)))
(t/is (= (:file-id data2) (:file-id row2)))
(t/is (= (:revn data2) (:revn row2)))
(t/is (uuid? (:media-id row2)))
@ -215,7 +218,10 @@
;; Run the File GC task that should remove unused file object
;; thumbnails
(let [result (th/run-task! :file-gc {:min-age (dt/duration 0)})]
(let [result (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
;; check if row1 related thumbnail row still exists
@ -227,19 +233,54 @@
(t/is (= (:object-id data1) (:object-id row)))
(t/is (uuid? (:media-id row1))))
(let [result (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 1 (:delete result))))
;; Check if storage objects still exists after file-gc
(t/is (nil? (sto/get-object storage (:media-id row1))))
(t/is (some? (sto/get-object storage (:media-id row2))))
(let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted? false})]
(let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted false})]
(t/is (some? (:deleted-at row))))
;; Run the storage gc deleted task, it should permanently delete
;; all storage objects related to the deleted thumbnails
(let [result (th/run-task! :storage-gc-deleted {:min-age (dt/duration 0)})]
(let [result (th/run-task! :storage-gc-deleted {:min-age 0})]
(t/is (= 1 (:deleted result))))
(t/is (some? (sto/get-object storage (:media-id row2)))))))
(t/is (some? (sto/get-object storage (:media-id row2))))
)))
(t/deftest error-on-direct-storage-obj-deletion
(let [storage (::sto/storage th/*system*)
profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false
:revn 3})
data1 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:revn 2
:media {:filename "sample.jpg"
:size 7923
:path (th/tempfile "backend_tests/test_files/sample2.jpg")
:mtype "image/jpeg"}}]
(let [out (th/command! data1)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (contains? (:result out) :uri)))
(let [[row1 :as rows] (th/db-query :file-thumbnail {:file-id (:id file)})]
(t/is (= 1 (count rows)))
(t/is (thrown? org.postgresql.util.PSQLException
(th/db-delete! :storage-object {:id (:media-id row1)}))))))
(t/deftest get-file-object-thumbnail
(let [storage (::sto/storage th/*system*)
@ -279,6 +320,3 @@
(let [result (:result out)]
(t/is (contains? result "test-key-2"))))))

View File

@ -92,3 +92,192 @@
:font-family
:font-weight
:font-style))))
(t/deftest font-deletion-1
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
font-id (uuid/custom 10 1)
data1 (-> (io/resource "backend_tests/test_files/font-1.woff")
io/input-stream
io/read-as-bytes)
data2 (-> (io/resource "backend_tests/test_files/font-2.woff")
io/input-stream
io/read-as-bytes)]
;; Create front variant
(let [params {::th/type :create-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 400
:font-style "normal"
:data {"font/woff" data1}}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out))))
(let [params {::th/type :create-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 500
:font-style "normal"
:data {"font/woff" data2}}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 6 (:freeze res))))
(let [params {::th/type :delete-font
::rpc/profile-id (:id prof)
:team-id team-id
:id font-id}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 6 (:freeze res)))
(t/is (= 0 (:delete res))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 2 (:processed res))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 0 (:freeze res)))
(t/is (= 6 (:delete res))))
))
(t/deftest font-deletion-2
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
font-id (uuid/custom 10 1)
data1 (-> (io/resource "backend_tests/test_files/font-1.woff")
io/input-stream
io/read-as-bytes)
data2 (-> (io/resource "backend_tests/test_files/font-2.woff")
io/input-stream
io/read-as-bytes)]
;; Create front variant
(let [params {::th/type :create-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 400
:font-style "normal"
:data {"font/woff" data1}}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out))))
(let [params {::th/type :create-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:font-id (uuid/custom 10 2)
:font-family "somefont"
:font-weight 400
:font-style "normal"
:data {"font/woff" data2}}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 6 (:freeze res))))
(let [params {::th/type :delete-font
::rpc/profile-id (:id prof)
:team-id team-id
:id font-id}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 3 (:freeze res)))
(t/is (= 0 (:delete res))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 0 (:freeze res)))
(t/is (= 3 (:delete res))))
))
(t/deftest font-deletion-3
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
font-id (uuid/custom 10 1)
data1 (-> (io/resource "backend_tests/test_files/font-1.woff")
io/input-stream
io/read-as-bytes)
data2 (-> (io/resource "backend_tests/test_files/font-2.woff")
io/input-stream
io/read-as-bytes)
params1 {::th/type :create-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 400
:font-style "normal"
:data {"font/woff" data1}}
params2 {::th/type :create-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 500
:font-style "normal"
:data {"font/woff" data2}}
out1 (th/command! params1)
out2 (th/command! params2)]
;; (th/print-result! out1)
(t/is (nil? (:error out1)))
(t/is (nil? (:error out2)))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 6 (:freeze res))))
(let [params {::th/type :delete-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:id (-> out1 :result :id)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 3 (:freeze res)))
(t/is (= 0 (:delete res))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 0 (:freeze res)))
(t/is (= 3 (:delete res))))
))

View File

@ -125,7 +125,7 @@
;; profile is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 0 (:processed result))))
;; Request profile to be deleted
@ -144,12 +144,20 @@
(t/is (= 1 (count (:result out)))))
;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age (dt/duration "-1m")})]
(t/is (= 2 (:processed result))))
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
(let [row (th/db-get :team
{:id (:default-team-id prof)}
{::db/remove-deleted? false})]
{::db/remove-deleted false})]
(t/is (nil? (:deleted-at row))))
(let [result (th/run-task! :orphan-teams-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
(let [row (th/db-get :team
{:id (:default-team-id prof)}
{::db/remove-deleted false})]
(t/is (dt/instant? (:deleted-at row))))
;; query profile after delete
@ -158,67 +166,9 @@
out (th/command! params)]
;; (th/print-result! out)
(let [result (:result out)]
(t/is (= uuid/zero (:id result)))))))
(t/is (= uuid/zero (:id result)))))
(t/deftest profile-immediate-deletion
(let [prof1 (th/create-profile* 1)
prof2 (th/create-profile* 2)
file (th/create-file* 1 {:profile-id (:id prof1)
:project-id (:default-project-id prof1)
:is-shared false})
team (th/create-team* 1 {:profile-id (:id prof1)})
_ (th/create-team-role* {:team-id (:id team)
:profile-id (:id prof2)
:role :admin})]
;; profile is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
(t/is (= 0 (:orphans result)))
(t/is (= 0 (:processed result))))
;; just delete the profile
(th/db-delete! :profile {:id (:id prof1)})
;; query files after profile deletion, expecting not found
(let [params {::th/type :get-project-files
::rpc/profile-id (:id prof1)
:project-id (:default-project-id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :not-found (:type edata)))))
;; the files and projects still exists on the database
(let [files (th/db-query :file {:project-id (:default-project-id prof1)})
projects (th/db-query :project {:team-id (:default-team-id prof1)})]
(t/is (= 1 (count files)))
(t/is (= 1 (count projects))))
;; execute the gc task
(let [result (th/run-task! :objects-gc {:min-age (dt/duration "-1m")})]
(t/is (= 1 (:processed result)))
(t/is (= 1 (:orphans result))))
;; Check the deletion flag on the default profile team
(let [row (th/db-get :team
{:id (:default-team-id prof1)}
{::db/remove-deleted? false})]
(t/is (dt/instant? (:deleted-at row))))
;; Check the deletion flag on the shared team
(let [row (th/db-get :team
{:id (:id team)}
{::db/remove-deleted? false})]
(t/is (nil? (:deleted-at row))))
;; Check the roles on the shared team
(let [rows (th/db-query :team-profile-rel {:team-id (:id team)})]
(t/is (= 1 (count rows)))
(t/is (= (:id prof2) (get-in rows [0 :profile-id])))
(t/is (= false (get-in rows [0 :is-owner]))))))
))
(t/deftest registration-domain-whitelist
(let [whitelist #{"gmail.com" "hey.com" "ya.ru"}]

View File

@ -172,14 +172,13 @@
(t/deftest test-deletion
(let [task (:app.tasks.objects-gc/handler th/*system*)
profile1 (th/create-profile* 1)
(let [profile1 (th/create-profile* 1)
project (th/create-project* 1 {:team-id (:default-team-id profile1)
:profile-id (:id profile1)})]
;; project is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (task {:min-age (dt/duration 0)})]
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 0 (:processed result))))
;; query the list of projects
@ -187,6 +186,7 @@
::rpc/profile-id (:id profile1)
:team-id (:default-team-id profile1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
@ -210,7 +210,7 @@
(t/is (= 1 (count result)))))
;; run permanent deletion (should be noop)
(let [result (task {:min-age (dt/duration {:minutes 1})})]
(let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})]
(t/is (= 0 (:processed result))))
;; query the list of files of a after soft deletion
@ -224,7 +224,7 @@
(t/is (= 0 (count result)))))
;; run permanent deletion
(let [result (task {:min-age (dt/duration 0)})]
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
;; query the list of files of a after hard deletion

View File

@ -269,76 +269,6 @@
(t/is (= 1 (count members)))
(t/is (true? (-> members first :can-edit))))))))
(t/deftest team-deletion
(let [profile1 (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile1)})
pool (:app.db/pool th/*system*)
data {::th/type :delete-team
::rpc/profile-id (:id profile1)
:team-id (:id team)}]
;; team is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
(t/is (= 0 (:processed result))))
;; query the list of teams
(let [data {::th/type :get-teams
::rpc/profile-id (:id profile1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= 2 (count result)))
(t/is (= (:id team) (get-in result [1 :id])))
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
;; Request team to be deleted
(let [params {::th/type :delete-team
::rpc/profile-id (:id profile1)
:id (:id team)}
out (th/command! params)]
(t/is (th/success? out)))
;; query the list of teams after soft deletion
(let [data {::th/type :get-teams
::rpc/profile-id (:id profile1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= 1 (count result)))
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
;; run permanent deletion (should be noop)
(let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})]
(t/is (= 0 (:processed result))))
;; query the list of projects after hard deletion
(let [data {::th/type :get-projects
::rpc/profile-id (:id profile1)
:team-id (:id team)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :not-found (:type edata)))))
;; run permanent deletion
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
(t/is (= 1 (:processed result))))
;; query the list of projects of a after hard deletion
(let [data {::th/type :get-projects
::rpc/profile-id (:id profile1)
:team-id (:id team)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :not-found (:type edata)))))))
(t/deftest query-team-invitations
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
@ -418,3 +348,119 @@
(t/is (th/success? out))
(t/is (nil? (:result out)))
(t/is (nil? res)))))
(t/deftest team-deletion-1
(let [profile1 (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile1)})
pool (:app.db/pool th/*system*)
data {::th/type :delete-team
::rpc/profile-id (:id profile1)
:team-id (:id team)}]
;; team is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
(t/is (= 0 (:processed result))))
;; query the list of teams
(let [data {::th/type :get-teams
::rpc/profile-id (:id profile1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= 2 (count result)))
(t/is (= (:id team) (get-in result [1 :id])))
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
;; Request team to be deleted
(let [params {::th/type :delete-team
::rpc/profile-id (:id profile1)
:id (:id team)}
out (th/command! params)]
(t/is (th/success? out)))
;; query the list of teams after soft deletion
(let [data {::th/type :get-teams
::rpc/profile-id (:id profile1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= 1 (count result)))
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
;; run permanent deletion (should be noop)
(let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})]
(t/is (= 0 (:processed result))))
;; query the list of projects after hard deletion
(let [data {::th/type :get-projects
::rpc/profile-id (:id profile1)
:team-id (:id team)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :not-found (:type edata)))))
;; run permanent deletion
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
(t/is (= 2 (:processed result))))
;; query the list of projects of a after hard deletion
(let [data {::th/type :get-projects
::rpc/profile-id (:id profile1)
:team-id (:id team)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :not-found (:type edata)))))))
(t/deftest team-deletion-2
(let [storage (-> (:app.storage/storage th/*system*)
(assoc ::sto/backend :assets-fs))
prof (th/create-profile* 1)
team (th/create-team* 1 {:profile-id (:id prof)})
proj (th/create-project* 1 {:profile-id (:id prof)
:team-id (:id team)})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id team)
:is-shared false})
mfile {:filename "sample.jpg"
:path (th/tempfile "backend_tests/test_files/sample.jpg")
:mtype "image/jpeg"
:size 312043}]
(let [params {::th/type :upload-file-media-object
::rpc/profile-id (:id prof)
:file-id (:id file)
:is-local true
:name "testfile"
:content mfile}
out (th/command! params)]
(t/is (nil? (:error out))))
(let [params {::th/type :delete-team
::rpc/profile-id (:id prof)
:id (:id team)}
out (th/command! params)]
#_(th/print-result! out)
(t/is (nil? (:error out))))
(let [rows (th/db-exec! ["select * from team where id = ?" (:id team)])]
(t/is (= 1 (count rows)))
(t/is (dt/instant? (:deleted-at (first rows)))))
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 5 (:processed result))))
))

View File

@ -113,7 +113,7 @@
(let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted res))))
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object;"])]
(let [res (th/db-exec-one! ["select count(*) from storage_object;"])]
(t/is (= 2 (:count res))))))
(t/deftest test-touched-gc-task-1
@ -156,29 +156,33 @@
(t/is (= (:media-id result-1) (:media-id result-2)))
;; now we proceed to manually delete one file-media-object
(db/exec-one! th/*pool* ["delete from file_media_object where id = ?" (:id result-1)])
(th/db-update! :file-media-object
{:deleted-at (dt/now)}
{:id (:id result-1)})
;; run the objects gc task for permanent deletion
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; check that we still have all the storage objects
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object"])]
(let [res (th/db-exec-one! ["select count(*) from storage_object"])]
(t/is (= 2 (:count res))))
;; now check if the storage objects are touched
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where touched_at is not null"])]
(let [res (th/db-exec-one! ["select count(*) from storage_object where touched_at is not null"])]
(t/is (= 2 (:count res))))
;; run the touched gc task
(let [task (:app.storage/gc-touched-task th/*system*)
res (task {})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 2 (:freeze res)))
(t/is (= 0 (:delete res))))
;; now check that there are no touched objects
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where touched_at is not null"])]
(let [res (th/db-exec-one! ["select count(*) from storage_object where touched_at is not null"])]
(t/is (= 0 (:count res))))
;; now check that all objects are marked to be deleted
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where deleted_at is not null"])]
(let [res (th/db-exec-one! ["select count(*) from storage_object where deleted_at is not null"])]
(t/is (= 0 (:count res)))))))
@ -231,31 +235,35 @@
(t/is (nil? (:error out2)))
;; run the touched gc task
(let [task (:app.storage/gc-touched-task th/*system*)
res (task {})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 5 (:freeze res)))
(t/is (= 0 (:delete res)))
(let [result-1 (:result out1)
result-2 (:result out2)]
;; now we proceed to manually delete one team-font-variant
(db/exec-one! th/*pool* ["delete from team_font_variant where id = ?" (:id result-2)])
(th/db-update! :team-font-variant
{:deleted-at (dt/now)}
{:id (:id result-2)})
;; run the objects gc task for permanent deletion
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
;; revert touched state to all storage objects
(db/exec-one! th/*pool* ["update storage_object set touched_at=now()"])
(th/db-exec-one! ["update storage_object set touched_at=now()"])
;; Run the task again
(let [res (task {})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 2 (:freeze res)))
(t/is (= 3 (:delete res))))
;; now check that there are no touched objects
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where touched_at is not null"])]
(let [res (th/db-exec-one! ["select count(*) from storage_object where touched_at is not null"])]
(t/is (= 0 (:count res))))
;; now check that all objects are marked to be deleted
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where deleted_at is not null"])]
(let [res (th/db-exec-one! ["select count(*) from storage_object where deleted_at is not null"])]
(t/is (= 3 (:count res))))))))
(t/deftest test-touched-gc-task-3
@ -289,28 +297,28 @@
result-2 (:result out2)]
;; now we proceed to manually mark all storage objects touched
(db/exec-one! th/*pool* ["update storage_object set touched_at=now()"])
(th/db-exec! ["update storage_object set touched_at=now()"])
;; run the touched gc task
(let [task (:app.storage/gc-touched-task th/*system*)
res (task {})]
(let [res (th/run-task! "storage-gc-touched" {:min-age 0})]
(t/is (= 2 (:freeze res)))
(t/is (= 0 (:delete res))))
;; check that we have all object in the db
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where deleted_at is null"])]
(t/is (= 2 (:count res)))))
(let [rows (th/db-exec! ["select * from storage_object"])]
(t/is (= 2 (count rows)))))
;; now we proceed to manually delete all file_media_object
(db/exec-one! th/*pool* ["delete from file_media_object"])
(th/db-exec! ["update file_media_object set deleted_at = now()"])
(let [res (th/run-task! "objects-gc" {:min-age 0})]
(t/is (= 2 (:processed res))))
;; run the touched gc task
(let [task (:app.storage/gc-touched-task th/*system*)
res (task {})]
(let [res (th/run-task! "storage-gc-touched" {:min-age 0})]
(t/is (= 0 (:freeze res)))
(t/is (= 2 (:delete res))))
;; check that we have all no objects
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where deleted_at is null"])]
(t/is (= 0 (:count res))))))
(let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])]
(t/is (= 0 (count rows))))))

View File

@ -517,6 +517,13 @@
(->> (apply c/iteration args)
(concat-all)))
(defn add-at-index
"Insert an element in a vector at an arbitrary index"
[coll index element]
(assert (vector? coll))
(let [[before after] (split-at index coll)]
(concat-vec [] before [element] after)))
(defn insert-at-index
"Insert a list of elements at the given index of a previous list.
Replace all existing elems."

View File

@ -358,13 +358,15 @@
(defn changed-attrs
"Returns the list of attributes that will change when `update-fn` is applied"
[object objects update-fn {:keys [attrs]}]
[object objects update-fn {:keys [attrs with-objects?]}]
(let [changed?
(fn [old new attr]
(let [old-val (get old attr)
new-val (get new attr)]
(not= old-val new-val)))
new-obj (update-fn object objects)]
new-obj (if with-objects?
(update-fn object objects)
(update-fn object))]
(when-not (= object new-obj)
(let [attrs (or attrs (d/concat-set (keys object) (keys new-obj)))]
(filter (partial changed? object new-obj) attrs)))))
@ -375,8 +377,8 @@
([changes ids update-fn]
(update-shapes changes ids update-fn nil))
([changes ids update-fn {:keys [attrs ignore-geometry? ignore-touched]
:or {ignore-geometry? false ignore-touched false}}]
([changes ids update-fn {:keys [attrs ignore-geometry? ignore-touched with-objects?]
:or {ignore-geometry? false ignore-touched false with-objects? false}}]
(assert-container-id! changes)
(assert-objects! changes)
(let [page-id (::page-id (meta changes))
@ -412,7 +414,7 @@
update-shape
(fn [changes id]
(let [old-obj (get objects id)
new-obj (update-fn old-obj objects)]
new-obj (if with-objects? (update-fn old-obj objects) (update-fn old-obj))]
(if (= old-obj new-obj)
changes
(let [[rops uops] (-> (or attrs (d/concat-set (keys old-obj) (keys new-obj)))

View File

@ -16,7 +16,7 @@
[app.common.uuid :as uuid]))
(defn prepare-add-shape
[changes shape objects _selected]
[changes shape objects]
(let [index (:index (meta shape))
id (:id shape)
@ -34,7 +34,7 @@
(cond-> (some? cell)
(pcb/update-shapes [(:parent-id shape)] #(ctl/push-into-cell % [id] row column)))
(cond-> (ctl/grid-layout? objects (:parent-id shape))
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells)))]
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells {:with-objects? true})))]
[shape changes]))
(defn prepare-move-shapes-into-frame
@ -50,58 +50,120 @@
(pcb/update-shapes ordered-indexes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true)))
(pcb/change-parent frame-id to-move-shapes 0)
(cond-> (ctl/grid-layout? objects frame-id)
(pcb/update-shapes [frame-id] ctl/assign-cells))
(pcb/reorder-grid-children [frame-id]))
(-> (pcb/update-shapes [frame-id] ctl/assign-cells {:with-objects? true})
(pcb/reorder-grid-children [frame-id]))))
changes)))
(defn prepare-create-artboard-from-selection
[changes id parent-id objects selected index frame-name without-fill?]
(let [selected-objs (map #(get objects %) selected)
new-index (or index
(cfh/get-index-replacement selected objects))]
(when (d/not-empty? selected)
(let [srect (gsh/shapes->rect selected-objs)
selected-id (first selected)
([changes id parent-id objects selected index frame-name without-fill?]
(prepare-create-artboard-from-selection
changes id parent-id objects selected index frame-name without-fill? nil))
frame-id (dm/get-in objects [selected-id :frame-id])
parent-id (or parent-id (dm/get-in objects [selected-id :parent-id]))
([changes id parent-id objects selected index frame-name without-fill? target-cell-id]
(let [selected-objs (map #(get objects %) selected)
new-index (or index
(cfh/get-index-replacement selected objects))]
(when (d/not-empty? selected)
(let [srect (gsh/shapes->rect selected-objs)
selected-id (first selected)
attrs {:type :frame
:x (:x srect)
:y (:y srect)
:width (:width srect)
:height (:height srect)}
frame-id (dm/get-in objects [selected-id :frame-id])
parent-id (or parent-id (dm/get-in objects [selected-id :parent-id]))
base-parent (get objects parent-id)
shape (cts/setup-shape
(cond-> attrs
(some? id)
(assoc :id id)
attrs {:type :frame
:x (:x srect)
:y (:y srect)
:width (:width srect)
:height (:height srect)}
(some? frame-name)
(assoc :name frame-name)
shape (cts/setup-shape
(cond-> attrs
(some? id)
(assoc :id id)
:always
(assoc :frame-id frame-id
:parent-id parent-id
:shapes (into [] selected))
(some? frame-name)
(assoc :name frame-name)
:always
(with-meta {:index new-index})
:always
(assoc :frame-id frame-id
:parent-id parent-id
:shapes (into [] selected))
(or (not= frame-id uuid/zero) without-fill?)
(assoc :fills [] :hide-in-viewer true)))
:always
(with-meta {:index new-index})
[shape changes]
(prepare-add-shape changes shape objects selected)
(or (not= frame-id uuid/zero) without-fill?)
(assoc :fills [] :hide-in-viewer true)))
changes
(prepare-move-shapes-into-frame changes (:id shape) selected objects)
[shape changes]
(prepare-add-shape changes shape objects)
changes
(cond-> changes
(ctl/grid-layout? objects (:parent-id shape))
(-> (pcb/update-shapes [(:parent-id shape)] ctl/assign-cells)
(pcb/reorder-grid-children [(:parent-id shape)])))]
changes
(prepare-move-shapes-into-frame changes (:id shape) selected objects)
[shape changes]))))
changes
(cond-> changes
(ctl/grid-layout? objects (:parent-id shape))
(-> (cond-> (some? target-cell-id)
(pcb/update-shapes
[(:parent-id shape)]
(fn [parent]
(-> parent
(assoc :layout-grid-cells (:layout-grid-cells base-parent))
(assoc-in [:layout-grid-cells target-cell-id :shapes] [id])
(assoc :position :auto)))))
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells {:with-objects? true})
(pcb/reorder-grid-children [(:parent-id shape)])))]
[shape changes])))))
(defn prepare-create-empty-artboard
[changes frame-id parent-id objects index frame-name without-fill? target-cell-id]
(let [base-parent (get objects parent-id)
attrs {:type :frame
:x 0
:y 0
:width 0.01
:height 0.01}
shape (cts/setup-shape
(cond-> attrs
(some? frame-id)
(assoc :id frame-id)
(some? frame-name)
(assoc :name frame-name)
:always
(assoc :frame-id frame-id
:parent-id parent-id
:shapes [])
:always
(with-meta {:index index})
(or (not= frame-id uuid/zero) without-fill?)
(assoc :fills [] :hide-in-viewer true)))
[shape changes]
(prepare-add-shape changes shape objects)
changes
(cond-> changes
(ctl/grid-layout? objects (:parent-id shape))
(-> (cond-> (some? target-cell-id)
(pcb/update-shapes
[(:parent-id shape)]
(fn [parent]
(-> parent
(assoc :layout-grid-cells (:layout-grid-cells base-parent))
(assoc-in [:layout-grid-cells target-cell-id :shapes] [frame-id])
(assoc :position :auto)))))
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells {:with-objects? true})
(pcb/reorder-grid-children [(:parent-id shape)])))]
[shape changes]))

View File

@ -0,0 +1,18 @@
;; 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.common.geom.line)
(defn line-value
[[{px :x py :y} {vx :x vy :y}] {:keys [x y]}]
(let [a vy
b (- vx)
c (+ (* (- vy) px) (* vx py))]
(+ (* a x) (* b y) c)))
(defn is-inside-lines?
[line-1 line-2 pos]
(< (* (line-value line-1 pos) (line-value line-2 pos)) 0))

View File

@ -113,8 +113,8 @@
([_objects shapes parent]
(if (empty? shapes)
(-> {:layout-grid-columns [{:type :auto} {:type :auto}]
:layout-grid-rows [{:type :auto} {:type :auto}]}
(-> {:layout-grid-columns [ctl/default-track-value ctl/default-track-value]
:layout-grid-rows [ctl/default-track-value ctl/default-track-value]}
(ctl/create-cells [1 1 2 2]))
(let [all-shapes-rect (gco/shapes->rect shapes)
@ -149,8 +149,8 @@
0
(/ (- (:height all-shapes-rect) total-rows-height) (dec num-rows)))
layout-grid-rows (mapv (constantly (array-map :type :auto)) rows)
layout-grid-columns (mapv (constantly (array-map :type :auto)) cols)
layout-grid-rows (mapv (constantly ctl/default-track-value) rows)
layout-grid-columns (mapv (constantly ctl/default-track-value) cols)
parent-childs-vector (gpt/to-vec (gpo/origin (:points parent)) (gpt/point all-shapes-rect))
p-left (:x parent-childs-vector)

View File

@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.line :as gl]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.grid-layout.layout-data :as ld]
@ -182,18 +183,6 @@
(-> (ctm/add-modifiers fill-modifiers)
(ctm/move position-delta)))))
(defn line-value
[[{px :x py :y} {vx :x vy :y}] {:keys [x y]}]
(let [a vy
b (- vx)
c (+ (* (- vy) px) (* vx py))]
(+ (* a x) (* b y) c)))
(defn is-inside-lines?
[line-1 line-2 pos]
(< (* (line-value line-1 pos) (line-value line-2 pos)) 0))
(defn get-position-grid-coord
[{:keys [layout-bounds row-tracks column-tracks]} position]
@ -206,7 +195,7 @@
(fn is-inside-track? [{:keys [start-p size] :as track}]
(let [unit-v (vfn 1)
end-p (gpt/add start-p (ofn size))]
(is-inside-lines? [start-p unit-v] [end-p unit-v] position)))))
(gl/is-inside-lines? [start-p unit-v] [end-p unit-v] position)))))
make-min-distance-track
(fn [type]
@ -214,8 +203,8 @@
(fn [[selected selected-dist] [cur-idx {:keys [start-p size] :as track}]]
(let [unit-v (vfn 1)
end-p (gpt/add start-p (ofn size))
dist-1 (mth/abs (line-value [start-p unit-v] position))
dist-2 (mth/abs (line-value [end-p unit-v] position))]
dist-1 (mth/abs (gl/line-value [start-p unit-v] position))
dist-2 (mth/abs (gl/line-value [end-p unit-v] position))]
(if (or (< dist-1 selected-dist) (< dist-2 selected-dist))
[[cur-idx track] (min dist-1 dist-2)]

View File

@ -9,9 +9,6 @@
#?(:cljs ["./svg/optimizer.js" :as svgo])
#?(:clj [clojure.xml :as xml]
:cljs [tubax.core :as tubax])
#?(:clj [integrant.core :as ig])
#?(:clj [app.common.jsrt :as jsrt])
#?(:clj [app.common.logging :as l])
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
@ -1053,21 +1050,6 @@
:height (d/parse-integer (:height attrs) 0)})))]
(reduce-nodes redfn [] svg-data )))
#?(:cljs
(defn optimize
([input] (optimize input nil))
([input options]
(svgo/optimize input (clj->js options))))
:clj
(defn optimize
[pool data]
(dm/assert! "expected a valid pool" (jsrt/pool? pool))
(dm/assert! "expect data to be a string" (string? data))
(jsrt/run! pool
(fn [context]
(jsrt/set! context "svgData" data)
(jsrt/eval! context "penpotSvgo.optimize(svgData, {})")))))
#?(:clj
(defn- secure-parser-factory
[^InputStream input ^XMLHandler handler]
@ -1091,15 +1073,9 @@
(dm/with-open [istream (IOUtils/toInputStream text "UTF-8")]
(xml/parse istream secure-parser-factory)))))
#?(:clj
(defmethod ig/init-key ::optimizer
[_ _]
(l/info :hint "initializing svg optimizer pool")
(let [init (jsrt/resource->source "app/common/svg/optimizer.js")]
(jsrt/pool :init init))))
#?(:clj
(defmethod ig/halt-key! ::optimizer
[_ pool]
(l/info :hint "stopping svg optimizer pool")
(.close ^java.lang.AutoCloseable pool)))
;; FIXME pass correct plugin set
#?(:cljs
(defn optimize
([input] (optimize input nil))
([input options]
(svgo/optimize input (clj->js options)))))

View File

@ -29431,12 +29431,13 @@ const optimize = (input, config) => {
exports.optimize = optimize;
},{"./svgo/parser.js":322,"./svgo/plugins.js":324,"./svgo/stringifier.js":381,"./svgo/tools.js":383,"lodash/isPlainObject":308}],320:[function(require,module,exports){
},{"./svgo/parser.js":322,"./svgo/plugins.js":324,"./svgo/stringifier.js":382,"./svgo/tools.js":384,"lodash/isPlainObject":308}],320:[function(require,module,exports){
'use strict';
exports.builtin = [
require('./plugins/default.js'),
require('./plugins/safe.js'),
require('./plugins/safeAndFast.js'),
require('./plugins/addAttributesToSVGElement.js'),
require('./plugins/addClassesToSVGElement.js'),
require('./plugins/cleanupAttrs.js'),
@ -29489,7 +29490,7 @@ exports.builtin = [
require('./plugins/sortDefsChildren.js'),
];
},{"./plugins/addAttributesToSVGElement.js":328,"./plugins/addClassesToSVGElement.js":329,"./plugins/cleanupAttrs.js":331,"./plugins/cleanupEnableBackground.js":332,"./plugins/cleanupIds.js":333,"./plugins/cleanupListOfValues.js":334,"./plugins/cleanupNumericValues.js":335,"./plugins/collapseGroups.js":336,"./plugins/convertColors.js":337,"./plugins/convertEllipseToCircle.js":338,"./plugins/convertPathData.js":339,"./plugins/convertShapeToPath.js":340,"./plugins/convertStyleToAttrs.js":341,"./plugins/convertTransform.js":342,"./plugins/default.js":343,"./plugins/inlineStyles.js":344,"./plugins/mergePaths.js":345,"./plugins/mergeStyles.js":346,"./plugins/minifyStyles.js":347,"./plugins/moveElemsAttrsToGroup.js":348,"./plugins/moveGroupAttrsToElems.js":349,"./plugins/prefixIds.js":350,"./plugins/removeAttributesBySelector.js":351,"./plugins/removeAttrs.js":352,"./plugins/removeComments.js":353,"./plugins/removeDesc.js":354,"./plugins/removeDimensions.js":355,"./plugins/removeDoctype.js":356,"./plugins/removeEditorsNSData.js":357,"./plugins/removeElementsByAttr.js":358,"./plugins/removeEmptyAttrs.js":359,"./plugins/removeEmptyContainers.js":360,"./plugins/removeEmptyText.js":361,"./plugins/removeHiddenElems.js":362,"./plugins/removeMetadata.js":363,"./plugins/removeNonInheritableGroupAttrs.js":364,"./plugins/removeOffCanvasPaths.js":365,"./plugins/removeRasterImages.js":366,"./plugins/removeScriptElement.js":367,"./plugins/removeStyleElement.js":368,"./plugins/removeTitle.js":369,"./plugins/removeUnknownsAndDefaults.js":370,"./plugins/removeUnusedNS.js":371,"./plugins/removeUselessDefs.js":372,"./plugins/removeUselessStrokeAndFill.js":373,"./plugins/removeViewBox.js":374,"./plugins/removeXMLNS.js":375,"./plugins/removeXMLProcInst.js":376,"./plugins/reusePaths.js":377,"./plugins/safe.js":378,"./plugins/sortAttrs.js":379,"./plugins/sortDefsChildren.js":380}],321:[function(require,module,exports){
},{"./plugins/addAttributesToSVGElement.js":328,"./plugins/addClassesToSVGElement.js":329,"./plugins/cleanupAttrs.js":331,"./plugins/cleanupEnableBackground.js":332,"./plugins/cleanupIds.js":333,"./plugins/cleanupListOfValues.js":334,"./plugins/cleanupNumericValues.js":335,"./plugins/collapseGroups.js":336,"./plugins/convertColors.js":337,"./plugins/convertEllipseToCircle.js":338,"./plugins/convertPathData.js":339,"./plugins/convertShapeToPath.js":340,"./plugins/convertStyleToAttrs.js":341,"./plugins/convertTransform.js":342,"./plugins/default.js":343,"./plugins/inlineStyles.js":344,"./plugins/mergePaths.js":345,"./plugins/mergeStyles.js":346,"./plugins/minifyStyles.js":347,"./plugins/moveElemsAttrsToGroup.js":348,"./plugins/moveGroupAttrsToElems.js":349,"./plugins/prefixIds.js":350,"./plugins/removeAttributesBySelector.js":351,"./plugins/removeAttrs.js":352,"./plugins/removeComments.js":353,"./plugins/removeDesc.js":354,"./plugins/removeDimensions.js":355,"./plugins/removeDoctype.js":356,"./plugins/removeEditorsNSData.js":357,"./plugins/removeElementsByAttr.js":358,"./plugins/removeEmptyAttrs.js":359,"./plugins/removeEmptyContainers.js":360,"./plugins/removeEmptyText.js":361,"./plugins/removeHiddenElems.js":362,"./plugins/removeMetadata.js":363,"./plugins/removeNonInheritableGroupAttrs.js":364,"./plugins/removeOffCanvasPaths.js":365,"./plugins/removeRasterImages.js":366,"./plugins/removeScriptElement.js":367,"./plugins/removeStyleElement.js":368,"./plugins/removeTitle.js":369,"./plugins/removeUnknownsAndDefaults.js":370,"./plugins/removeUnusedNS.js":371,"./plugins/removeUselessDefs.js":372,"./plugins/removeUselessStrokeAndFill.js":373,"./plugins/removeViewBox.js":374,"./plugins/removeXMLNS.js":375,"./plugins/removeXMLProcInst.js":376,"./plugins/reusePaths.js":377,"./plugins/safe.js":378,"./plugins/safeAndFast.js":379,"./plugins/sortAttrs.js":380,"./plugins/sortDefsChildren.js":381}],321:[function(require,module,exports){
'use strict';
const isTag = (node) => {
@ -30102,7 +30103,7 @@ const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => {
};
exports.stringifyPathData = stringifyPathData;
},{"./tools.js":383}],324:[function(require,module,exports){
},{"./tools.js":384}],324:[function(require,module,exports){
'use strict';
const { builtin } = require('./builtin.js');
@ -33826,7 +33827,7 @@ const applyMatrixToPathData = (pathData, matrix) => {
}
};
},{"../style.js":382,"../tools.js":383,"./_collections.js":325,"./_path.js":326,"./_transforms.js":327}],331:[function(require,module,exports){
},{"../style.js":383,"../tools.js":384,"./_collections.js":325,"./_path.js":326,"./_transforms.js":327}],331:[function(require,module,exports){
'use strict';
exports.name = 'cleanupAttrs';
@ -33948,7 +33949,7 @@ exports.fn = (root) => {
};
};
},{"../xast.js":384}],333:[function(require,module,exports){
},{"../xast.js":385}],333:[function(require,module,exports){
'use strict';
const { visitSkip } = require('../xast.js');
@ -34207,7 +34208,7 @@ exports.fn = (_root, params) => {
};
};
},{"../xast.js":384,"./_collections.js":325}],334:[function(require,module,exports){
},{"../xast.js":385,"./_collections.js":325}],334:[function(require,module,exports){
'use strict';
const { removeLeadingZero } = require('../tools.js');
@ -34345,7 +34346,7 @@ exports.fn = (_root, params) => {
};
};
},{"../tools.js":383}],335:[function(require,module,exports){
},{"../tools.js":384}],335:[function(require,module,exports){
'use strict';
const { removeLeadingZero } = require('../tools.js');
@ -34451,7 +34452,7 @@ exports.fn = (_root, params) => {
};
};
},{"../tools.js":383}],336:[function(require,module,exports){
},{"../tools.js":384}],336:[function(require,module,exports){
'use strict';
const { inheritableAttrs, elemsGroups } = require('./_collections.js');
@ -35838,7 +35839,7 @@ function data2Path(params, pathData) {
}, '');
}
},{"../style.js":382,"../tools.js":383,"../xast.js":384,"./_collections.js":325,"./_path.js":326,"./applyTransforms.js":330}],340:[function(require,module,exports){
},{"../style.js":383,"../tools.js":384,"../xast.js":385,"./_collections.js":325,"./_path.js":326,"./applyTransforms.js":330}],340:[function(require,module,exports){
'use strict';
const { stringifyPathData } = require('../path.js');
@ -36004,7 +36005,7 @@ exports.fn = (root, params) => {
};
};
},{"../path.js":323,"../xast.js":384}],341:[function(require,module,exports){
},{"../path.js":323,"../xast.js":385}],341:[function(require,module,exports){
'use strict';
const { attrsGroups } = require('./_collections');
@ -36506,7 +36507,7 @@ const smartRound = (precision, data) => {
return data;
};
},{"../tools.js":383,"./_transforms.js":327}],343:[function(require,module,exports){
},{"../tools.js":384,"./_transforms.js":327}],343:[function(require,module,exports){
'use strict';
const { createPreset } = require('../tools.js');
@ -36590,7 +36591,7 @@ const presetDefault = createPreset({
module.exports = presetDefault;
},{"../tools.js":383,"./cleanupAttrs.js":331,"./cleanupEnableBackground.js":332,"./cleanupIds.js":333,"./cleanupNumericValues.js":335,"./collapseGroups.js":336,"./convertColors.js":337,"./convertEllipseToCircle.js":338,"./convertPathData.js":339,"./convertShapeToPath.js":340,"./convertTransform.js":342,"./inlineStyles.js":344,"./mergePaths.js":345,"./mergeStyles.js":346,"./minifyStyles.js":347,"./moveElemsAttrsToGroup.js":348,"./moveGroupAttrsToElems.js":349,"./removeComments.js":353,"./removeDesc.js":354,"./removeDoctype.js":356,"./removeEditorsNSData.js":357,"./removeEmptyAttrs.js":359,"./removeEmptyContainers.js":360,"./removeEmptyText.js":361,"./removeHiddenElems.js":362,"./removeMetadata.js":363,"./removeNonInheritableGroupAttrs.js":364,"./removeTitle.js":369,"./removeUnknownsAndDefaults.js":370,"./removeUnusedNS.js":371,"./removeUselessDefs.js":372,"./removeUselessStrokeAndFill.js":373,"./removeViewBox.js":374,"./removeXMLProcInst.js":376,"./sortAttrs.js":379,"./sortDefsChildren.js":380}],344:[function(require,module,exports){
},{"../tools.js":384,"./cleanupAttrs.js":331,"./cleanupEnableBackground.js":332,"./cleanupIds.js":333,"./cleanupNumericValues.js":335,"./collapseGroups.js":336,"./convertColors.js":337,"./convertEllipseToCircle.js":338,"./convertPathData.js":339,"./convertShapeToPath.js":340,"./convertTransform.js":342,"./inlineStyles.js":344,"./mergePaths.js":345,"./mergeStyles.js":346,"./minifyStyles.js":347,"./moveElemsAttrsToGroup.js":348,"./moveGroupAttrsToElems.js":349,"./removeComments.js":353,"./removeDesc.js":354,"./removeDoctype.js":356,"./removeEditorsNSData.js":357,"./removeEmptyAttrs.js":359,"./removeEmptyContainers.js":360,"./removeEmptyText.js":361,"./removeHiddenElems.js":362,"./removeMetadata.js":363,"./removeNonInheritableGroupAttrs.js":364,"./removeTitle.js":369,"./removeUnknownsAndDefaults.js":370,"./removeUnusedNS.js":371,"./removeUselessDefs.js":372,"./removeUselessStrokeAndFill.js":373,"./removeViewBox.js":374,"./removeXMLProcInst.js":376,"./sortAttrs.js":380,"./sortDefsChildren.js":381}],344:[function(require,module,exports){
'use strict';
const csstree = require('css-tree');
@ -36937,7 +36938,7 @@ exports.fn = (root, params) => {
};
};
},{"../xast.js":384,"css-tree":25,"csso":138}],345:[function(require,module,exports){
},{"../xast.js":385,"css-tree":25,"csso":138}],345:[function(require,module,exports){
'use strict';
const { detachNodeFromParent } = require('../xast.js');
@ -37035,7 +37036,7 @@ exports.fn = (root, params) => {
};
};
},{"../style.js":382,"../xast.js":384,"./_path.js":326}],346:[function(require,module,exports){
},{"../style.js":383,"../xast.js":385,"./_path.js":326}],346:[function(require,module,exports){
'use strict';
const { visitSkip, detachNodeFromParent } = require('../xast.js');
@ -37119,7 +37120,7 @@ exports.fn = () => {
};
};
},{"../xast.js":384}],347:[function(require,module,exports){
},{"../xast.js":385}],347:[function(require,module,exports){
'use strict';
const csso = require('csso');
@ -37364,7 +37365,7 @@ exports.fn = (root) => {
};
};
},{"../xast.js":384,"./_collections.js":325}],349:[function(require,module,exports){
},{"../xast.js":385,"./_collections.js":325}],349:[function(require,module,exports){
'use strict';
const { pathElems, referencesProps } = require('./_collections.js');
@ -37742,7 +37743,7 @@ exports.fn = (root, params) => {
return {};
};
},{"../xast.js":384}],352:[function(require,module,exports){
},{"../xast.js":385}],352:[function(require,module,exports){
'use strict';
exports.name = 'removeAttrs';
@ -37924,7 +37925,7 @@ exports.fn = () => {
};
};
},{"../xast.js":384}],354:[function(require,module,exports){
},{"../xast.js":385}],354:[function(require,module,exports){
'use strict';
const { detachNodeFromParent } = require('../xast.js');
@ -37963,7 +37964,7 @@ exports.fn = (root, params) => {
};
};
},{"../xast.js":384}],355:[function(require,module,exports){
},{"../xast.js":385}],355:[function(require,module,exports){
'use strict';
exports.name = 'removeDimensions';
@ -38046,7 +38047,7 @@ exports.fn = () => {
};
};
},{"../xast.js":384}],357:[function(require,module,exports){
},{"../xast.js":385}],357:[function(require,module,exports){
'use strict';
const { detachNodeFromParent } = require('../xast.js');
@ -38110,7 +38111,7 @@ exports.fn = (_root, params) => {
};
};
},{"../xast.js":384,"./_collections.js":325}],358:[function(require,module,exports){
},{"../xast.js":385,"./_collections.js":325}],358:[function(require,module,exports){
'use strict';
const { detachNodeFromParent } = require('../xast.js');
@ -38183,7 +38184,7 @@ exports.fn = (root, params) => {
};
};
},{"../xast.js":384}],359:[function(require,module,exports){
},{"../xast.js":385}],359:[function(require,module,exports){
'use strict';
const { attrsGroups } = require('./_collections.js');
@ -38270,7 +38271,7 @@ exports.fn = () => {
};
};
},{"../xast.js":384,"./_collections.js":325}],361:[function(require,module,exports){
},{"../xast.js":385,"./_collections.js":325}],361:[function(require,module,exports){
'use strict';
const { detachNodeFromParent } = require('../xast.js');
@ -38321,7 +38322,7 @@ exports.fn = (root, params) => {
};
};
},{"../xast.js":384}],362:[function(require,module,exports){
},{"../xast.js":385}],362:[function(require,module,exports){
'use strict';
const {
@ -38631,7 +38632,7 @@ exports.fn = (root, params) => {
};
};
},{"../path.js":323,"../style.js":382,"../xast.js":384}],363:[function(require,module,exports){
},{"../path.js":323,"../style.js":383,"../xast.js":385}],363:[function(require,module,exports){
'use strict';
const { detachNodeFromParent } = require('../xast.js');
@ -38658,7 +38659,7 @@ exports.fn = () => {
};
};
},{"../xast.js":384}],364:[function(require,module,exports){
},{"../xast.js":385}],364:[function(require,module,exports){
'use strict';
const {
@ -38815,7 +38816,7 @@ exports.fn = () => {
};
};
},{"../path.js":323,"../xast.js":384,"./_path.js":326}],366:[function(require,module,exports){
},{"../path.js":323,"../xast.js":385,"./_path.js":326}],366:[function(require,module,exports){
'use strict';
const { detachNodeFromParent } = require('../xast.js');
@ -38846,7 +38847,7 @@ exports.fn = () => {
};
};
},{"../xast.js":384}],367:[function(require,module,exports){
},{"../xast.js":385}],367:[function(require,module,exports){
'use strict';
const { detachNodeFromParent } = require('../xast.js');
@ -38873,7 +38874,7 @@ exports.fn = () => {
};
};
},{"../xast.js":384}],368:[function(require,module,exports){
},{"../xast.js":385}],368:[function(require,module,exports){
'use strict';
const { detachNodeFromParent } = require('../xast.js');
@ -38900,7 +38901,7 @@ exports.fn = () => {
};
};
},{"../xast.js":384}],369:[function(require,module,exports){
},{"../xast.js":385}],369:[function(require,module,exports){
'use strict';
const { detachNodeFromParent } = require('../xast.js');
@ -38927,7 +38928,7 @@ exports.fn = () => {
};
};
},{"../xast.js":384}],370:[function(require,module,exports){
},{"../xast.js":385}],370:[function(require,module,exports){
'use strict';
const { visitSkip, detachNodeFromParent } = require('../xast.js');
@ -39135,7 +39136,7 @@ exports.fn = (root, params) => {
};
};
},{"../style.js":382,"../xast.js":384,"./_collections":325}],371:[function(require,module,exports){
},{"../style.js":383,"../xast.js":385,"./_collections":325}],371:[function(require,module,exports){
'use strict';
exports.name = 'removeUnusedNS';
@ -39252,7 +39253,7 @@ const collectUsefulNodes = (node, usefulNodes) => {
}
};
},{"../xast.js":384,"./_collections.js":325}],373:[function(require,module,exports){
},{"../xast.js":385,"./_collections.js":325}],373:[function(require,module,exports){
'use strict';
const { visit, visitSkip, detachNodeFromParent } = require('../xast.js');
@ -39390,7 +39391,7 @@ exports.fn = (root, params) => {
};
};
},{"../style.js":382,"../xast.js":384,"./_collections.js":325}],374:[function(require,module,exports){
},{"../style.js":383,"../xast.js":385,"./_collections.js":325}],374:[function(require,module,exports){
'use strict';
exports.name = 'removeViewBox';
@ -39497,7 +39498,7 @@ exports.fn = () => {
};
};
},{"../xast.js":384}],377:[function(require,module,exports){
},{"../xast.js":385}],377:[function(require,module,exports){
'use strict';
exports.name = 'reusePaths';
@ -39678,7 +39679,72 @@ const presetSafe = createPreset({
module.exports = presetSafe;
},{"../tools.js":383,"./cleanupAttrs.js":331,"./cleanupEnableBackground.js":332,"./cleanupIds.js":333,"./cleanupNumericValues.js":335,"./collapseGroups.js":336,"./convertColors.js":337,"./convertEllipseToCircle.js":338,"./convertPathData.js":339,"./convertTransform.js":342,"./inlineStyles.js":344,"./mergePaths.js":345,"./mergeStyles.js":346,"./minifyStyles.js":347,"./removeComments.js":353,"./removeDesc.js":354,"./removeDoctype.js":356,"./removeEditorsNSData.js":357,"./removeEmptyAttrs.js":359,"./removeEmptyContainers.js":360,"./removeEmptyText.js":361,"./removeHiddenElems.js":362,"./removeMetadata.js":363,"./removeNonInheritableGroupAttrs.js":364,"./removeTitle.js":369,"./removeUnknownsAndDefaults.js":370,"./removeUnusedNS.js":371,"./removeUselessDefs.js":372,"./removeUselessStrokeAndFill.js":373,"./removeViewBox.js":374,"./removeXMLProcInst.js":376,"./sortDefsChildren.js":380}],379:[function(require,module,exports){
},{"../tools.js":384,"./cleanupAttrs.js":331,"./cleanupEnableBackground.js":332,"./cleanupIds.js":333,"./cleanupNumericValues.js":335,"./collapseGroups.js":336,"./convertColors.js":337,"./convertEllipseToCircle.js":338,"./convertPathData.js":339,"./convertTransform.js":342,"./inlineStyles.js":344,"./mergePaths.js":345,"./mergeStyles.js":346,"./minifyStyles.js":347,"./removeComments.js":353,"./removeDesc.js":354,"./removeDoctype.js":356,"./removeEditorsNSData.js":357,"./removeEmptyAttrs.js":359,"./removeEmptyContainers.js":360,"./removeEmptyText.js":361,"./removeHiddenElems.js":362,"./removeMetadata.js":363,"./removeNonInheritableGroupAttrs.js":364,"./removeTitle.js":369,"./removeUnknownsAndDefaults.js":370,"./removeUnusedNS.js":371,"./removeUselessDefs.js":372,"./removeUselessStrokeAndFill.js":373,"./removeViewBox.js":374,"./removeXMLProcInst.js":376,"./sortDefsChildren.js":381}],379:[function(require,module,exports){
'use strict';
const { createPreset } = require('../tools.js');
const removeDoctype = require('./removeDoctype.js');
const removeXMLProcInst = require('./removeXMLProcInst.js');
const removeComments = require('./removeComments.js');
const removeMetadata = require('./removeMetadata.js');
const removeEditorsNSData = require('./removeEditorsNSData.js');
const cleanupAttrs = require('./cleanupAttrs.js');
const mergeStyles = require('./mergeStyles.js');
const minifyStyles = require('./minifyStyles.js');
const cleanupIds = require('./cleanupIds.js');
const removeUselessDefs = require('./removeUselessDefs.js');
const cleanupNumericValues = require('./cleanupNumericValues.js');
const convertColors = require('./convertColors.js');
const removeUnknownsAndDefaults = require('./removeUnknownsAndDefaults.js');
const removeNonInheritableGroupAttrs = require('./removeNonInheritableGroupAttrs.js');
const removeUselessStrokeAndFill = require('./removeUselessStrokeAndFill.js');
const removeViewBox = require('./removeViewBox.js');
const cleanupEnableBackground = require('./cleanupEnableBackground.js');
const removeHiddenElems = require('./removeHiddenElems.js');
const removeEmptyText = require('./removeEmptyText.js');
const collapseGroups = require('./collapseGroups.js');
const removeEmptyAttrs = require('./removeEmptyAttrs.js');
const removeEmptyContainers = require('./removeEmptyContainers.js');
const mergePaths = require('./mergePaths.js');
const removeUnusedNS = require('./removeUnusedNS.js');
const sortDefsChildren = require('./sortDefsChildren.js');
const removeTitle = require('./removeTitle.js');
const removeDesc = require('./removeDesc.js');
const presetSafe = createPreset({
name: 'safeAndFastPreset',
plugins: [
removeDoctype,
removeXMLProcInst,
removeComments,
removeMetadata,
removeEditorsNSData,
cleanupAttrs,
mergeStyles,
cleanupIds,
removeUselessDefs,
cleanupNumericValues,
convertColors,
removeUnknownsAndDefaults,
removeNonInheritableGroupAttrs,
removeUselessStrokeAndFill,
removeViewBox,
cleanupEnableBackground,
removeHiddenElems,
removeEmptyText,
collapseGroups,
removeEmptyAttrs,
removeEmptyContainers,
removeUnusedNS,
removeTitle,
removeDesc,
],
});
module.exports = presetSafe;
},{"../tools.js":384,"./cleanupAttrs.js":331,"./cleanupEnableBackground.js":332,"./cleanupIds.js":333,"./cleanupNumericValues.js":335,"./collapseGroups.js":336,"./convertColors.js":337,"./mergePaths.js":345,"./mergeStyles.js":346,"./minifyStyles.js":347,"./removeComments.js":353,"./removeDesc.js":354,"./removeDoctype.js":356,"./removeEditorsNSData.js":357,"./removeEmptyAttrs.js":359,"./removeEmptyContainers.js":360,"./removeEmptyText.js":361,"./removeHiddenElems.js":362,"./removeMetadata.js":363,"./removeNonInheritableGroupAttrs.js":364,"./removeTitle.js":369,"./removeUnknownsAndDefaults.js":370,"./removeUnusedNS.js":371,"./removeUselessDefs.js":372,"./removeUselessStrokeAndFill.js":373,"./removeViewBox.js":374,"./removeXMLProcInst.js":376,"./sortDefsChildren.js":381}],380:[function(require,module,exports){
'use strict';
exports.name = 'sortAttrs';
@ -39778,7 +39844,7 @@ exports.fn = (_root, params) => {
};
};
},{}],380:[function(require,module,exports){
},{}],381:[function(require,module,exports){
'use strict';
exports.name = 'sortDefsChildren';
@ -39836,7 +39902,7 @@ exports.fn = () => {
};
};
},{}],381:[function(require,module,exports){
},{}],382:[function(require,module,exports){
'use strict';
const { textElems } = require('./plugins/_collections.js');
@ -40070,7 +40136,7 @@ const stringifyText = (node, config, state) => {
);
};
},{"./plugins/_collections.js":325}],382:[function(require,module,exports){
},{"./plugins/_collections.js":325}],383:[function(require,module,exports){
'use strict';
const csstree = require('css-tree');
@ -40300,7 +40366,7 @@ const computeStyle = (stylesheet, node) => {
};
exports.computeStyle = computeStyle;
},{"./plugins/_collections.js":325,"./xast.js":384,"css-tree":25,"csso":138}],383:[function(require,module,exports){
},{"./plugins/_collections.js":325,"./xast.js":385,"css-tree":25,"csso":138}],384:[function(require,module,exports){
(function (Buffer){(function (){
'use strict';
@ -40455,7 +40521,7 @@ exports.invokePlugins = invokePlugins;
exports.createPreset = createPreset;
}).call(this)}).call(this,require("buffer").Buffer)
},{"./xast.js":384,"buffer":4}],384:[function(require,module,exports){
},{"./xast.js":385,"buffer":4}],385:[function(require,module,exports){
'use strict';
const { selectAll, selectOne, is } = require('css-select');

View File

@ -255,7 +255,11 @@
(dissoc :component-root)))
[new-root-shape new-shapes updated-shapes]
(ctst/clone-object shape nil objects update-new-shape update-original-shape)
(ctst/clone-shape shape
nil
objects
:update-new-shape update-new-shape
:update-original-shape update-original-shape)
;; If frame-id points to a shape inside the component, remap it to the
;; corresponding new frame shape. If not, set it to nil.
@ -339,15 +343,14 @@
(dissoc :component-root))))
[new-shape new-shapes _]
(ctst/clone-object component-shape
frame-id
(if components-v2 (:objects component-page) (:objects component))
update-new-shape
(fn [object _] object)
force-id
keep-ids?
frame-id)
(ctst/clone-shape component-shape
frame-id
(if components-v2 (:objects component-page) (:objects component))
:update-new-shape update-new-shape
:force-id force-id
:keep-ids? keep-ids?
:frame-id frame-id
:dest-objects (:objects container))
;; Fix empty parent-id and remap all grid cells to the new ids.
remap-ids

View File

@ -327,6 +327,20 @@
[pad-top pad-right pad-top pad-right]
[pad-top pad-right pad-bottom pad-left])))
(defn h-padding
[{:keys [layout-padding-type layout-padding]}]
(let [{pad-right :p2 pad-left :p4} layout-padding]
(if (= :simple layout-padding-type)
(+ pad-right pad-right)
(+ pad-right pad-left))))
(defn v-padding
[{:keys [layout-padding-type layout-padding]}]
(let [{pad-top :p1 pad-bottom :p3} layout-padding]
(if (= :simple layout-padding-type)
(+ pad-top pad-top)
(+ pad-top pad-bottom))))
(defn child-min-width
[child]
(if (and (fill-width? child)
@ -590,7 +604,8 @@
(declare assign-cells)
(def default-track-value
{:type :auto})
{:type :flex
:value 1})
(def grid-cell-defaults
{:row-span 1
@ -600,53 +615,225 @@
:justify-self :auto
:shapes []})
(declare resize-cell-area)
(declare cells-by-column)
(declare cells-by-row)
(defn remove-cell-areas
"Remove the areas in the given `index` before and after the index"
[parent prop index]
(let [prop-span (if (= prop :column) :column-span :row-span)
cells (if (= prop :column) (cells-by-column parent index) (cells-by-row parent index))]
(->> cells
(filter #(> (get % prop-span) 1))
(reduce
(fn [parent cell]
(let [area? (= :area (:position cell))
changed-cells
(cond
;; New track at the beginning
(= (get cell prop) (inc index))
[(assoc cell prop-span 1)
(-> cell
(assoc :id (uuid/next) :shapes [] prop (inc (get cell prop)) prop-span (dec (get cell prop-span)))
(dissoc :area-name)
(cond-> area? (assoc :position :manual)))]
;; New track at the middle
(< (get cell prop) (inc index) (+ (get cell prop) (dec (get cell prop-span))))
[(assoc cell prop-span (- (inc index) (get cell prop)))
(-> cell
(assoc :id (uuid/next) :shapes [] prop (inc index) prop-span 1)
(dissoc :area-name)
(cond-> area? (assoc :position :manual)))
(-> cell
(assoc :id (uuid/next) :shapes [] prop (+ index 2) prop-span (- (+ (get cell prop) (dec (get cell prop-span))) (inc index)))
(dissoc :area-name)
(cond-> area? (assoc :position :manual)))]
;; New track at the end
(= (+ (get cell prop) (dec (get cell prop-span))) (inc index))
[(assoc cell prop-span (- (inc index) (get cell prop)))
(-> cell
(assoc :id (uuid/next) :shapes [] prop (inc index) prop-span 1)
(dissoc :area-name)
(cond-> area? (assoc :position :manual)))])]
(->> changed-cells
(reduce #(update %1 :layout-grid-cells assoc (:id %2) %2) parent))))
parent))))
(defn remove-cell-areas-after
"Remove the areas in the given `index` but only after the index."
[parent prop index]
(let [prop-span (if (= prop :column) :column-span :row-span)
cells (if (= prop :column) (cells-by-column parent index) (cells-by-row parent index))]
(->> cells
(filter #(> (get % prop-span) 1))
(reduce
(fn [parent cell]
(let [area? (= :area (:position cell))
changed-cells
(cond
;; New track at the beginning
(= (get cell prop) (inc index))
[(assoc cell prop-span 1)
(-> cell
(assoc :id (uuid/next) :shapes [] prop (inc (get cell prop)) prop-span (dec (get cell prop-span)))
(dissoc :area-name)
(cond-> area? (assoc :position :manual)))]
;; New track at the middle
(< (get cell prop) (inc index) (+ (get cell prop) (dec (get cell prop-span))))
[(assoc cell prop-span (- (+ index 2) (get cell prop)))
(-> cell
(assoc :id (uuid/next) :shapes [] prop (+ index 2) prop-span (- (+ (get cell prop) (dec (get cell prop-span))) (inc index)))
(dissoc :area-name)
(cond-> area? (assoc :position :manual)))])]
(->> changed-cells
(reduce #(update %1 :layout-grid-cells assoc (:id %2) %2) parent))))
parent))))
;; Adding a track creates the cells. We should check the shapes that are not tracked (with default values) and assign to the correct tracked values
(defn add-grid-track
([type parent value]
(add-grid-track type parent value nil))
([type parent value index]
(dm/assert!
"expected a valid grid definition for `value`"
(check-grid-track! value))
(let [[tracks-prop tracks-prop-other prop prop-other prop-span prop-span-other]
(if (= type :column)
[:layout-grid-columns :layout-grid-rows :column :row :column-span :row-span]
[:layout-grid-rows :layout-grid-columns :row :column :row-span :column-span])
new-index (d/nilv index (count (get parent tracks-prop)))
new-track-num (inc new-index)
;; Increase the values for the existing cells
layout-grid-cells
(-> (:layout-grid-cells parent)
(update-vals
(fn [cell]
(cond-> cell
(>= (get cell prop) new-track-num)
(update prop inc)
(and (< (get cell prop) new-track-num)
(> (get cell prop-span) 1)
(>= (+ (get cell prop) (dec (get cell prop-span))) new-track-num))
(update prop-span inc)))))
;; Search for the cells already created
exist-cells?
(into #{}
(comp (filter
(fn [cell]
(and (>= new-track-num (get cell prop))
(< new-track-num (+ (get cell prop) (get cell prop-span))))))
(mapcat #(range (get % prop-other) (+ (get % prop-other) (get % prop-span-other)))))
(vals layout-grid-cells))
;; Create the new cells as necesary
layout-grid-cells
(->> (d/enumerate (get parent tracks-prop-other))
(remove (fn [[idx _]] (exist-cells? (inc idx))))
(reduce
(fn [result [idx _]]
(let [id (uuid/next)]
(assoc result id
(merge {:id id
prop-other (inc idx)
prop new-track-num}
grid-cell-defaults))))
layout-grid-cells))]
(-> parent
(update tracks-prop d/add-at-index new-index value)
(assoc :layout-grid-cells layout-grid-cells)))))
(defn add-grid-column
[parent value]
(dm/assert!
"expected a valid grid definition for `value`"
(check-grid-track! value))
(let [rows (:layout-grid-rows parent)
new-col-num (inc (count (:layout-grid-columns parent)))
layout-grid-cells
(->> (d/enumerate rows)
(reduce (fn [result [row-idx _]]
(let [id (uuid/next)]
(assoc result id
(merge {:id id
:row (inc row-idx)
:column new-col-num}
grid-cell-defaults))))
(:layout-grid-cells parent)))]
(-> parent
(update :layout-grid-columns (fnil conj []) value)
(assoc :layout-grid-cells layout-grid-cells))))
([parent value]
(add-grid-column parent value nil))
([parent value index]
(add-grid-track :column parent value index)))
(defn add-grid-row
[parent value]
(dm/assert!
"expected a valid grid definition for `value`"
(check-grid-track! value))
([parent value]
(add-grid-row parent value nil))
([parent value index]
(add-grid-track :row parent value index)))
(let [cols (:layout-grid-columns parent)
new-row-num (inc (count (:layout-grid-rows parent)))
(defn- duplicate-cells
[shape prop from-index to-index ids-map]
layout-grid-cells
(->> (d/enumerate cols)
(reduce (fn [result [col-idx _]]
(let [id (uuid/next)]
(assoc result id
(merge {:id id
:column (inc col-idx)
:row new-row-num}
grid-cell-defaults))))
(:layout-grid-cells parent)))]
(-> parent
(update :layout-grid-rows (fnil conj []) value)
(assoc :layout-grid-cells layout-grid-cells))))
(let [[prop-span prop-other prop-other-span]
(if (= prop :column)
[:column-span :row :row-span]
[:row-span :column :column-span])
from-cells
(if (= prop :column)
(cells-by-column shape from-index)
(cells-by-row shape from-index))
to-cells
(if (= prop :column)
(cells-by-column shape to-index)
(cells-by-row shape to-index))
to-cells-idx (d/index-by prop-other to-cells)
;; This loop will go throught the original cells and copy their data to the target cell
;; After this some cells could have no correspondence and should be removed
[shape matched]
(loop [from-cells (seq from-cells)
matched #{}
result shape]
(if-let [cell (first from-cells)]
(let [match-cell
(-> (get to-cells-idx (get cell prop-other))
(d/patch-object (select-keys cell [prop-other-span :position :align-self :justify-self]))
(cond-> (= :area (:position cell))
(assoc :position :manual))
(cond-> (= (get cell prop-span) 1)
(assoc :shapes (mapv ids-map (:shapes cell)))))]
(recur (rest from-cells)
(conj matched (:id match-cell))
(assoc-in result [:layout-grid-cells (:id match-cell)] match-cell)))
[result matched]))
;; Remove cells that haven't been matched
shape
(->> to-cells
(remove (fn [{:keys [id]}] (contains? matched id)))
(reduce (fn [shape cell]
(update shape :layout-grid-cells dissoc (:id cell)))
shape))]
shape))
(defn duplicate-row
[shape objects index ids-map]
(let [value (dm/get-in shape [:layout-grid-rows index])]
(-> shape
(remove-cell-areas-after :row index)
(add-grid-row value (inc index))
(duplicate-cells :row index (inc index) ids-map)
(assign-cells objects))))
(defn duplicate-column
[shape objects index ids-map]
(let [value (dm/get-in shape [:layout-grid-columns index])]
(-> shape
(remove-cell-areas-after :column index)
(add-grid-column value (inc index))
(duplicate-cells :column index (inc index) ids-map)
(assign-cells objects))))
(defn make-remove-cell
[attr span-attr track-num]
@ -747,12 +934,9 @@
update-vals
(fn [cell] (update cell prop #(get remap-tracks % %)))))))
(declare resize-cell-area)
(declare cells-by-column)
(declare cells-by-row)
(defn- reorder-grid-track
[parent from-index to-index move-content? cells-by tracks-props prop prop-span]
[parent from-index to-index move-content? tracks-props prop]
(let [from-track (inc from-index)
to-track (if (< to-index from-index)
(+ to-index 2)
@ -761,23 +945,10 @@
(and move-content? (not= from-track to-track))
parent
(if move-content?
(->> (concat (cells-by parent (dec from-track))
(cells-by parent (dec to-track)))
(reduce (fn [parent cell]
(cond-> parent
(and (> (get cell prop-span) 1)
(or (> to-track from-track) (not (= to-track (get cell prop))))
(or (< to-track from-track) (not (= to-track (+ (get cell prop) (dec (get cell prop-span)))))))
(resize-cell-area
(:row cell)
(:column cell)
(:row cell)
(:column cell)
(if (= prop :row) 1 (:row-span cell))
(if (= prop :column) 1 (:column-span cell)))))
parent))
parent)
(cond-> parent
move-content?
(-> (remove-cell-areas prop from-index)
(remove-cell-areas-after prop to-index)))
parent
(reorder-grid-tracks parent tracks-props from-index to-index)]
@ -788,11 +959,11 @@
(defn reorder-grid-column
[parent from-index to-index move-content?]
(reorder-grid-track parent from-index to-index move-content? cells-by-column :layout-grid-columns :column :column-span))
(reorder-grid-track parent from-index to-index move-content? :layout-grid-columns :column))
(defn reorder-grid-row
[parent from-index to-index move-content?]
(reorder-grid-track parent from-index to-index move-content? cells-by-row :layout-grid-rows :row :row-span))
(reorder-grid-track parent from-index to-index move-content? :layout-grid-rows :row))
(defn cells-seq
[{:keys [layout-grid-cells layout-grid-dir]} & {:keys [sort?] :or {sort? false}}]
@ -877,6 +1048,41 @@
parent
overlaps))
(defn reassign-positions
"Propagate the manual positioning to the following cells"
[parent]
(->> (cells-seq parent :sort? true)
(reduce
(fn [[parent auto?] cell]
(let [[cell auto?]
(cond
(and (empty? (:shapes cell))
(= :manual (:position cell))
(= (:row-span cell) 1)
(= (:column-span cell) 1))
[(assoc cell :position :auto) false]
(and (or (not= (:row-span cell) 1)
(not= (:column-span cell) 1))
(= :auto (:position cell)))
[(assoc cell :position :manual) false]
(empty? (:shapes cell))
[cell false]
(and (not auto?) (= :auto (:position cell)))
[(assoc cell :position :manual) false]
(= :manual (:position cell))
[cell false]
:else
[cell auto?])]
[(assoc-in parent [:layout-grid-cells (:id cell)] cell) auto?]))
[parent true])
(first)))
(defn position-auto-shapes
[parent]
;; Iterate through the cells. While auto and contains shape no changes.
@ -903,6 +1109,14 @@
(rest shapes)))))]
parent))
(defn assign-cell-positions
[parent objects]
(prn ">>>>assign-cell-positions" (:name parent))
(-> parent
(check-deassigned-cells objects)
(reassign-positions)
(position-auto-shapes)))
;; Assign cells takes the children and move them into the allotted cells. If there are not enough cells it creates
;; not-tracked rows/columns and put the shapes there
;; Non-tracked tracks need to be deleted when they are empty and there are no more shapes unallocated
@ -914,13 +1128,10 @@
;; - (maybe) create group/frames. This case will assigna a cell that had one of its children
(defn assign-cells
[parent objects]
(let [;; TODO: Remove this, shouldn't be happening
;;overlaps (overlapping-cells parent)
;;_ (when (not (empty? overlaps))
;; (.warn js/console "OVERLAPS" overlaps))
parent (cond-> (check-deassigned-cells parent objects)
#_(d/not-empty? overlaps)
#_(fix-overlaps overlaps))
(prn ">assign-cells")
(let [
parent (assign-cell-positions parent objects)
shape-has-cell?
(into #{} (mapcat (comp :shapes second)) (:layout-grid-cells parent))
@ -928,9 +1139,7 @@
no-cell-shapes
(->> (:shapes parent)
(remove shape-has-cell?)
(remove (partial position-absolute? objects)))
parent (position-auto-shapes parent)]
(remove (partial position-absolute? objects)))]
(if (empty? no-cell-shapes)
;; All shapes are within a cell. No need to assign
@ -1080,6 +1289,7 @@
target-cell
(-> prev-cell
(assoc
:position :manual
:row new-row
:column new-column
:row-span new-row-span
@ -1224,30 +1434,64 @@
(assoc parent :shapes (into [] (reverse new-shapes)))))
(defn cells-by-row
[parent index]
(->> (:layout-grid-cells parent)
(filter (fn [[_ {:keys [row row-span]}]]
(and (>= (inc index) row)
(< (inc index) (+ row row-span)))))
(map second)))
([parent index]
(cells-by-row parent index true))
([parent index check-span?]
(->> (:layout-grid-cells parent)
(vals)
(filter
(fn [{:keys [row row-span]}]
(if check-span?
(and (>= (inc index) row)
(< (inc index) (+ row row-span)))
(= (inc index) row)))))))
(defn cells-by-column
[parent index]
([parent index]
(cells-by-column parent index true))
([parent index check-span?]
(->> (:layout-grid-cells parent)
(vals)
(filter
(fn [{:keys [column column-span] :as cell}]
(if check-span?
(and (>= (inc index) column)
(< (inc index) (+ column column-span)))
(= (inc index) column)))))))
(defn cells-in-area
[parent first-row last-row first-column last-column]
(->> (:layout-grid-cells parent)
(filter (fn [[_ {:keys [column column-span]}]]
(and (>= (inc index) column)
(< (inc index) (+ column column-span)))))
(map second)))
(vals)
(filter
(fn [{:keys [row column row-span column-span] :as cell}]
(and
(or (<= row first-row (+ row row-span -1))
(<= row last-row (+ row row-span -1))
(<= first-row row last-row)
(<= first-row (+ row row-span -1) last-row))
(or (<= column first-column (+ column column-span -1))
(<= column last-column (+ column column-span -1))
(<= first-column column last-column)
(<= first-column (+ column column-span -1) last-column)))))))
(defn shapes-by-row
[parent index]
(->> (cells-by-row parent index)
(mapcat :shapes)))
"Find all the shapes for a given row"
([parent index]
(shapes-by-row parent index true))
;; check-span? if false will only see if there is a coincidence in file&row
([parent index check-span?]
(->> (cells-by-row parent index check-span?)
(mapcat :shapes))))
(defn shapes-by-column
[parent index]
(->> (cells-by-column parent index)
(mapcat :shapes)))
"Find all the shapes for a given column"
([parent index]
(shapes-by-column parent index true))
([parent index check-span?]
(->> (cells-by-column parent index check-span?)
(mapcat :shapes))))
(defn cells-coordinates
"Given a group of cells returns the coordinates that define"
@ -1300,7 +1544,6 @@
(defn valid-area-cells?
[cells]
(let [{:keys [first-row last-row first-column last-column cell-coords]} (cells-coordinates cells)]
(every?
#(contains? cell-coords %)
@ -1309,7 +1552,7 @@
[r c]))))
(defn remap-grid-cells
"Remaps the shapes inside the cells"
"Remaps the shapes ids inside the cells"
[shape ids-map]
(let [do-remap-cells
(fn [cell]

View File

@ -342,91 +342,89 @@
[frame]
(not (mth/almost-zero? (:rotation frame 0))))
(defn clone-object
"Gets a copy of the object and all its children, with new ids and with
(defn clone-shape
"Gets a copy of the shape and all its children, with new ids and with
the parent-children links correctly set. Admits functions to make
more transformations to the cloned objects and the original ones.
more transformations to the cloned shapes and the original ones.
Returns the cloned object, the list of all new objects (including
the cloned one), and possibly a list of original objects modified.
Returns the cloned shape, the list of all new shapes (including
the cloned one), and possibly a list of original shapes modified.
The list of objects are returned in tree traversal order, respecting
The list of shapes are returned in tree traversal order, respecting
the order of the children of each parent."
([object parent-id objects]
(clone-object object parent-id objects (fn [object _] object) (fn [object _] object) nil false nil))
([object parent-id objects update-new-object]
(clone-object object parent-id objects update-new-object (fn [object _] object) nil false nil))
([object parent-id objects update-new-object update-original-object]
(clone-object object parent-id objects update-new-object update-original-object nil false nil))
([object parent-id objects update-new-object update-original-object force-id]
(clone-object object parent-id objects update-new-object update-original-object force-id false nil))
([object parent-id objects update-new-object update-original-object force-id keep-ids?]
(clone-object object parent-id objects update-new-object update-original-object force-id keep-ids? nil))
([object parent-id objects update-new-object update-original-object force-id keep-ids? frame-id]
(let [new-id (cond
(some? force-id) force-id
keep-ids? (:id object)
:else (uuid/next))
[shape parent-id objects & {:keys [update-new-shape update-original-shape force-id keep-ids? frame-id dest-objects]
:or {update-new-shape (fn [shape _] shape)
update-original-shape (fn [shape _] shape)
force-id nil
keep-ids? false
frame-id nil
dest-objects objects}}]
(let [new-id (cond
(some? force-id) force-id
keep-ids? (:id shape)
:else (uuid/next))
;; Assign the correct frame-id for the given parent. It's the parent-id (if parent is frame)
;; or the parent's frame-id otherwise. Only for the first cloned shapes. In recursive calls
;; this is not needed.
frame-id (cond
(and (nil? frame-id) (cfh/frame-shape? objects parent-id))
parent-id
frame-id (cond
(and (nil? frame-id) (cfh/frame-shape? dest-objects parent-id))
parent-id
(nil? frame-id)
(dm/get-in objects [parent-id :frame-id])
(nil? frame-id)
(dm/get-in dest-objects [parent-id :frame-id] uuid/zero)
:else
frame-id)]
:else
frame-id)]
(loop [child-ids (seq (:shapes object))
new-direct-children []
new-children []
updated-children []]
(loop [child-ids (seq (:shapes shape))
new-direct-children []
new-children []
updated-children []]
(if (empty? child-ids)
(let [new-object (cond-> object
:always
(assoc :id new-id
:parent-id parent-id
:frame-id frame-id)
(if (empty? child-ids)
(let [new-shape (cond-> shape
:always
(assoc :id new-id
:parent-id parent-id
:frame-id frame-id)
(some? (:shapes object))
(assoc :shapes (mapv :id new-direct-children)))
(some? (:shapes shape))
(assoc :shapes (mapv :id new-direct-children)))
new-object (update-new-object new-object object)
new-objects (into [new-object] new-children)
new-shape (update-new-shape new-shape shape)
new-shapes (into [new-shape] new-children)
updated-object (update-original-object object new-object)
updated-objects (if (identical? object updated-object)
updated-children
(into [updated-object] updated-children))]
updated-shape (update-original-shape shape new-shape)
updated-shapes (if (identical? shape updated-shape)
updated-children
(into [updated-shape] updated-children))]
[new-object new-objects updated-objects])
[new-shape new-shapes updated-shapes])
(let [child-id (first child-ids)
child (get objects child-id)
_ (dm/assert! (some? child))
frame-id-child (if (cfh/frame-shape? object)
new-id
frame-id)
(let [child-id (first child-ids)
child (get objects child-id)
_ (dm/assert! (some? child))
frame-id-child (if (cfh/frame-shape? shape)
new-id
frame-id)
[new-child new-child-objects updated-child-objects]
(clone-object child new-id objects update-new-object update-original-object nil keep-ids? frame-id-child)]
[new-child new-child-shapes updated-child-shapes]
(clone-shape child
new-id
objects
:update-new-shape update-new-shape
:update-original-shape update-original-shape
:force-id nil
:keep-ids? keep-ids?
:frame-id frame-id-child
:dest-objects dest-objects)]
(recur
(next child-ids)
(into new-direct-children [new-child])
(into new-children new-child-objects)
(into updated-children updated-child-objects))))))))
(recur
(next child-ids)
(into new-direct-children [new-child])
(into new-children new-child-shapes)
(into updated-children updated-child-shapes)))))))
(defn generate-shape-grid
"Generate a sequence of positions that lays out the list of

View File

@ -1,6 +1,6 @@
import fs from "fs";
import l from "lodash";
import path from "path"
import path from "path";
import gulp from "gulp";
import gulpConcat from "gulp-concat";
@ -9,8 +9,8 @@ import gulpMustache from "gulp-mustache";
import gulpPostcss from "gulp-postcss";
import gulpRename from "gulp-rename";
import * as sass from 'sass';
import gsass from 'gulp-sass';
import * as sass from "sass";
import gsass from "gulp-sass";
const gulpSass = gsass(sass);
import svgSprite from "gulp-svg-sprite";
@ -204,6 +204,7 @@ function templatePipeline(options) {
manifest: manifest,
translations: JSON.stringify(locales),
themes: JSON.stringify(themes),
isDebug: process.env.NODE_ENV !== "production",
});
return gulp.src(input).pipe(tmpl).pipe(gulpRename(name)).pipe(gulp.dest(output)).pipe(touch());
@ -231,16 +232,16 @@ gulp.task("scss:modules", function () {
modules({
getJSON: function (cssFileName, json, outputFileName) {
// We do nothing because we don't want the generated JSON files
},
},
// Calculates the whole css-module selector name.
// Should be the same as the one in the file `/src/app/main/style.clj`
generateScopedName: function (selector, filename, css) {
const dir = path.dirname(filename);
const name = path.basename(filename, ".css");
const parts = dir.split("/");
const rootIdx = parts.findIndex(s => s === ROOT_NAME);
const rootIdx = parts.findIndex((s) => s === ROOT_NAME);
return parts.slice(rootIdx + 1).join("_") + "_" + name + "__" + selector;
},
},
}),
autoprefixer(),
]),
@ -249,13 +250,15 @@ gulp.task("scss:modules", function () {
});
gulp.task("scss:main", function () {
const sources = [`${paths.resources}styles/main-default.scss`, `${paths.resources}styles/debug.scss`];
return gulp
.src(paths.resources + "styles/main-default.scss")
.pipe(gulpSass.sync({
includePaths: [
"./node_modules/animate.css"
]
}))
.src(sources)
.pipe(
gulpSass.sync({
includePaths: ["./node_modules/animate.css"],
}),
)
.pipe(gulpPostcss([autoprefixer]))
.pipe(gulp.dest(paths.output + "css/"));
});

View File

@ -17,9 +17,6 @@ body {
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: red; //debugger colors
color: yellow; //debugger colors
}
#app {

View File

@ -216,6 +216,7 @@
.button-disabled {
@include buttonStyle;
@include flexCenter;
height: $s-32;
background-color: var(--button-background-color-disabled);
border: $s-1 solid var(--button-border-color-disabled);
color: var(--button-foreground-color-disabled);
@ -738,7 +739,6 @@
@include titleTipography;
color: var(--color-foreground-primary);
text-align: left;
display: grid;
grid-template-columns: 1fr 22px;
grid-template-areas: "name button";

View File

@ -34,7 +34,3 @@ $da-primary: var(--color-accent-primary);
$da-primary-muted: var(--color-accent-primary-muted);
$da-secondary: var(--color-accent-secondary);
$da-tertiary: var(--color-accent-tertiary);
#app {
background-color: var(--app-background);
}

View File

@ -305,6 +305,11 @@
--resize-area-background-color: var(--color-background-primary);
--resize-area-border-color: var(--color-background-quaternary);
--flow-tag-background-color: var(--color-background-tertiary);
--flow-tag-foreground-color: var(--color-foreground-secondary);
--flow-tag-background-color-hover: var(--color-accent-primary);
--flow-tag-foreground-color-hover: var(--color-background-primary);
// VIEWER
--viewer-background-color: var(--color-background-secondary);
--viewer-paginator-background-color: var(--color-background-tertiary);
@ -314,3 +319,7 @@
--viewer-thumbnail-border-color: var(--color-accent-primary);
--viewer-thumbnail-background-color-selected: var(--color-accent-primary-muted);
}
#app {
background-color: var(--app-background);
}

View File

@ -0,0 +1,15 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
// NOTE: This CSS only gets included when the NODE_ENV env var
// is *not* set to `production`.
// It is useful to have some styles that are useful in local dev, like
// debugging.
body {
background-color: red;
color: yellow;
}

View File

@ -44,7 +44,6 @@
//#################################################
@import "common/framework";
@import "main/partials/modal";
@import "main/partials/forms";
@import "main/partials/texts";
@import "main/partials/context-menu";
@ -58,31 +57,16 @@
@import "main/partials/viewer-header";
@import "main/partials/viewer-thumbnails";
@import "main/partials/activity-bar";
@import "main/partials/colorpicker";
@import "main/partials/dashboard";
@import "main/partials/dashboard-header";
@import "main/partials/dashboard-grid";
@import "main/partials/dashboard-sidebar";
@import "main/partials/dashboard-team";
@import "main/partials/dashboard-settings";
@import "main/partials/dashboard-fonts";
@import "main/partials/debug-icons-preview";
@import "main/partials/editable-label";
@import "main/partials/loader";
@import "main/partials/project-bar";
@import "main/partials/sidebar";
@import "main/partials/sidebar-align-options";
@import "main/partials/sidebar-assets";
@import "main/partials/sidebar-document-history";
@import "main/partials/sidebar-element-options";
@import "main/partials/sidebar-interactions";
@import "main/partials/tab-container";
@import "main/partials/tool-bar";
@import "main/partials/user-settings";
@import "main/partials/workspace";
@import "main/partials/comments";
@import "main/partials/color-bullet";
@import "main/partials/inspect";
@import "main/partials/exception-page";
@import "main/partials/share-link";
@import "main/partials/signup-questions";

View File

@ -1,598 +0,0 @@
// 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
.colorpicker {
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
background-color: $color-white;
}
.colorpicker-content {
display: flex;
flex-direction: column;
padding: $size-2;
& > * {
width: 200px;
}
.top-actions {
display: flex;
margin-bottom: $size-1;
flex-direction: row-reverse;
justify-content: space-between;
.picker-btn {
background: none;
border: none;
cursor: pointer;
&.active svg,
&:hover svg {
fill: $color-primary;
}
svg {
width: 14px;
height: 14px;
}
}
.element-set-content {
width: auto;
padding: 0.25rem 0;
.custom-select {
border: none;
&:hover {
border: none;
}
.custom-select-dropdown {
left: auto;
right: 0;
}
}
}
}
.select-image {
.content {
display: flex;
justify-content: center;
background-image: url("/images/colorpicker-no-image.png");
background-position: center;
background-size: auto 6.75rem;
height: 6.75rem;
img {
height: fit-content;
width: fit-content;
max-height: 100%;
max-width: 100%;
margin: auto;
}
}
button {
width: 100%;
margin-top: 10px;
}
}
.gradients-buttons {
.gradient {
cursor: pointer;
width: 15px;
height: 15px;
padding: 0;
margin: 0;
border: 1px solid $color-gray-20;
border-radius: $br2;
margin-left: $size-1;
}
.active {
border-color: $color-primary;
}
.linear-gradient {
background: linear-gradient(180deg, $color-gray-20, transparent);
}
.radial-gradient {
background: radial-gradient(transparent, $color-gray-20);
}
}
.gradient-stops {
height: 10px;
display: flex;
margin-top: $size-2;
margin-bottom: $size-4;
.gradient-background-wrapper {
height: 100%;
width: 100%;
border: 1px solid $color-gray-10;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=")
left center;
}
.gradient-background {
height: 100%;
width: 100%;
}
.gradient-stop-wrapper {
position: absolute;
width: calc(100% - 2rem);
margin-left: 0.5rem;
}
.gradient-stop {
display: grid;
grid-template-columns: 50% 50%;
position: absolute;
width: 15px;
height: 15px;
border-radius: $br2;
border: 1px solid $color-gray-20;
margin-top: -2px;
margin-left: -7px;
box-shadow: 0 2px 2px rgb(0 0 0 / 15%);
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=")
left center;
background-color: $color-white;
&.active {
border-color: $color-primary;
}
}
}
.picker-detail-wrapper {
position: relative;
.center-circle {
width: 14px;
height: 14px;
border: 2px solid $color-white;
border-radius: $br8;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-7px, -7px);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
}
}
#picker-detail {
border: 1px solid $color-gray-10;
}
.slider-selector {
--gradient-direction: 90deg;
--background-repeat: left;
&.vertical {
--gradient-direction: 0deg;
--background-repeat: top;
}
border: 1px solid $color-gray-10;
background: linear-gradient(
var(--gradient-direction),
rgba(var(--color), 0) 0%,
rgba(var(--color), 1) 100%
);
align-self: center;
position: relative;
cursor: pointer;
width: 100%;
height: calc(0.5rem + 1px);
&.vertical {
width: calc(0.5rem + 1px);
height: 100%;
}
&.hue {
background: linear-gradient(
var(--gradient-direction),
#f00 0%,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00 100%
);
}
&.saturation {
background: linear-gradient(
var(--gradient-direction),
var(--saturation-grad-from) 0%,
var(--saturation-grad-to) 100%
);
}
&.opacity {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=")
var(--background-repeat) center;
&::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(
var(--gradient-direction),
rgba(var(--color), 0) 0%,
rgba(var(--color), 1) 100%
);
}
}
&.value {
background: linear-gradient(var(--gradient-direction), #000 0%, #fff 100%);
}
.handler {
background-color: $color-white;
box-shadow: rgba(0, 0, 0, 0.37) 0px 1px 4px 0px;
transform: translate(-6px, -2px);
left: 50%;
position: absolute;
width: 12px;
height: 12px;
border-radius: $br6;
z-index: 1;
}
&.vertical .handler {
transform: translate(-6px, 6px);
}
}
.value-saturation-selector {
background-color: rgba(var(--hue-rgb));
position: relative;
height: 6.75rem;
cursor: pointer;
.handler {
position: absolute;
width: 12px;
height: 12px;
border-radius: $br6;
z-index: 1;
border: 1px solid $color-white;
box-shadow:
rgb(255, 255, 255) 0px 0px 0px 1px inset,
rgb(0 0 0 / 0.25) 0px 4px 4px inset,
rgb(0 0 0 / 0.25) 0px 4px 4px;
transform: translate(-6px, -6px);
left: 50%;
top: 50%;
}
&::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
&::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
}
}
.shade-selector {
display: grid;
justify-items: center;
align-items: center;
grid-template-areas:
"color hue"
"color opacity";
grid-template-columns: 2.5rem 1fr;
height: 3.5rem;
grid-row-gap: 0.5rem;
cursor: pointer;
margin-bottom: 0.25rem;
.slider-selector.hue {
grid-area: "hue";
align-self: flex-end;
}
.slider-selector.opacity {
grid-area: "opacity";
align-self: flex-start;
}
}
.color-values {
display: grid;
grid-template-columns: 3.5rem repeat(4, 1fr);
grid-row-gap: 0.25rem;
justify-items: center;
grid-column-gap: 0.25rem;
&.disable-opacity {
grid-template-columns: 3.5rem repeat(3, 1fr);
}
input {
width: 100%;
margin: 0;
border: 1px solid $color-gray-10;
border-radius: $br2;
font-size: $fs12;
height: 1.5rem;
padding: 0 $size-1;
color: $color-gray-40;
}
label {
font-size: $fs12;
}
}
.libraries {
border-top: 1px solid $color-gray-10;
padding-top: 0.5rem;
margin-top: 0.25rem;
width: 200px;
select {
background-image: url(/images/icons/arrow-down.svg);
background-repeat: no-repeat;
background-position: 95% 48%;
background-size: 10px;
margin: 0;
margin-bottom: $size-2;
width: 100%;
padding: $size-1 0.25rem;
font-size: $fs12;
color: $color-gray-40;
cursor: pointer;
border: 1px solid $color-gray-10;
border-radius: $br2;
option {
padding: 0;
}
}
.selected-colors {
display: grid;
grid-template-columns: repeat(8, 1fr);
justify-content: space-between;
margin-right: -8px;
max-height: 5.5rem;
overflow: auto;
div {
grid-area: unset;
}
}
.selected-colors::after {
content: "";
flex: auto;
}
}
.actions {
margin-top: 0.5rem;
display: flex;
flex-direction: row;
justify-content: center;
.btn-primary {
height: 1.5rem;
font-size: $fs12;
width: 100%;
}
}
.harmony-selector {
display: flex;
flex-direction: row;
margin-bottom: 0.5rem;
.hue-wheel-wrapper {
position: relative;
.hue-wheel {
width: 152px;
height: 152px;
}
.handler {
position: absolute;
width: 12px;
height: 12px;
border-radius: $br6;
z-index: 1;
border: 1px solid $color-white;
box-shadow:
rgb(255, 255, 255) 0px 0px 0px 1px inset,
rgb(0 0 0 / 0.25) 0px 4px 4px inset,
rgb(0 0 0 / 0.25) 0px 4px 4px;
transform: translate(-6px, -6px);
left: 50%;
top: 50%;
}
.handler.complement {
background-color: $color-white;
box-shadow: rgb(0 0 0 / 0.25) 0px 4px 4px;
}
}
.handlers-wrapper {
height: 152px;
display: flex;
flex-direction: row;
flex-grow: 1;
justify-content: space-around;
padding-top: 0.5rem;
& > * {
height: 100%;
}
}
}
.hsva-selector {
display: grid;
padding: 0.25rem;
grid-template-columns: 20px 1fr;
grid-template-rows: repeat(4, 2rem);
grid-row-gap: 0.5rem;
margin-bottom: 0.5rem;
.hue,
.saturation,
.value,
.opacity {
border-radius: $br10;
}
.hsva-selector-label {
grid-column: 1;
align-self: center;
}
}
}
.colorpicker-tooltip {
border-radius: $br3;
display: flex;
flex-direction: column;
left: 1400px;
top: 100px;
position: absolute;
z-index: 11;
width: auto;
span {
color: $color-gray-20;
font-size: $fs12;
}
.inputs-area {
.input-text {
color: $color-gray-60;
font-size: $fs12;
margin: 5px;
padding: 5px;
width: 100%;
}
}
.colorpicker-tabs {
display: flex;
margin-bottom: $size-2;
border-radius: $br5;
border: 1px solid $color-gray-10;
height: 2rem;
.colorpicker-tab {
cursor: pointer;
display: flex;
flex-grow: 1;
justify-content: center;
align-items: center;
svg {
width: 16px;
height: 16px;
fill: $color-gray-20;
}
}
.active {
background-color: $color-gray-10;
svg {
fill: $color-gray-60;
}
}
:hover svg {
fill: $color-primary;
}
}
}
.color-data {
align-items: center;
display: flex;
margin-bottom: $size-2;
position: relative;
&[draggable="true"] {
cursor: pointer;
}
.color-name {
font-size: $fs12;
margin: 5px 6px 0px 6px;
}
.color-info {
flex: 1 1 0;
input {
background-color: $color-gray-50;
border: 1px solid $color-gray-30;
border-radius: $br3;
color: $color-white;
height: 20px;
margin: 5px 0 0 0;
padding: 0 $size-1;
width: 84px;
font-size: $fs12;
&:focus {
border-color: $color-primary !important;
color: $color-white;
outline: none;
}
&:hover {
border-color: $color-gray-20;
}
&:invalid {
border-color: $color-danger;
}
}
}
::placeholder {
color: $color-gray-10;
}
.type {
color: $color-gray-10;
margin-right: $size-1;
}
.number {
color: $color-gray-60;
}
.element-set-actions-button svg {
width: 10px;
height: 10px;
}
}

View File

@ -1,467 +0,0 @@
.comments-section {
.thread-bubble {
position: absolute;
display: flex;
transform: translate(-15px, -15px);
cursor: pointer;
pointer-events: auto;
background-color: $color-gray-10;
color: $color-gray-60;
border: 1px solid #b1b2b5;
box-sizing: border-box;
box-shadow: 0px 4px 4px rgba($color-black, 0.25);
font-size: $fs12;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.resolved {
color: $color-gray-10;
background-color: $color-gray-50;
}
&.unread {
background-color: $color-primary;
}
span {
user-select: none;
}
}
.thread-content {
position: absolute;
pointer-events: auto;
margin-left: 10px;
background: $color-white;
border: 1px solid $color-gray-20;
box-sizing: border-box;
box-shadow: 0px 2px 8px rgba($color-black, 0.25);
border-radius: $br2;
min-width: 280px;
max-width: 280px;
user-select: text;
.comments {
max-height: 420px;
min-height: 105px;
overflow-y: auto;
}
hr {
border: 0;
height: 1px;
background-color: $color-gray-20;
margin: 0px 10px;
}
}
.reply-form {
display: flex;
padding: 10px;
flex-direction: column;
&.edit-form {
padding-bottom: 0px;
}
textarea {
font-family: "worksans", sans-serif;
font-size: $fs12;
min-height: 32px;
outline: none;
overflow: hidden;
padding: $size-2;
resize: none;
width: 100%;
border-radius: $br2;
border: 1px solid $color-gray-20;
max-height: 4rem;
}
.buttons {
margin-top: 10px;
display: flex;
justify-content: flex-end;
input {
margin: 0px;
font-size: $fs14;
&:not(:last-child) {
margin-right: 6px;
}
}
}
}
.comment-container {
position: relative;
}
.comment {
display: flex;
flex-direction: column;
padding: $size-4 $size-2;
.author {
display: flex;
align-items: center;
height: 26px;
max-height: 26px;
position: relative;
.name {
display: flex;
flex-direction: column;
.fullname {
font-weight: $fw700;
color: $color-gray-60;
font-size: $fs12;
@include text-ellipsis;
width: 174px;
}
.timeago {
margin-top: -2px;
font-size: $fs12;
color: $color-gray-30;
}
}
.avatar {
display: flex;
align-items: center;
padding-right: 6px;
img {
border-radius: 50%;
flex-shrink: 0;
height: 24px;
width: 24px;
}
}
.options-resolve {
position: absolute;
right: 20px;
top: 0px;
width: 16px;
height: 16px;
cursor: pointer;
svg {
width: 16px;
height: 16px;
fill: $color-gray-30;
}
}
.options {
position: absolute;
right: -2px;
top: 2px;
height: 16px;
display: flex;
align-items: center;
cursor: pointer;
.options-icon {
svg {
width: 14px;
height: 14px;
fill: $color-black;
}
}
}
}
.content {
margin: $size-4 0;
font-size: $fs14;
color: $color-black;
.text {
margin: 0 $size-2 0 26px;
white-space: pre-wrap;
display: inline-block;
word-break: break-word;
}
}
}
.comment-options-dropdown {
top: 7px;
right: 7px;
width: 150px;
border: 1px solid #b1b2b5;
}
}
.workspace-comment-threads-sidebar-header {
display: flex;
background-color: $color-black;
height: 34px;
align-items: center;
padding: 0px 9px;
color: $color-gray-10;
font-size: $fs12;
justify-content: space-between;
.options {
display: flex;
margin-right: 3px;
cursor: pointer;
.label {
padding-right: 8px;
}
.icon {
display: flex;
align-items: center;
}
svg {
fill: $color-gray-10;
width: 10px;
height: 10px;
}
}
.dropdown {
top: 80px;
right: 7px;
}
}
.comment-threads-section {
pointer-events: auto;
.thread-groups {
height: calc(100% - 34px);
overflow-y: scroll;
hr {
border: 0;
height: 1px;
background-color: $color-gray-30;
margin: 0px 0px;
}
}
.thread-group {
display: flex;
flex-direction: column;
font-size: $fs12;
.section-title {
margin: 0px 10px;
margin-top: 15px;
.icon {
margin-right: 4px;
}
.label {
&.filename {
font-weight: $fw700;
}
}
svg {
fill: $color-gray-10;
height: 10px;
width: 10px;
}
}
}
.thread-bubble {
position: unset;
transform: unset;
width: 24px;
height: 24px;
margin-right: 6px;
box-shadow: unset;
}
.comment {
cursor: pointer;
.author {
margin-bottom: $size-4;
.name {
display: flex;
.fullname {
width: unset;
max-width: 170px;
color: $color-gray-20;
padding-right: 3px;
}
.timeago {
margin-top: unset;
color: $color-gray-20;
}
}
}
.content {
margin-top: 0px;
color: $color-white;
&.replies {
margin: 0 $size-2 0 26px;
display: flex;
.total-replies {
margin-right: 9px;
color: $color-info;
}
.new-replies {
color: $color-primary;
}
}
}
}
}
.viewer-comments-container {
width: 100%;
height: 100%;
z-index: 1;
position: absolute;
top: 0px;
left: 0px;
}
.workspace-comments-container {
width: 100%;
height: 100%;
grid-column: 1 / span 2;
grid-row: 1 / span 2;
z-index: 1000;
pointer-events: none;
overflow: hidden;
user-select: text;
.threads {
position: absolute;
top: 0px;
left: 0px;
}
}
.dashboard-comments-section {
width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
background-color: $color-dashboard;
border-radius: $br3;
position: relative;
.button {
width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
background-color: $color-dashboard;
border-radius: $br3;
svg {
width: 15px;
height: 15px;
}
&.unread {
background-color: $color-warning;
}
&.open {
background-color: $color-black;
svg {
fill: $color-primary;
}
}
}
.dropdown {
width: 280px;
bottom: 35px;
left: 0px;
border-radius: $br3;
}
.header {
display: flex;
height: 40px;
align-items: center;
padding: 0px 11px;
h3 {
font-weight: $fw400;
color: $color-black;
font-size: $fs14;
line-height: $lh-128; // Original value was $fs18 => 1.125rem = 18px; 18px/14px = 128.571428571% => $lh-128 (rounded)
flex-grow: 1;
}
.close {
display: flex;
align-items: center;
}
svg {
width: 15px;
height: 15px;
transform: rotate(45deg);
}
}
.thread-groups {
max-height: calc(30rem - 40px);
overflow: auto;
hr {
background-color: $color-gray-10;
}
}
.thread-group .section-title {
color: $color-black;
}
.comment {
.author .name .fullname {
color: $color-gray-40;
}
.content {
color: $color-black;
}
}
}
.thread-groups-placeholder {
align-items: center;
display: flex;
flex-direction: column;
font-size: $fs12;
padding: $size-5;
text-align: center;
svg {
fill: $color-gray-20;
height: 24px;
margin-bottom: $size-5;
width: 24px;
}
}

View File

@ -1,254 +0,0 @@
.dashboard-fonts {
display: flex;
flex-direction: column;
align-items: center;
.dashboard-installed-fonts {
max-width: 1000px;
width: 100%;
display: flex;
margin-top: $size-5;
flex-direction: column;
h3 {
font-size: $fs14;
color: $color-gray-30;
margin: $size-1;
}
.font-item {
color: $color-black;
}
}
.installed-fonts-header {
color: $color-gray-40;
display: flex;
height: 40px;
font-size: $fs12;
background-color: $color-white;
align-items: center;
padding: 0px $size-5;
> .family {
min-width: 200px;
width: 200px;
}
> .variants {
padding-left: 12px;
}
.search-input {
display: flex;
flex-grow: 1;
justify-content: flex-end;
input {
font-size: $fs12;
border: 1px solid $color-gray-30;
border-radius: $br3;
width: 130px;
padding: $size-1;
margin: 0px;
}
}
}
.font-item {
color: $color-gray-40;
font-size: $fs14;
background-color: $color-white;
display: flex;
max-width: 1000px;
width: 100%;
min-height: 97px;
align-items: center;
padding: $size-5;
justify-content: space-between;
&:not(:first-child) {
border-top: 1px solid $color-gray-10;
}
input {
border: 1px solid $color-gray-30;
border-radius: $br3;
margin: 0px;
padding: $size-2;
font-size: $fs12;
}
> .family {
min-width: 200px;
width: 200px;
}
> .filenames {
min-width: 200px;
}
> .variants {
font-size: $fs14;
display: flex;
flex-wrap: wrap;
flex-grow: 1;
.variant {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
cursor: pointer;
.icon {
display: flex;
height: 16px;
width: 16px;
margin-left: 6px;
align-items: center;
svg {
fill: transparent;
width: 12px;
height: 12px;
transform: rotate(45deg);
}
}
&:hover {
.icon svg {
fill: $color-gray-30;
}
}
}
}
.filenames {
display: flex;
flex-direction: column;
font-size: $fs12;
}
.options {
display: flex;
justify-content: flex-end;
min-width: 180px;
.icon {
width: $size-5;
cursor: pointer;
display: flex;
margin-left: 10px;
justify-content: center;
align-items: center;
&.failure {
margin-right: 10px;
svg {
fill: $color-warning;
}
}
svg {
width: 16px;
height: 16px;
}
&.close {
svg {
transform: rotate(45deg);
}
}
}
}
}
.dashboard-fonts-upload {
max-width: 1000px;
width: 100%;
display: flex;
flex-direction: column;
.upload-button {
width: 100px;
}
.btn-secondary {
margin-left: 10px;
}
}
.dashboard-fonts-hero {
font-size: $fs14;
padding: $size-6;
background-color: $color-white;
margin-top: $size-6;
display: flex;
justify-content: space-between;
.banner {
background-color: $color-info-lighter;
display: grid;
grid-template-columns: 40px 1fr;
&:not(:last-child) {
margin-bottom: 10px;
}
.icon {
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 10px;
background-color: $color-info;
svg {
fill: $color-white;
}
}
.content {
margin: 10px;
}
&.warning {
background-color: $color-warning-lighter;
.icon {
background-color: $color-warning;
}
}
}
.desc {
h2 {
margin-bottom: $size-4;
color: $color-black;
}
width: 80%;
color: $color-gray-40;
}
.btn-primary {
flex-shrink: 0;
}
}
.fonts-placeholder {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
max-width: 1000px;
width: 100%;
height: 161px;
border: 1px dashed $color-gray-20;
margin-top: 16px;
.icon {
svg {
fill: $color-gray-40;
width: 32px;
height: 32px;
}
}
.label {
color: $color-gray-40;
font-size: $fs14;
}
}
}

View File

@ -1,527 +0,0 @@
// 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
.dashboard-grid {
font-size: $fs14;
height: 100%;
overflow: hidden;
overflow-y: auto;
margin-bottom: 0;
.grid-row {
display: grid;
width: 99%;
margin-left: 13px;
}
.grid-item {
align-items: center;
cursor: pointer;
display: flex;
flex-direction: column;
flex: 1 0 260px;
height: 230px;
margin: $size-3 $size-4 $size-4 $size-2;
position: relative;
text-align: center;
a,
button {
width: 100%;
font-weight: $fw400;
}
button {
background-color: transparent;
border: none;
}
@media #{$bp-max-1366} {
height: 200px;
flex: 1 0 230px;
}
&:hover {
.grid-item-th {
border: 2px solid $color-primary;
}
}
.grid-item-th {
border-radius: $br3;
border: 2px solid lighten($color-gray-20, 15%);
text-align: initial;
img {
object-fit: contain;
}
}
&.dragged {
border-radius: $br3;
border: 2px solid lighten($color-gray-20, 15%);
text-align: initial;
max-height: 160px;
}
&.placeholder {
min-width: 115px;
max-width: 115px;
display: flex;
flex-direction: column;
justify-content: center;
.placeholder-icon {
svg {
transform: rotate(-90deg);
width: 20px;
height: 20px;
fill: $color-gray-30;
}
}
.placeholder-label {
font-size: $fs14;
}
}
&.overlay {
border-radius: $br4;
border: 2px solid $color-primary;
height: 100%;
opacity: 0;
pointer-events: none;
position: absolute;
width: 100%;
z-index: 1;
}
&:hover .overlay {
display: block;
opacity: 1;
}
&.small-item {
max-width: 12%;
min-width: 190px;
padding: $size-4;
justify-content: center;
}
.grid-item-icon {
width: 90px;
height: 90px;
}
.info-wrapper {
display: grid;
grid-template-columns: 1fr auto;
}
.item-info {
display: grid;
padding: $size-2;
text-align: left;
width: 100%;
font-size: $fs12;
h3 {
border: 1px solid transparent;
color: $color-gray-60;
font-size: $fs14;
font-weight: $fw500;
overflow: hidden;
padding: 0;
height: 27px;
padding-right: $size-2;
text-overflow: ellipsis;
width: 100%;
white-space: nowrap;
line-height: $lh-192; // Original value was 27px; 27px/14px = 192.857142857% => $lh-192 (rounded)
max-width: 260px;
@media #{$bp-max-1366} {
max-width: 230px;
}
}
span.date {
color: $color-gray-30;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
white-space: nowrap;
max-width: 260px;
&::first-letter {
text-transform: capitalize;
}
@media #{$bp-max-1366} {
max-width: 230px;
}
}
.edit-wrapper {
.element-title {
padding: 0px;
height: 25px;
color: $color-gray-60;
font-size: $fs14;
font-weight: $fw400;
}
}
}
.item-badge {
background-color: $color-white;
border: 1px solid $color-gray-20;
border-radius: $br2;
position: absolute;
top: $size-2;
right: $size-2;
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
svg {
fill: $color-gray-30;
height: 16px;
width: 16px;
}
}
&.add-file {
border: 1px dashed $color-gray-20;
justify-content: center;
box-shadow: none;
span {
color: $color-gray-60;
font-size: $fs14;
}
&:hover {
background-color: $color-white;
border: 2px solid $color-primary;
}
}
// PROJECTS, ELEMENTS & ICONS GRID
&.project-th {
background-color: $color-white;
&:hover,
&:focus,
&:focus-within {
.project-th-actions {
opacity: 1;
}
a {
text-decoration: none;
}
}
.selected {
.grid-item-th {
border: 2px solid $color-primary;
}
}
.project-th-actions {
align-items: center;
opacity: 0;
display: flex;
right: 5px;
justify-content: center;
width: 30px;
height: 100%;
span {
color: $color-black;
}
.project-th-icon {
align-items: center;
display: flex;
margin-right: $size-2;
&.menu {
margin-right: 0;
display: flex;
justify-content: center;
align-items: flex-end;
flex-direction: column;
width: 100%;
height: 30px;
margin-top: 20px;
> svg {
fill: $color-gray-60;
margin-right: 0;
height: 18px;
width: 18px;
}
&:hover,
&:focus {
> svg {
fill: $color-primary-dark;
}
}
}
}
}
.project-th-actions.force-display {
opacity: 1;
}
}
// IMAGES SECTION
&.images-th {
border: 1px dashed $color-gray-20;
border-bottom: 2px solid lighten($color-gray-20, 12%);
&:hover {
border-color: $color-primary;
}
}
.grid-item-image {
svg {
max-height: 100px;
max-width: 100px;
min-height: 40px;
min-width: 40px;
width: 8vw;
}
}
.color-swatch {
border-top-left-radius: $br5;
border-top-right-radius: $br5;
height: 25%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
.color-data {
color: $color-gray-30;
margin-top: 15px;
}
.drag-counter {
position: absolute;
top: 5px;
left: 4px;
width: 32px;
height: 32px;
background-color: $color-primary;
border-radius: 50%;
color: $color-black;
font-size: $fs18;
display: flex;
justify-content: center;
align-items: center;
}
}
.grid-item-th {
background-position: center;
background-size: auto 80%;
background-repeat: no-repeat;
border-top-left-radius: $br3;
border-top-right-radius: $br3;
height: 230px;
max-height: 160px;
overflow: hidden;
position: relative;
width: 100%;
background-color: $color-canvas;
display: flex;
justify-content: center;
flex-direction: row;
.img-th {
height: auto;
width: 100%;
}
svg {
height: 100%;
width: 100%;
}
svg#loader-pencil {
fill: $color-gray-20;
}
}
// LIBRARY VIEW
.grid-item {
.library {
height: 580px;
}
&.project-th.library {
height: 610px;
width: 300px;
}
.grid-item-th.library {
background-color: $color-gray-50;
flex-direction: column;
height: 90%;
justify-content: flex-start;
max-height: 550px;
padding: $size-6;
.asset-section {
font-size: $fs12;
color: $color-gray-20;
&:not(:first-child) {
margin-top: $size-4;
}
}
.asset-title {
display: flex;
font-size: $fs12;
text-transform: uppercase;
& .num-assets {
color: $color-gray-30;
}
}
.asset-list-item {
display: flex;
align-items: center;
border: 1px solid transparent;
border-radius: $br3;
margin-top: $size-1;
padding: 2px;
font-size: $fs12;
color: $color-white;
position: relative;
& .name-block {
color: $color-gray-20;
width: calc(100% - 24px - #{$size-2});
}
& .item-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
& svg {
background-color: $color-canvas;
border-radius: $br4;
border: 2px solid transparent;
height: 24px;
width: 24px;
margin-right: $size-2;
}
& .color-name {
color: $color-white;
}
& .color-value {
margin-left: $size-1;
color: $color-gray-30;
text-transform: uppercase;
}
& .typography-sample {
height: 20px;
margin-right: $size-1;
width: 20px;
}
}
}
}
}
.grid-empty-placeholder {
border-radius: $br12;
display: grid;
background-color: rgba(227, 227, 227, 0.3);
padding: 13px;
margin-right: 13px;
height: 230px;
&.loader {
justify-items: center;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
}
&.libs {
background-image: url(/images/ph-left.svg), url(/images/ph-right.svg);
background-position:
15% bottom,
85% top;
background-repeat: no-repeat;
align-items: center;
border: 1px dashed #b1b2b5;
border-radius: $br3;
display: flex;
flex-direction: column;
height: 200px;
margin: 1rem;
padding: 3rem;
justify-content: center;
.text {
p {
max-width: 360px;
text-align: center;
font-size: $fs16;
}
}
}
.create-new {
background-color: white;
border: 2px solid $color-gray-10;
border-radius: $br3;
color: $color-black;
cursor: pointer;
height: 158px;
font-family: "worksans", sans-serif;
margin: 0.5rem;
&:hover {
border: 2px solid $color-primary;
}
}
&.search {
align-items: center;
display: flex;
justify-content: center;
flex-direction: column;
height: 200px;
background: $color-white;
border: 1px dashed #e3e3e3;
border-radius: $br0;
}
svg {
width: 36px;
height: 36px;
fill: $color-gray-20;
}
.text {
margin-top: 10px;
color: $color-gray-30;
font-size: $fs16;
}
}

View File

@ -1,139 +0,0 @@
// 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
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
background-color: $color-white;
height: 63px;
padding: $size-1 $size-4 $size-1 $size-2;
position: relative;
z-index: 10;
user-select: none;
&.team {
display: grid;
grid-template-columns: 20% 1fr 20%;
}
.element-name {
margin-right: $size-2;
}
.btn-secondary {
flex-shrink: 0;
z-index: 10;
height: 32px;
}
svg {
fill: $color-black;
height: 14px;
margin-right: $size-1;
width: 14px;
}
nav {
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 1;
ul {
display: flex;
font-size: $fs14;
justify-content: center;
margin: 0;
}
li {
a {
display: flex;
align-items: center;
flex-basis: 140px;
border-bottom: 3px solid transparent;
color: $color-gray-30;
height: 40px;
padding: $size-1 $size-5;
font-weight: $fw400;
&:hover {
color: $color-black;
text-decoration: none;
}
}
&.active {
a {
color: $color-black;
border-color: $color-primary;
}
}
}
}
.dashboard-title {
display: flex;
align-items: center;
margin-left: 13px;
h1 {
color: $color-black;
display: flex;
flex-shrink: 0;
font-size: $fs22;
font-weight: $fw600;
z-index: 10;
user-select: all;
}
.context-menu.is-open {
margin-top: 10px;
}
}
.icon {
display: flex;
align-items: center;
cursor: pointer;
margin-left: $size-2;
z-index: 10;
svg {
fill: $color-gray-40;
width: 15px;
height: 15px;
&:hover {
fill: $color-primary-dark;
}
}
}
.dashboard-buttons {
display: flex;
justify-content: flex-end;
align-items: center;
}
.dashboard-header-actions {
display: flex;
}
.pin-icon {
margin: 0 $size-2 0 $size-5;
background-color: transparent;
border: none;
svg {
fill: $color-gray-20;
}
&.active {
svg {
fill: $color-gray-50;
}
}
}
}

View File

@ -1,303 +0,0 @@
// Copyright (c) 2020 KALEIDOS INC
// 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
.dashboard-sidebar {
&.settings {
.back-to-dashboard {
padding: 12px 18px;
font-size: $fs14;
cursor: pointer;
display: flex;
.icon {
display: flex;
align-items: center;
margin-right: 14px;
}
.text {
color: $color-gray-60;
}
svg {
fill: $color-gray-60;
transform: rotate(90deg);
width: 12px;
height: 12px;
}
}
}
}
.dashboard-settings {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
.form-container {
margin-top: 50px;
display: flex;
max-width: 368px;
margin-bottom: 2rem;
width: 100%;
&.two-columns {
max-width: 536px;
justify-content: space-between;
flex-direction: row;
}
h2 {
margin-bottom: 1rem;
}
}
.avatar-form {
display: flex;
flex-direction: column;
width: 120px;
min-width: 120px;
img {
border-radius: 50%;
flex-shrink: 0;
height: 120px;
margin-right: $size-4;
width: 120px;
}
.image-change-field {
position: relative;
width: 120px;
height: 120px;
.update-overlay {
opacity: 0;
cursor: pointer;
position: absolute;
width: 121px;
height: 121px;
border-radius: 50%;
font-size: $fs24;
color: $color-white;
line-height: $lh-500; // Original value was 120px; 120px/24px = 500% => $lh-500
text-align: center;
background: $color-primary-dark;
z-index: 14;
}
input[type="file"] {
width: 120px;
height: 120px;
position: absolute;
opacity: 0;
cursor: pointer;
top: 0;
z-index: 15;
}
&:hover {
.update-overlay {
opacity: 0.8;
}
}
}
}
.profile-form {
display: flex;
flex-direction: column;
max-width: 368px;
width: 100%;
.newsletter-subs {
border-bottom: 1px solid $color-gray-20;
border-top: 1px solid $color-gray-20;
padding: 30px 0;
margin-bottom: 31px;
.newsletter-title {
font-family: "worksans", sans-serif;
color: $color-gray-30;
font-size: $fs14;
}
label {
font-family: "worksans", sans-serif;
color: $color-gray-60;
font-size: $fs12;
margin-right: -17px;
margin-bottom: 13px;
}
.info {
font-family: "worksans", sans-serif;
color: $color-gray-30;
font-size: $fs12;
margin-bottom: 8px;
}
.input-checkbox label {
align-items: flex-start;
}
}
}
.options-form,
.password-form {
h2 {
font-size: $fs14;
margin-bottom: 20px;
}
}
}
.dashboard-access-tokens {
display: flex;
flex-direction: column;
align-items: center;
.access-tokens-hero-container {
max-width: 1000px;
width: 100%;
display: flex;
flex-direction: column;
}
.access-tokens-hero {
font-size: $fs14;
padding: $size-6;
background-color: $color-white;
margin-top: $size-6;
display: flex;
justify-content: space-between;
.desc {
width: 80%;
color: $color-gray-40;
h2 {
margin-bottom: $size-4;
color: $color-black;
}
p {
font-size: $fs16;
}
}
.btn-primary {
flex-shrink: 0;
}
}
.access-tokens-empty {
text-align: center;
max-width: 1000px;
width: 100%;
padding: $size-6;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px dashed $color-gray-20;
color: $color-gray-40;
margin-top: 12px;
min-height: 136px;
}
.table-row {
background-color: $color-white;
display: grid;
grid-template-columns: 1fr 43% 12px;
height: 63px;
&:not(:first-child) {
margin-top: 8px;
}
}
.table-field {
&.name {
color: $color-gray-60;
}
&.expiration-date {
color: $color-gray-40;
font-size: $fs14;
.content {
padding: 2px 5px;
&.expired {
background-color: $color-warning-lighter;
border-radius: $br4;
color: $color-gray-40;
}
}
}
&.access-token-created {
word-break: break-all;
}
&.actions {
position: relative;
}
}
}
.access-tokens-modal {
.action-buttons {
gap: 10px;
.cancel-button {
border: 1px solid $color-gray-30;
background: $color-canvas;
border-radius: $br3;
padding: 0.5rem 1rem;
cursor: pointer;
margin-right: 8px;
&:hover {
background: $color-gray-20;
}
}
}
.access-token-created {
position: relative;
word-break: break-all;
.custom-input input {
background-color: $color-success-lighter;
border: 0;
padding: 0 0 0 15px;
}
}
.help-icon {
border: none;
height: 40px;
width: 40px;
position: absolute;
top: 0;
right: 0;
cursor: pointer;
background-color: $color-success-lighter;
svg {
fill: $color-gray-30;
}
&:hover {
svg {
fill: $color-gray-60;
}
}
}
.token-created-info {
font-size: $fs12;
color: $color-gray-40;
}
}

View File

@ -1,476 +0,0 @@
// 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
.dashboard-sidebar {
background-color: $color-white;
z-index: 1;
display: flex;
flex-direction: column;
height: 100%;
padding-top: $size-2;
.sidebar-content {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding: 0;
hr {
border-color: $color-gray-10;
margin: 1rem 15px;
}
}
.sidebar-team-switch {
position: relative;
display: flex;
margin: 5px 15px;
.teams-dropdown {
left: 0;
top: 50px;
z-index: 12;
max-height: 30rem;
min-width: 234px;
overflow-y: auto;
}
.options-dropdown {
right: 2px;
top: 50px;
z-index: 12;
max-height: 30rem;
min-width: 162px;
}
.switch-content {
height: 40px;
display: flex;
width: 100%;
border: 1px solid $color-gray-10;
border-radius: $br5;
align-items: center;
}
.switch-options {
display: flex;
max-width: 22px;
min-width: 28px;
border-left: 1px solid $color-gray-10;
justify-content: center;
align-items: center;
cursor: pointer;
background-color: transparent;
border: none;
svg {
width: 15px;
height: 13px;
fill: $color-gray-60;
}
}
.current-team {
cursor: pointer;
display: flex;
align-items: center;
flex-grow: 1;
font-size: $fs14;
padding: 0px 10px;
background-color: transparent;
border: none;
}
.team-name {
flex-grow: 1;
display: flex;
height: 40px;
align-items: center;
&.action {
.team-icon {
border-radius: 50%;
background-color: $color-gray-10;
height: 24px;
margin-right: 10px;
padding: 6px;
width: 24px;
svg {
height: 12px;
width: 12px;
}
}
&:hover {
.team-icon {
background-color: $color-primary;
}
}
.team-text {
width: 150px;
}
}
.team-icon {
display: flex;
align-items: center;
padding-right: 10px;
svg {
width: 23px;
height: 23px;
fill: $color-gray-60;
}
img {
border-radius: 50%;
flex-shrink: 0;
height: 23px;
width: 23px;
}
}
.team-text {
color: $color-gray-60;
@include text-ellipsis;
width: 130px;
text-align: left;
}
.icon {
margin-left: auto;
svg {
fill: $color-gray-60;
}
}
}
.switch-icon {
display: flex;
align-items: center;
justify-content: center;
svg {
width: 10px;
height: 10px;
fill: $color-gray-60;
}
}
}
.sidebar-empty-placeholder {
padding: 10px 12px;
color: $color-gray-30;
display: flex;
align-items: flex-start;
.icon {
padding: 0px 10px;
svg {
fill: $color-gray-30;
width: 12px;
height: 12px;
}
}
.text {
font-size: $fs12;
}
}
.sidebar-nav {
display: flex;
flex-direction: column;
overflow-y: auto;
margin: 0;
user-select: none;
// TODO: should be deprecated / unclear name
&.dashboard-common {
overflow: unset;
}
&.no-overflow {
overflow: unset;
}
& > li {
align-items: center;
cursor: pointer;
display: flex;
flex-shrink: 0;
padding: $size-2;
a {
font-weight: $fw400;
width: 100%;
&:hover {
text-decoration: none;
}
}
svg {
fill: $color-black;
margin-right: 8px;
height: $size-3;
width: $size-3;
}
span.element-title {
color: $color-gray-60;
font-size: $fs14;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&::before {
background-color: transparent;
border-radius: $br3;
content: "";
height: 26px;
margin-right: $size-2;
width: 4px;
}
&.recent-projects {
svg {
fill: $color-white;
}
}
& .edit-wrapper {
border: 1px solid $color-gray-10;
border-radius: $br3;
display: flex;
width: 100%;
}
input.element-title {
border: 0;
height: 30px;
padding: 5px;
margin: 0;
width: 100%;
background-color: $color-white;
}
.close {
background-color: $color-white;
cursor: pointer;
padding-left: 5px;
svg {
fill: $color-gray-30;
height: 15px;
transform: rotate(45deg) translateY(7px);
width: 15px;
margin: 0;
}
}
.element-subtitle {
color: $color-gray-20;
font-style: italic;
}
&:hover {
&::before {
background-color: $color-gray-10;
}
}
&.current {
a {
font-weight: $fw700;
}
&::before {
background-color: $color-primary;
}
}
&.dragging {
background-color: color.adjust($color-primary, $alpha: -0.69);
}
}
}
.sidebar-search {
align-items: center;
background-color: $color-white;
border: 1px solid $color-gray-10;
border-radius: $br5;
display: flex;
margin: 5px 15px;
.input-text {
background: transparent;
border: 0;
color: $color-gray-60;
font-size: $fs14;
padding: 6px;
margin: 0;
max-width: 195px;
width: 100%;
height: 40px;
}
&:focus,
&:focus-within {
border-color: $color-black;
}
.search,
.clear-search {
align-items: center;
cursor: pointer;
display: flex;
height: 22px;
margin-left: auto;
padding: 0 $size-2;
width: 32px;
svg {
fill: $color-gray-30;
height: 15px;
width: 15px;
}
}
.clear-search svg {
transform: rotate(45deg);
&:hover {
fill: $color-danger;
}
}
}
&.profile-bar {
background-color: $color-gray-10;
.dashboard-sidebar-inside {
display: none;
}
}
}
.projects-row {
align-items: center;
display: flex;
margin-top: 1rem;
padding: $size-2;
position: relative;
span {
color: $color-gray-30;
font-size: $fs14;
}
.btn-icon-light {
margin-left: auto;
}
&::before {
background-color: $color-gray-10;
content: "";
height: 1px;
left: 4%;
position: absolute;
right: 4%;
top: -5px;
width: 92%;
}
}
.team-form-modal {
h2 {
font-weight: $fw400;
color: $color-gray-40;
font-size: 28px;
margin-bottom: 30px;
}
.buttons-row {
margin-top: 20px;
display: flex;
justify-content: center;
}
input[type="submit"] {
margin-bottom: 0px;
}
}
.profile-section {
align-items: center;
cursor: pointer;
display: flex;
padding: 10px 15px;
position: relative;
.profile {
align-items: center;
cursor: pointer;
display: flex;
flex-grow: 1;
span {
@include text-ellipsis;
color: $color-black;
margin: 10px;
font-size: $fs14;
max-width: 160px;
}
img {
border-radius: 50%;
flex-shrink: 0;
height: 25px;
width: 25px;
}
svg {
height: 10px;
margin-left: auto;
margin-right: $size-2;
width: 10px;
}
}
.dropdown {
left: 15px;
bottom: 45px;
min-width: 189px;
@include animation(0, 0.2s, fadeInUp);
li {
font-size: $fs14;
padding: $size-2 $size-4;
svg {
fill: $color-gray-20;
margin-right: $size-2;
height: 12px;
width: 12px;
}
&.separator {
border-top: 1px solid $color-gray-10;
}
}
}
}
.primary-badge {
border: 1px solid $color-primary;
border-radius: $br2;
font-size: $fs9 !important;
font-weight: $fw500;
color: $color-primary !important;
padding: 2px 4px;
}

View File

@ -1,605 +0,0 @@
.dashboard-invite-modal {
top: 72px;
right: 13px;
padding: 32px;
box-shadow: 0px 4px 8px rgba($color-black, 0.25);
border-radius: $br8;
width: 400px;
position: fixed;
z-index: 16;
&.hero {
top: 218px;
right: 35px;
}
form {
width: 100%;
}
.form-row {
display: flex;
justify-content: space-between;
margin: 4px 0px;
.label {
margin-bottom: 0;
display: flex;
align-items: center;
}
}
.custom-input {
width: 100%;
min-height: 116px;
max-height: 176px;
overflow-y: hidden;
input {
&.no-padding {
padding-top: 12px;
height: 50px;
}
min-height: 40px;
}
.selected-items {
gap: 8px;
padding: 8px;
max-height: 132px;
overflow-y: scroll;
.selected-item {
.around {
height: 24px;
display: flex;
align-items: center;
justify-content: flex-start;
width: fit-content;
.icon {
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
.custom-select {
width: 180px;
overflow: hidden;
justify-content: normal;
select {
height: auto;
}
}
.action-buttons {
display: flex;
margin-top: 16px;
input[type="submit"] {
margin-bottom: 0px;
}
}
.title {
color: $color-black;
font-weight: $fw700;
margin-bottom: 16px;
}
.hint {
font-size: $fs12;
&.hidden {
display: none;
}
}
svg {
width: 12px;
height: 12px;
fill: $color-gray-20;
}
.error,
.warning {
width: 100%;
display: flex;
.icon {
text-align: center;
padding: 5px;
svg {
fill: $color-white;
width: 20px;
height: 20px;
margin: 5px;
}
}
.text {
color: $color-black;
padding: 5px;
font-size: $fs12;
}
}
.error {
background-color: #ffd9e0;
.icon {
background-color: $color-danger;
}
}
.warning {
background-color: #ffeaca;
.icon {
background-color: $color-warning;
}
}
}
.dashboard-team-members,
.dashboard-team-invitations,
.dashboard-team-webhooks {
.empty-invitations {
height: 156px;
max-width: 1040px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border: 1px dashed $color-gray-20;
margin-top: 16px;
}
.table-header {
user-select: none;
}
.table-row {
background-color: $color-white;
height: 63px;
&:not(:first-child) {
margin-top: 16px;
}
}
.table-field {
&.name {
width: 43%;
min-width: 300px;
display: flex;
.member-info {
display: flex;
flex-direction: column;
margin-left: 16px;
.member-name {
font-size: $fs16;
.you {
color: $color-gray-30;
margin-left: 5px;
}
}
.member-email {
color: $color-gray-30;
font-size: $fs12;
}
}
.member-image {
height: 32px;
width: 32px;
img {
border-radius: 50%;
}
}
}
&.roles {
flex-grow: 1;
cursor: default;
position: relative;
.rol-label {
user-select: none;
}
.rol-selector {
&.has-priv {
border: 1px solid $color-gray-20;
cursor: pointer;
}
min-width: 160px;
height: 32px;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: $br2;
padding: 3px 8px;
font-size: $fs14;
}
}
&.actions {
position: relative;
.actions-dropdown {
max-height: 30rem;
min-width: 180px;
}
}
&.status {
.status-badge {
color: $color-white;
border-radius: $br12;
min-width: 74px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
&.pending {
background-color: $color-warning;
}
&.expired {
background-color: $color-gray-20;
}
.status-label {
font-size: $fs12;
}
}
}
&.uri {
flex-grow: 1;
}
&.active {
min-width: 100px;
}
&.last-delivery {
display: flex;
justify-content: center;
width: 50px;
position: relative;
.success svg {
fill: $color-primary;
width: 16px;
height: 16px;
}
.failure svg {
fill: $color-warning;
width: 16px;
height: 16px;
}
.icon-container {
width: 16px;
height: 16px;
overflow-x: visible;
}
.icon {
padding: 0;
}
}
.tooltip {
display: none;
position: absolute;
top: -58px;
left: 50%;
transform: translate(-50%, 0);
text-align: center;
.label {
border-radius: $br3;
color: $color-white;
background-color: $color-black;
white-space: nowrap;
padding: 12px 20px;
}
.arrow-down {
margin: 0 auto;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid $color-black;
}
}
.last-delivery-icon:hover {
.tooltip {
display: block;
}
}
}
.dropdown {
position: absolute;
max-height: 30rem;
overflow-y: auto;
background-color: $color-white;
border-radius: $br4;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
z-index: 12;
top: 30px;
left: -151px;
width: 155px;
hr {
margin: 0;
border-color: $color-gray-10;
}
li {
display: flex;
align-items: center;
color: $color-gray-60;
cursor: pointer;
font-size: $fs14;
height: 31px;
padding: 5px 16px;
&.title {
font-weight: $fw600;
cursor: default;
}
&:hover {
background-color: $color-primary-lighter;
}
}
}
}
.dashboard-team-settings {
.team-settings {
display: flex;
justify-content: center;
margin-top: 16px;
svg {
width: 20px;
height: 20px;
}
.horizontal-blocks {
display: flex;
max-width: 1010px;
justify-content: space-between;
width: 100%;
}
.block {
display: flex;
max-width: 324px;
width: 324px;
background-color: $color-white;
flex-direction: column;
padding: 12px;
.label {
font-size: $fs13;
color: $color-gray-30;
}
}
.info-block {
position: relative;
.name {
margin-top: 10px;
font-size: $fs24;
color: $color-black;
@include text-ellipsis;
margin-right: 90px;
}
.icon {
position: absolute;
padding: 15px;
width: 100px;
height: 100px;
right: 0px;
top: 0px;
img {
border-radius: 50%;
width: 70px;
height: 70px;
}
.update-overlay {
opacity: 0;
cursor: pointer;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 71px;
height: 71px;
border-radius: 50%;
color: $color-white;
background: $color-primary-dark;
z-index: 14;
svg {
fill: $color-white;
}
}
&:hover {
.update-overlay {
opacity: 1;
width: 72px;
height: 72px;
top: 14px;
left: 14px;
}
}
}
}
.owner-block {
img {
width: 30px;
height: 30px;
border-radius: 50%;
}
svg {
width: 12px;
height: 12px;
fill: $color-black;
}
.owner {
margin-top: 5px;
display: flex;
align-items: center;
color: $color-black;
.icon {
margin-right: 12px;
}
}
.summary {
margin-top: 5px;
color: $color-black;
.icon {
padding: 0px 10px;
margin-right: 12px;
}
}
}
.stats-block {
svg {
fill: $color-black;
}
.projects,
.files {
margin-top: 7px;
display: flex;
align-items: center;
color: $color-black;
.icon {
display: flex;
align-items: center;
padding: 0px 2px;
margin-right: 14px;
}
}
}
}
}
.dashboard-team-webhooks {
display: flex;
flex-direction: column;
align-items: center;
.webhooks-hero-container {
max-width: 1000px;
width: 100%;
display: flex;
flex-direction: column;
.upload-button {
width: 100px;
}
.btn-secondary {
margin-left: 10px;
}
}
.webhooks-hero {
font-size: $fs14;
padding: $size-6;
background-color: $color-white;
margin-top: $size-6;
display: flex;
justify-content: space-between;
.banner {
background-color: unset;
display: flex;
.icon {
display: flex;
align-items: center;
padding-left: 0px;
padding-right: 10px;
svg {
fill: $color-info;
}
}
}
.desc {
h2 {
margin-bottom: $size-4;
color: $color-black;
}
width: 80%;
color: $color-gray-40;
p {
font-size: $fs16;
}
}
.btn-primary {
flex-shrink: 0;
}
}
.webhooks-empty {
text-align: center;
max-width: 1000px;
width: 100%;
padding: $size-6;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px dashed $color-gray-20;
color: $color-gray-40;
margin-top: 12px;
min-height: 136px;
}
}
.webhooks-modal {
.action-buttons {
gap: 10px;
.cancel-button {
border: 1px solid $color-gray-30;
background: $color-canvas;
border-radius: $br3;
padding: 0.5rem 1rem;
cursor: pointer;
margin-right: 8px;
&:hover {
background: $color-gray-20;
}
}
}
.input-checkbox label {
font-size: $fs14;
color: $color-black;
}
.explain {
font-size: $fs12;
color: $color-gray-40;
}
}

View File

@ -1,638 +0,0 @@
// 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
.team-hero {
display: flex;
position: relative;
border: 2px solid $color-gray-10;
border-radius: $br8;
padding: 20px;
margin: 0 1rem 0 21px;
height: 154px;
.text {
flex-grow: 1;
padding-left: 20px;
.title {
font-size: $fs24;
font-weight: $fw700;
color: $color-black;
}
.info {
span {
color: $color-gray-30;
display: block;
}
padding-top: 10px;
}
}
.close {
position: absolute;
top: 20px;
right: 20px;
background-color: transparent;
border: none;
cursor: pointer;
svg {
transform: rotate(45deg);
width: 16px;
height: 16px;
}
}
.invite {
align-self: flex-end;
height: 40px;
font-family: "worksans", sans-serif;
width: 180px;
}
img {
width: 274px;
margin-bottom: -19px;
@media (max-width: 1200px) {
display: none;
width: 0;
}
}
}
.hero-projects {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 30px;
margin: 0 1rem 1rem 1.2rem;
.tutorial,
.walkthrough {
display: grid;
grid-template-columns: 1fr 1fr;
position: relative;
border: 2px solid $color-gray-10;
border-radius: $br8;
min-height: 211px;
.thumbnail {
border-top-left-radius: $br6;
border-bottom-left-radius: $br6;
padding: 30px;
display: block;
background-color: #e0e4e9;
}
.text {
padding: 30px;
.title {
color: $color-black;
font-size: $fs24;
font-weight: $fw700;
margin-bottom: 8px;
}
.info {
color: $color-gray-50;
margin-bottom: 20px;
font-size: $fs14;
}
}
.action {
font-family: "worksans", sans-serif;
width: 180px;
height: 40px;
}
.close {
position: absolute;
top: 0;
right: 0;
width: $size-5;
cursor: pointer;
display: flex;
margin: 20px;
justify-content: center;
align-items: center;
background-color: transparent;
border: none;
.icon {
svg {
fill: $color-gray-30;
height: 16px;
width: 16px;
transform: rotate(45deg);
&:hover {
fill: $color-primary;
}
}
}
}
@media (max-width: 1846px) {
grid-template-columns: 190px 1fr;
}
@media (max-width: 1526px) {
grid-template-columns: 1fr;
.img {
display: none;
width: 0;
}
}
}
.walkthrough {
.thumbnail {
background-image: url("/images/walkthrough-cover.png");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
}
.tutorial {
.thumbnail {
background-image: url("/images/hands-on-tutorial.png");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.loader {
display: flex;
svg#loader-pencil {
width: 31px;
}
}
}
}
.dashboard-container {
background-color: $color-dashboard;
flex: 1 0 0;
margin-right: $size-4;
overflow-y: auto;
width: 100%;
&.dashboard-projects {
user-select: none;
}
&.no-bg {
background-color: transparent;
}
&.dashboard-shared {
width: calc(100vw - 320px);
margin-right: 50px;
}
&.search {
margin-top: 10px;
}
}
.dashboard-project-row {
margin-bottom: $size-5;
position: relative;
.project {
align-items: center;
background: $color-white;
border-radius: $br3;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: $size-4;
padding: $size-2 $size-2 $size-2 $size-4;
width: 99%;
max-height: 40px;
gap: $size-2;
.project-name-wrapper {
display: flex;
align-items: center;
justify-content: flex-start;
min-height: 32px;
margin-left: $size-2;
}
.show-more {
align-items: center;
color: $color-gray-30;
display: flex;
font-size: $fs14;
justify-content: space-between;
cursor: pointer;
background-color: transparent;
border: none;
.placeholder-icon {
transform: rotate(-90deg);
margin-left: 10px;
svg {
height: 14px;
width: 14px;
fill: $color-gray-30;
}
}
&:hover {
color: $color-primary-dark;
svg {
fill: $color-primary-dark;
}
}
}
.btn-secondary {
border: none;
padding: $size-2;
}
h2 {
cursor: pointer;
font-size: $fs18;
line-height: $lh-088; // Original value was 1rem = 16px; 16px/18px = 88.88888% => $lh-088
font-weight: $fw600;
color: $color-black;
margin-right: $size-1;
}
.edit-wrapper {
margin-right: $size-4;
}
.info {
font-size: $fs14;
line-height: $lh-115; // Original value was 1rem = 16px; 16px/14px = 114.285714286% => $lh-115 (rounded)
font-weight: $fw400;
color: $color-gray-60;
margin-left: 0.75rem;
@media (max-width: 760px) {
display: none;
}
}
.project-actions {
display: flex;
opacity: 0;
margin-left: $size-6;
.btn-small {
height: 32px;
margin: 0 $size-2;
width: 32px;
}
}
.pin-icon {
cursor: pointer;
display: flex;
align-items: center;
margin-right: 14px;
background-color: transparent;
border: none;
svg {
width: 15px;
height: 15px;
fill: $color-gray-20;
}
&.active {
svg {
fill: $color-gray-50;
}
}
}
}
&:hover,
&:focus,
&:focus-within {
.project-actions {
opacity: 1;
}
}
.show-more {
align-items: center;
color: $color-gray-30;
display: flex;
font-size: $fs14;
justify-content: space-between;
cursor: pointer;
background-color: transparent;
border: none;
position: absolute;
top: 9px;
right: 53px;
.placeholder-icon {
transform: rotate(-90deg);
margin-left: 10px;
svg {
height: 14px;
width: 14px;
fill: $color-gray-30;
}
}
&:hover {
color: $color-primary-dark;
svg {
fill: $color-primary-dark;
}
}
}
}
.recent-files-row-title-info {
color: $color-gray-60;
line-height: $lh-115; // Original value was 1rem = 16px; 16px/14px = 114.285714286% => $lh-115
font-size: $fs14;
font-weight: $fw400;
@media (max-width: 880px) {
display: none;
}
}
.dashboard-table {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
font-size: $fs16;
&.team-members {
margin-bottom: 52px;
}
&.invitations {
.table-row {
display: grid;
grid-template-columns: 43% 1fr 109px 12px;
}
}
.table-header {
display: grid;
grid-template-columns: 43% 1fr 109px 12px;
max-width: 1040px;
background-color: $color-white;
color: $color-gray-30;
width: 100%;
height: 40px;
padding: 0px 16px;
}
.table-rows {
display: flex;
flex-direction: column;
max-width: 1040px;
width: 100%;
margin-top: 16px;
color: $color-black;
}
.table-row {
display: flex;
width: 100%;
height: 45px;
align-items: center;
padding: 0px 16px;
}
.table-field {
display: flex;
align-items: center;
.icon {
padding-left: 10px;
cursor: pointer;
}
}
svg {
width: 10px;
height: 10px;
fill: $color-black;
}
}
.edit-wrapper {
border: 1px solid $color-gray-10;
border-radius: $br3;
display: flex;
padding-right: $size-5;
position: relative;
input.element-title {
border: 0;
height: 30px;
padding: 5px;
margin: 0;
width: 100%;
background-color: $color-white;
}
.close {
cursor: pointer;
position: absolute;
top: 1px;
right: 2px;
svg {
fill: $color-gray-30;
height: 15px;
transform: rotate(45deg) translateY(7px);
width: 15px;
margin: 0;
}
}
}
.import-file-btn {
align-items: center;
display: flex;
flex-direction: column;
height: 2rem;
justify-content: center;
overflow: hidden;
padding: 4px;
width: 2rem;
background: none;
border: 1px solid $color-gray-20;
border-radius: $br2;
cursor: pointer;
transition: all 0.4s;
margin-left: 1rem;
&:hover {
background: $color-primary;
}
svg {
width: 16px;
height: 16px;
}
}
.dashboard-templates-section {
position: absolute;
display: flex;
flex-direction: column;
justify-content: flex-end;
bottom: 0;
width: 100%;
height: 228px;
transition: bottom 300ms;
pointer-events: none;
&.collapsed {
bottom: -228px;
transition: bottom 300ms;
}
.title {
pointer-events: all;
width: fit-content;
top: -56px;
right: -28px;
text-align: right;
height: 56px;
position: absolute;
button {
border: none;
cursor: pointer;
height: 58px;
display: inline-flex;
align-items: center;
border-top: 2px solid #e4e4e4;
border-left: 2px solid #e4e4e4;
border-right: 2px solid #e4e4e4;
border-top-left-radius: $br10;
border-top-right-radius: $br10;
margin-right: 30px;
background-color: $color-white;
position: relative;
z-index: 1;
span {
display: inline-block;
vertical-align: middle;
line-height: $lh-normal;
font-size: $fs18;
font-weight: $fw600;
color: $color-black;
margin-left: 18px;
margin-right: 10px;
&.icon {
margin-left: 10px;
margin-right: 16px;
}
}
svg {
width: 12px;
height: 12px;
}
}
}
.button {
position: absolute;
top: 133px;
border: 2px solid #e0e4e9;
border-radius: 50%;
text-align: center;
width: 35px;
height: 35px;
cursor: pointer;
background-color: $color-white;
display: flex;
align-items: center;
justify-content: center;
pointer-events: all;
svg {
width: 12px;
height: 12px;
}
&.left {
left: 0;
margin-left: 43px;
}
&.right {
right: 0;
margin-right: 43px;
}
&:hover {
border: 2px solid $color-primary;
}
}
.content {
pointer-events: all;
background-color: $color-white;
width: 200%;
height: 229px;
border-top: 2px solid #e4e4e4;
border-left: 2px solid #e4e4e4;
margin-left: 5px;
position: absolute;
.card-container {
width: 275px;
margin-top: 20px;
display: inline-block;
text-align: center;
vertical-align: top;
background-color: transparent;
border: none;
padding: 0;
}
.template-card {
display: inline-block;
width: 255px;
font-size: $fs16;
color: #181a22;
cursor: pointer;
.img-container {
width: 100%;
height: 135px;
margin-bottom: 15px;
border-radius: $br5;
border: 2px solid #e0e4e9;
display: flex;
justify-content: center;
flex-direction: column;
}
.card-name {
padding: 0 5px;
display: flex;
justify-content: space-between;
height: 23px;
svg {
width: 16px;
height: 16px;
}
span {
font-weight: $fw500;
font-size: $fs14;
}
}
.template-link {
border: 2px solid transparent;
margin: 30px;
padding: 32px 0;
}
.template-link-title {
font-size: $fs14;
font-weight: $fw600;
color: $color-gray-60;
}
.template-link-text {
font-size: $fs12;
margin-top: $size-2;
color: $color-gray-50;
}
&:hover {
.img-container {
border: 2px solid $color-primary;
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,243 +0,0 @@
.share-modal {
background: none;
display: block;
top: 50px;
left: calc(100vw - 500px);
.share-link-dialog {
width: 480px;
background-color: $color-white;
box-shadow: 0px 2px 8px 0px rgb(0 0 0 / 20%);
.modal-content {
padding: 16px 32px;
&:first-child {
border-top: 0px;
padding: 0;
height: 50px;
display: flex;
justify-content: center;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
margin-left: 32px;
h2 {
font-size: $fs18;
color: $color-black;
}
.modal-close-button {
margin-right: 16px;
}
}
.share-link-section {
.custom-input {
display: flex;
flex-direction: row;
margin-bottom: 15px;
border: 1px solid $color-gray-20;
input {
padding: 0 0 0 15px;
border: none;
}
}
.hint-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
.hint {
font-size: $fs12;
color: $color-gray-40;
}
.confirm-dialog {
display: flex;
flex-direction: column;
background-color: unset;
.actions {
display: flex;
justify-content: flex-end;
gap: 16px;
}
.description {
font-size: $fs12;
margin-bottom: 16px;
color: $color-black;
}
.btn-primary,
.btn-secondary,
.btn-warning,
.btn-danger {
width: 126px;
margin-bottom: 0px;
&:not(:last-child) {
margin-right: 10px;
}
}
}
}
label {
font-size: $fs12;
color: $color-black;
}
.help-icon {
height: 40px;
width: 40px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
position: relative;
right: 0;
top: 0;
background-color: $color-gray-10;
border-left: 1px solid $color-gray-20;
svg {
fill: $color-gray-30;
}
&:hover {
background-color: $color-primary;
svg {
fill: $color-gray-60;
}
}
}
input {
margin: 0;
}
}
&.ops-section {
.manage-permissions {
display: flex;
color: $color-primary-dark;
font-size: $fs12;
cursor: pointer;
.icon {
svg {
height: 16px;
width: 16px;
fill: $color-primary-dark;
}
}
.title {
margin-left: 8px;
}
}
.view-mode {
min-height: 34px;
.subtitle {
height: 32px;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
.count-pages {
font-size: $fs12;
color: $color-gray-30;
}
}
.current-tag {
font-size: $fs12;
color: $color-gray-30;
}
label {
color: $color-black;
}
}
.access-mode,
.inspect-mode {
display: grid;
grid-template-columns: auto 1fr;
.items {
display: flex;
justify-content: flex-end;
align-items: center;
}
}
.view-mode,
.access-mode,
.inspect-mode {
margin: 8px 0;
.subtitle {
display: flex;
justify-content: flex-start;
align-items: center;
color: $color-black;
font-size: $fs16;
.icon {
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
svg {
height: 16px;
width: 16px;
}
}
}
.items {
.input-select {
background-image: url("/images/icons/arrow-down.svg");
margin: 0;
padding-right: 28px;
border: 1px solid $color-gray-10;
max-width: 227px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
> .input-radio {
display: flex;
user-select: none;
margin-top: 0;
margin-bottom: 0;
label {
display: flex;
align-items: center;
color: $color-black;
max-width: 115px;
&::before {
height: 16px;
width: 16px;
}
.hint {
margin-left: 5px;
color: $color-gray-30;
}
}
&.disabled {
label {
color: $color-gray-30;
}
}
}
}
}
.pages-selection {
border-top: 1px solid $color-gray-10;
border-bottom: 1px solid $color-gray-10;
padding-left: 20px;
max-height: 200px;
overflow-y: scroll;
user-select: none;
label {
color: $color-black;
}
}
}
}
}
}

View File

@ -1,62 +0,0 @@
// 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
.align-options {
display: flex;
border-bottom: solid 1px $color-gray-60;
height: 40px;
.align-group {
padding: 0 $size-1;
display: flex;
justify-content: flex-start;
width: 50%;
&:not(:last-child) {
border-right: solid 1px $color-gray-60;
}
}
.align-button {
align-items: center;
cursor: pointer;
display: flex;
height: 30px;
justify-content: center;
margin: 5px 0;
padding: $size-2 $size-1;
width: 25%;
svg {
height: 16px;
width: 16px;
fill: $color-gray-20;
}
&:hover {
background-color: $color-primary;
svg {
fill: $color-gray-50;
}
}
&.disabled {
background-color: transparent;
cursor: default;
svg {
fill: $color-gray-40;
}
}
&.selected svg {
fill: $color-primary;
}
&.selected:hover svg {
fill: $color-white;
}
}
}

View File

@ -1,538 +0,0 @@
// 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
.assets-bar {
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
.assets-bar-title {
color: $color-gray-10;
font-size: $fs14;
margin: $size-2 $size-2 0 $size-2;
display: flex;
align-items: center;
cursor: pointer;
& .libraries-button {
margin-left: auto;
display: flex;
align-items: center;
svg {
fill: $color-gray-30;
height: 16px;
width: 16px;
padding-right: 6px;
}
}
& .libraries-button:hover {
color: $color-primary;
& svg {
fill: $color-primary;
}
}
}
.search-block {
border: 1px solid $color-gray-30;
margin: $size-2 $size-2 0 $size-2;
padding: $size-1 $size-2;
display: flex;
align-items: center;
&:hover {
border-color: $color-gray-20;
}
&:focus-within {
border-color: $color-primary;
}
& .search-input {
background-color: $color-gray-50;
border: none;
color: $color-gray-10;
font-size: $fs12;
margin: 0;
padding: 0;
flex-grow: 1;
&:focus {
color: lighten($color-gray-10, 8%);
outline: none;
}
}
& .search-icon {
display: flex;
align-items: center;
svg {
fill: $color-gray-30;
height: 16px;
width: 16px;
}
&.close {
transform: rotate(45deg);
cursor: pointer;
}
}
}
.input-select {
background-color: $color-gray-50;
color: $color-gray-10;
border: 1px solid transparent;
border-bottom-color: $color-gray-40;
padding: $size-1;
margin: $size-2 $size-2 $size-4 $size-2;
&:focus {
color: lighten($color-gray-10, 8%);
}
&:active {
border-color: $color-primary;
}
option {
background: $color-white;
color: $color-gray-60;
font-size: $fs12;
}
}
.collapse-library {
margin-right: $size-2;
flex-shrink: inherit; // Inheriting shrink behaviour
&.open svg {
transform: rotate(90deg);
}
}
.library-bar {
cursor: pointer;
}
.listing-options {
background-color: $color-gray-60;
display: flex;
align-items: center;
padding: $size-4 $size-2 0 $size-2;
.selected-count {
color: $color-primary;
font-size: $fs12;
}
.listing-option-btn {
cursor: pointer;
margin-left: $size-2;
&.first {
margin-left: auto;
}
svg {
fill: $color-gray-20;
height: 16px;
width: 16px;
}
}
}
.asset-section {
background-color: $color-gray-60;
padding: $size-2;
font-size: $fs12;
color: $color-gray-20;
/* TODO: see if this is useful, or is better to leave only
one scroll bar in the whole sidebar
(also see .asset-list) */
// max-height: 30rem;
// overflow-y: scroll;
// First child is the listing options buttons
&:not(:nth-child(2)) {
border-top: 1px solid $color-gray-50;
}
.asset-title {
display: flex;
cursor: pointer;
font-size: $fs12;
text-transform: uppercase;
& .num-assets {
color: $color-gray-30;
}
& svg {
height: 8px;
width: 8px;
fill: $color-gray-30;
margin-right: 4px;
transform: rotate(90deg);
}
&.closed svg {
transform: rotate(0deg);
transition: transform 0.3s;
}
}
.group-title {
display: flex;
cursor: pointer;
margin-top: $size-2;
margin-bottom: $size-1;
color: $color-white;
& svg {
height: 8px;
width: 8px;
fill: $color-white;
margin-right: 4px;
transform: rotate(90deg);
}
&.closed svg {
transform: rotate(0deg);
transition: transform 0.3s;
}
& .dim {
color: $color-gray-40;
}
}
.assets-button {
margin-left: auto;
cursor: pointer;
& svg {
width: 0.7rem;
height: 0.7rem;
fill: #f0f0f0;
}
&:hover svg {
fill: $color-primary;
}
}
.asset-title + .asset-grid {
margin-top: $size-2;
}
.asset-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 6vh;
column-gap: 0.5rem;
row-gap: 0.5rem;
&.big {
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 10vh;
.three-row & {
grid-template-columns: repeat(3, 1fr);
}
.four-row & {
grid-template-columns: repeat(4, 1fr);
}
.grid-cell {
padding: $size-1;
& svg {
height: 10vh;
}
}
}
}
.grid-cell {
background-color: $color-canvas;
border-radius: $br4;
border: 2px solid transparent;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
padding: $size-2;
position: relative;
cursor: pointer;
& img {
max-height: 100%;
max-width: 100%;
height: auto;
width: auto;
pointer-events: none;
}
}
.cell-name {
background-color: $color-gray-60;
font-size: $fs9;
display: none;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
padding: 3px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.editing {
display: block;
}
.editable-label-input {
border: 1px solid $color-gray-20;
border-radius: $br3;
font-size: $fs12;
padding: 2px;
margin: 0;
height: unset;
width: 100%;
}
.editable-label-close {
display: none;
}
}
.grid-cell:hover {
border: 2px solid $color-primary;
& .cell-name {
display: block;
}
}
.grid-cell.selected {
border: 2px solid $color-primary;
}
.grid-placeholder {
border: 2px solid $color-gray-20;
border-radius: $br4;
}
.drop-space {
height: 10px;
}
.typography-container {
position: relative;
&:last-child {
padding-bottom: 0.5em;
}
}
.drag-counter {
position: absolute;
top: 5px;
left: 4px;
width: 16px;
height: 16px;
background-color: $color-primary;
border-radius: 50%;
color: $color-black;
font-size: $fs12;
display: flex;
justify-content: center;
align-items: center;
}
.asset-title + .asset-enum {
margin-top: $size-2;
}
.asset-enum {
.enum-item {
position: relative;
display: flex;
align-items: center;
margin-bottom: $size-2;
cursor: pointer;
& > svg,
& > img {
background-color: $color-canvas;
border-radius: $br4;
border: 2px solid transparent;
height: 24px;
width: 24px;
margin-right: $size-2;
}
.item-name {
width: calc(100% - 24px - #{$size-2});
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
&.editing {
display: flex;
align-items: center;
.editable-label-input {
height: 24px;
}
.editable-label-close {
display: none;
}
}
}
}
.enum-item:hover,
.enum-item.selected {
color: $color-primary;
}
.grid-placeholder {
margin-bottom: 5px;
}
}
/* TODO: see if this is useful, or is better to leave only
one scroll bar in the whole sidebar
(also see .asset-section) */
// .asset-list {
// max-height: 30rem;
// overflow-y: scroll;
// }
.asset-list-item {
display: flex;
align-items: center;
border: 1px solid transparent;
border-radius: $br3;
margin-top: $size-1;
padding: 2px;
font-size: $fs12;
color: $color-white;
cursor: pointer;
position: relative;
.name-block {
color: $color-gray-20;
width: calc(100% - 24px - #{$size-2});
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
& span {
margin-left: $size-1;
color: $color-gray-30;
text-transform: uppercase;
}
&.selected {
border: 1px solid $color-primary;
}
}
.context-menu {
position: fixed;
top: 10px;
left: 10px;
}
.advanced-options {
border-color: $color-black;
background-color: $color-gray-60;
.input-text,
.input-select,
.adv-typography-name {
background-color: $color-gray-60;
}
}
.dragging {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: color.adjust($color-primary, $alpha: -0.5);
}
}
}
.modal-create-color {
position: relative;
background-color: $color-white;
padding: 4rem;
display: flex;
flex-direction: column;
align-items: center;
& .sketch-picker,
.chrome-picker {
box-shadow: none !important;
border: 1px solid $color-gray-10 !important;
border-radius: $br0 !important;
& input {
background-color: $color-white;
}
}
& .close {
position: absolute;
right: 1rem;
transform: rotate(45deg);
top: 1rem;
svg {
fill: $color-black;
height: 20px;
width: 20px;
&:hover {
fill: $color-danger;
}
}
}
& .btn-primary {
width: 10rem;
padding: 0.5rem;
margin-top: 1rem;
}
}
.modal-create-color-title {
color: $color-black;
font-size: $fs24;
font-weight: $fw400;
}
.libraries-wrapper {
overflow: auto;
display: flex;
flex-direction: column;
flex: 1;
}

View File

@ -1,139 +0,0 @@
// 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
.history-debug-overlay {
background: $color-gray-50;
bottom: 0;
max-height: 500px;
overflow-y: auto;
position: absolute;
width: 500px;
z-index: 1000;
}
.history-toolbox {
display: flex;
flex-direction: column;
}
.history-toolbox-title {
color: $color-gray-10;
font-size: $fs14;
padding: 0.5rem;
}
.history-entry-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
.history-entry-empty-icon {
margin-bottom: 1rem;
svg {
width: 32px;
height: 32px;
fill: $color-gray-40;
}
}
.history-entry-empty-msg {
color: $color-gray-30;
font-size: $fs12;
}
}
.history-entries {
font-size: $fs12;
color: $color-gray-20;
fill: $color-gray-20;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
.history-entry {
border: 1px solid $color-gray-60;
border-radius: $br4;
margin: 0.5rem;
display: flex;
flex-direction: column;
padding: 0.5rem;
cursor: pointer;
transition: border 0.2s;
&.disabled {
opacity: 0.5;
}
&.current {
background-color: $color-gray-60;
}
&.hover {
border-color: $color-primary;
}
}
.history-entry-summary {
display: flex;
flex-direction: row;
align-items: center;
* {
display: flex;
}
}
.history-entry-summary-icon {
svg {
width: 16px;
height: 16px;
}
}
.history-entry-summary-text {
flex: 1;
margin: 0 0.5rem;
margin-top: 2px;
}
.history-entry-summary-button {
opacity: 0;
transition: transform 0.2s;
svg {
width: 12px;
height: 12px;
}
.show-detail &,
.hover & {
opacity: 1;
}
.show-detail & {
transform: rotate(90deg);
}
}
.history-entry-detail {
display: none;
.show-detail & {
display: block;
padding: 1rem 0 0.5rem 0;
}
.history-entry-details-list {
margin: 0;
li {
margin-bottom: 0.5rem;
}
}
}

View File

@ -1,215 +0,0 @@
// 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
.interactions-help {
font-size: $fs12;
padding: 7px $size-4;
margin: 0 -7px;
text-align: center;
&.separator {
padding-bottom: $size-4;
border-bottom: 1px solid $color-black;
}
}
.interactions-help-icon {
height: 32px;
width: 32px;
margin: $size-4 auto;
svg {
fill: $color-gray-40;
height: 32px;
width: 32px;
}
}
.interactions-summary {
cursor: pointer;
flex-basis: 0;
flex-grow: 1;
.trigger-name {
font-size: $fs12;
color: $color-white;
}
.action-summary {
font-size: $fs12;
color: $color-gray-20;
}
}
.interactions-element {
display: flex;
align-items: center;
margin: 0 -7px;
padding: 0 7px;
.element-label {
color: $color-gray-20;
font-size: $fs12;
width: 64px;
}
&.separator {
border-top: 1px solid $color-black;
margin-top: $size-1;
}
}
.interactions-pos-buttons {
margin-top: $size-2;
padding-top: $size-2;
padding-bottom: $size-2;
justify-content: space-between;
.element-set-actions-button {
min-width: 18px;
min-height: 18px;
}
svg {
height: 18px;
width: 18px;
}
}
.interactions-way-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
& .input-radio {
margin-bottom: 0;
& label {
color: $color-gray-20;
&:before {
background-color: unset;
}
}
& input[type="radio"]:checked {
& + label {
&:before {
background-color: $color-primary;
box-shadow: inset 0 0 0 5px $color-gray-50;
}
}
}
}
}
.interactions-direction-buttons {
margin-top: $size-2;
padding-top: $size-2;
padding-bottom: $size-2;
justify-content: space-around;
.element-set-actions-button {
min-width: 40px;
min-height: 13px;
}
svg {
height: 13px;
width: 13px;
}
}
.interactions-easing-icon {
display: flex;
justify-content: center;
align-items: center;
min-width: 30px;
min-height: 30px;
& svg {
width: 12px;
height: 12px;
stroke: $color-gray-20;
}
}
.flow-element {
display: flex;
align-items: center;
padding: $size-1;
.element-label {
font-size: $fs11;
}
.flow-name {
cursor: pointer;
}
& input.element-name {
background: transparent;
border-color: $color-primary;
color: $color-white;
font-size: $fs11;
}
}
.flow-button {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
margin-right: $size-2;
& svg {
height: 12px;
width: 12px;
fill: $color-gray-20;
}
&:hover svg {
fill: $color-primary;
}
}
.flow-badge {
cursor: pointer;
display: flex;
& .content {
align-items: center;
background-color: $color-gray-50;
border-radius: $br4;
display: flex;
height: 24px;
& svg {
height: 12px;
margin: 0 $size-2;
width: 12px;
fill: $color-gray-20;
}
& span {
color: $color-gray-20;
font-size: $fs12;
margin-right: $size-4;
}
}
&.selected .content,
&:hover .content {
background-color: $color-primary;
& svg {
fill: $color-gray-60;
}
& span {
color: $color-gray-60;
}
}
}

View File

@ -4,6 +4,7 @@
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Penpot - Design Freedom for Teams</title>
<meta name="description" content="The open-source solution for design and prototyping.">
<meta property="og:locale" content="en_US">
<meta property="og:title" content="Penpot | Design Freedom for Teams">
@ -17,6 +18,9 @@
<meta name="twitter:creator" content="@penpotapp">
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
<link id="theme" href="css/main.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
{{#isDebug}}
<link href="css/debug.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
{{/isDebug}}
<link rel="icon" href="images/favicon.png" />

View File

@ -855,7 +855,8 @@
(fn [parent objects]
(cond-> parent
(ctl/grid-layout? parent)
(ctl/assign-cells objects))))
(ctl/assign-cells objects)))
{:with-objects? true})
(pcb/reorder-grid-children parents)
@ -1490,6 +1491,30 @@
(rx/of (show-context-menu
(-> params (assoc :kind :page :selected (:id page))))))))
(defn show-track-context-menu
[{:keys [grid-id type index] :as params}]
(ptk/reify ::show-track-context-menu
ptk/WatchEvent
(watch [_ _ _]
(rx/of (show-context-menu
(-> params (assoc :kind :grid-track
:grid-id grid-id
:type type
:index index)))))))
(defn show-grid-cell-context-menu
[{:keys [grid-id] :as params}]
(ptk/reify ::show-grid-cell-context-menu
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
grid (get objects grid-id)
cells (->> (get-in state [:workspace-grid-edition grid-id :selected])
(map #(get-in grid [:layout-grid-cells %])))]
(rx/of (show-context-menu
(-> params (assoc :kind :grid-cells
:grid grid
:cells cells))))))))
(def hide-context-menu
(ptk/reify ::hide-context-menu
ptk/UpdateEvent
@ -1497,6 +1522,8 @@
(assoc-in state [:workspace-local :context-menu] nil))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Clipboard
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -15,6 +15,7 @@
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
@ -51,8 +52,8 @@
(defn update-shapes
([ids update-fn] (update-shapes ids update-fn nil))
([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-remote? ignore-touched undo-group]
:or {reg-objects? false save-undo? true stack-undo? false ignore-remote? false ignore-touched false}}]
([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-remote? ignore-touched undo-group with-objects?]
:or {reg-objects? false save-undo? true stack-undo? false ignore-remote? false ignore-touched false with-objects? false}}]
(dm/assert!
"expected a valid coll of uuid's"
@ -70,14 +71,15 @@
update-layout-ids
(->> ids
(map (d/getf objects))
(filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs})))
(filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs :with-objects? with-objects?})))
(map :id))
changes (reduce
(fn [changes id]
(let [opts {:attrs attrs
:ignore-geometry? (get ignore-tree id)
:ignore-touched ignore-touched}]
:ignore-touched ignore-touched
:with-objects? with-objects?}]
(pcb/update-shapes changes [id] update-fn (d/without-nils opts))))
(-> (pcb/empty-changes it page-id)
(pcb/set-save-undo? save-undo?)
@ -86,6 +88,8 @@
(cond-> undo-group
(pcb/set-undo-group undo-group)))
ids)
grid-ids (->> ids (filter (partial ctl/grid-layout? objects)))
changes (pcb/update-shapes changes grid-ids ctl/assign-cell-positions {:with-objects? true})
changes (pcb/reorder-grid-children changes ids)
changes (add-undo-group changes state)]
(rx/concat

View File

@ -13,7 +13,7 @@
[app.common.schema :as sm]
[app.common.types.shape-tree :as ctst]
[app.main.data.comments :as dcm]
[app.main.data.workspace.changes :as dwc]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwco]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.state-helpers :as wsh]
@ -148,7 +148,7 @@
(pcb/update-page-option :comment-threads-position assoc thread-id (select-keys thread [:position :frame-id])))]
(rx/merge
(rx/of (dwc/commit-changes changes))
(rx/of (dch/commit-changes changes))
(->> (rp/cmd! :update-comment-thread-position thread)
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
(rx/ignore))))))))

View File

@ -6,6 +6,7 @@
(ns app.main.data.workspace.grid-layout.editor
(:require
[app.common.data.macros :as dm]
[app.common.geom.rect :as grc]
[app.common.types.shape.layout :as ctl]
[app.main.data.workspace.state-helpers :as wsh]
@ -25,14 +26,34 @@
(conj hover-set cell-id)
(disj hover-set cell-id))))))))
(defn select-grid-cell
[grid-id cell-id add?]
(ptk/reify ::select-grid-cell
(defn add-to-selection
([grid-id cell-id]
(add-to-selection grid-id cell-id false))
([grid-id cell-id shift?]
(ptk/reify ::add-to-selection
ptk/UpdateEvent
(update [_ state]
(if shift?
(let [objects (wsh/lookup-page-objects state)
grid (get objects grid-id)
selected (or (dm/get-in state [:workspace-grid-edition grid-id :selected]) #{})
selected (into selected [cell-id])
cells (->> selected (map #(dm/get-in grid [:layout-grid-cells %])))
{:keys [first-row last-row first-column last-column]} (ctl/cells-coordinates cells)
new-selected
(into #{}
(map :id)
(ctl/cells-in-area grid first-row last-row first-column last-column))]
(assoc-in state [:workspace-grid-edition grid-id :selected] new-selected))
(update-in state [:workspace-grid-edition grid-id :selected] (fnil conj #{}) cell-id))))))
(defn set-selection
[grid-id cell-id]
(ptk/reify ::set-selection
ptk/UpdateEvent
(update [_ state]
(if add?
(update-in state [:workspace-grid-edition grid-id :selected] (fnil conj #{}) cell-id)
(assoc-in state [:workspace-grid-edition grid-id :selected] #{cell-id})))))
(assoc-in state [:workspace-grid-edition grid-id :selected] #{cell-id}))))
(defn remove-selection
[grid-id cell-id]

View File

@ -125,7 +125,7 @@
[parent-id]
(fn [parent]
(assoc-in parent [:layout-grid-cells (:id target-cell) :shapes] [(:id group)]))))
(pcb/update-shapes grid-parents ctl/assign-cells)
(pcb/update-shapes grid-parents ctl/assign-cells {:with-objects? true})
(pcb/remove-objects ids-to-delete))]
[group changes]))
@ -216,7 +216,7 @@
(cond-> changes
(ctl/grid-layout? objects (:parent-id shape))
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells))))
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells {:with-objects? true}))))
selected (->> (wsh/lookup-selected state)
(remove #(ctn/has-any-copy-parent? objects (get objects %)))

View File

@ -11,7 +11,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.types.page :as ctp]
[app.main.data.workspace.changes :as dwc]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -35,7 +35,7 @@
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/update-page-option :guides assoc (:id guide) guide))]
(rx/of (dwc/commit-changes changes))))))
(rx/of (dch/commit-changes changes))))))
(defn remove-guide [guide]
(dm/assert!
@ -56,7 +56,7 @@
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/update-page-option :guides dissoc (:id guide)))]
(rx/of (dwc/commit-changes changes))))))
(rx/of (dch/commit-changes changes))))))
(defn remove-guides
[ids]

Some files were not shown because too many files have changed in this diff Show More