mirror of
https://github.com/penpot/penpot.git
synced 2026-05-03 07:08:44 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
9aff12f3c6
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,6 +23,7 @@
|
||||
/*.jpg
|
||||
/*.md
|
||||
/*.png
|
||||
/*.svg
|
||||
/*.sql
|
||||
/*.txt
|
||||
/*.yml
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))))
|
||||
|
||||
@ -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))))))))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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}
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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;
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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;
|
||||
15
backend/src/app/migrations/sql/0112-mod-profile-table.sql
Normal file
15
backend/src/app/migrations/sql/0112-mod-profile-table.sql
Normal 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();
|
||||
|
||||
@ -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();
|
||||
10
backend/src/app/migrations/sql/0114-mod-team-table.sql
Normal file
10
backend/src/app/migrations/sql/0114-mod-team-table.sql
Normal 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;
|
||||
@ -0,0 +1,3 @@
|
||||
ALTER TABLE project
|
||||
DROP CONSTRAINT project_team_id_fkey,
|
||||
ADD FOREIGN KEY (team_id) REFERENCES team(id) DEFERRABLE;
|
||||
3
backend/src/app/migrations/sql/0116-mod-file-table.sql
Normal file
3
backend/src/app/migrations/sql/0116-mod-file-table.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE file
|
||||
DROP CONSTRAINT file_project_id_fkey,
|
||||
ADD FOREIGN KEY (project_id) REFERENCES project(id) DEFERRABLE;
|
||||
@ -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;
|
||||
@ -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"}
|
||||
|
||||
@ -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))))
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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))})))))
|
||||
|
||||
@ -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)))
|
||||
|
||||
|
||||
@ -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)}})))))
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)}})))))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))))
|
||||
|
||||
@ -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]}]
|
||||
|
||||
@ -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)))))))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
|
||||
125
backend/src/app/storage/gc_deleted.clj
Normal file
125
backend/src/app/storage/gc_deleted.clj
Normal 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}))))))
|
||||
|
||||
|
||||
208
backend/src/app/storage/gc_touched.clj
Normal file
208
backend/src/app/storage/gc_touched.clj
Normal 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!)))
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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
65
backend/src/app/svgo.clj
Normal 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))
|
||||
@ -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
|
||||
|
||||
@ -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)))
|
||||
|
||||
59
backend/src/app/tasks/orphan_teams_gc.clj
Normal file
59
backend/src/app/tasks/orphan_teams_gc.clj
Normal 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)))
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)))))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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"))))))
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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))))
|
||||
|
||||
))
|
||||
|
||||
@ -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"}]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))))
|
||||
))
|
||||
|
||||
@ -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))))))
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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]))
|
||||
|
||||
18
common/src/app/common/geom/line.cljc
Normal file
18
common/src/app/common/geom/line.cljc
Normal 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))
|
||||
@ -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)
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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)))))
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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/"));
|
||||
});
|
||||
|
||||
@ -17,9 +17,6 @@ body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
background-color: red; //debugger colors
|
||||
color: yellow; //debugger colors
|
||||
}
|
||||
|
||||
#app {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
15
frontend/resources/styles/debug.scss
Normal file
15
frontend/resources/styles/debug.scss
Normal file
@ -0,0 +1,15 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
// 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;
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" />
|
||||
|
||||
|
||||
@ -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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))))))))
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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 %)))
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user