0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-13 07:21:40 -05:00

🎉 Add features assignation for teams

This commit is contained in:
Andrey Antukh 2023-10-23 19:31:41 +02:00 committed by Andrés Moya
parent 7db8d7b7ab
commit 6f93b41920
84 changed files with 2390 additions and 1777 deletions

View file

@ -5,6 +5,7 @@
promesa.exec.csp/go-loop clojure.core/loop
rumext.v2/defc clojure.core/defn
rumext.v2/fnc clojure.core/fn
promesa.util/with-open clojure.core/with-open
app.common.data/export clojure.core/def
app.common.data.macros/get-in clojure.core/get-in
app.common.data.macros/with-open clojure.core/with-open
@ -62,4 +63,3 @@
:exclude-destructured-keys-in-fn-args false
}
}}

View file

@ -21,7 +21,7 @@
[app.common.transit :as t]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.config :as cf]
[app.main :as main]
[app.srepl.helpers :as srepl.helpers]
[app.srepl.main :as srepl]
@ -96,7 +96,9 @@
(try
(alter-var-root #'system (fn [sys]
(when sys (ig/halt! sys))
(-> (merge main/system-config main/worker-config)
(-> main/system-config
(cond-> (contains? cf/flags :backend-worker)
(merge main/worker-config))
(ig/prep)
(ig/init))))
:started

View file

@ -10,9 +10,10 @@ export PENPOT_FLAGS="\
enable-login-with-google \
enable-login-with-github \
enable-login-with-gitlab \
disable-backend-worker \
enable-backend-asserts \
enable-fdata-storage-pointer-map \
enable-fdata-storage-objets-map \
enable-feature-fdata-pointer-map \
enable-feature-fdata-objects-map \
enable-audit-log \
enable-transit-readable-response \
enable-demo-users \

View file

@ -231,60 +231,76 @@
`(jdbc/with-transaction ~@args)))
(defn open
[pool]
(jdbc/get-connection pool))
[system-or-pool]
(if (pool? system-or-pool)
(jdbc/get-connection system-or-pool)
(if (map? system-or-pool)
(open (::pool system-or-pool))
(ex/raise :type :internal
:code :unable-resolve-pool))))
(defn- resolve-connectable
(defn get-connection
[cfg-or-conn]
(if (connection? cfg-or-conn)
cfg-or-conn
(if (map? cfg-or-conn)
(get-connection (::conn cfg-or-conn))
(ex/raise :type :internal
:code :unable-resolve-connection
:hint "expected conn or system map"))))
(defn- get-connectable
[o]
(if (connection? o)
o
(if (pool? o)
o
(or (::conn o) (::pool o)))))
(cond
(connection? o) o
(pool? o) o
(map? o) (get-connectable (or (:conn o) (::pool o)))
:else (ex/raise :type :internal
:code :unable-resolve-connectable
:hint "expected conn, pool or system")))
(def ^:private default-opts
{:builder-fn sql/as-kebab-maps})
(defn exec!
([ds sv]
(-> (resolve-connectable ds)
(-> (get-connectable ds)
(jdbc/execute! sv default-opts)))
([ds sv opts]
(-> (resolve-connectable ds)
(-> (get-connectable ds)
(jdbc/execute! sv (merge default-opts opts)))))
(defn exec-one!
([ds sv]
(-> (resolve-connectable ds)
(-> (get-connectable ds)
(jdbc/execute-one! sv default-opts)))
([ds sv opts]
(-> (resolve-connectable ds)
(-> (get-connectable ds)
(jdbc/execute-one! sv
(-> (merge default-opts opts)
(assoc :return-keys (::return-keys? opts false)))))))
(defn insert!
[ds table params & {:as opts}]
(-> (resolve-connectable ds)
(-> (get-connectable ds)
(exec-one! (sql/insert table params opts)
(merge {::return-keys? true} opts))))
(defn insert-multi!
[ds table cols rows & {:as opts}]
(-> (resolve-connectable ds)
(-> (get-connectable ds)
(exec! (sql/insert-multi table cols rows opts)
(merge {::return-keys? true} opts))))
(defn update!
[ds table params where & {:as opts}]
(-> (resolve-connectable ds)
(-> (get-connectable ds)
(exec-one! (sql/update table params where opts)
(merge {::return-keys? true} opts))))
(defn delete!
[ds table params & {:as opts}]
(-> (resolve-connectable ds)
(-> (get-connectable ds)
(exec-one! (sql/delete table params opts)
(merge {::return-keys? true} opts))))
@ -318,7 +334,7 @@
(defn plan
[ds sql]
(-> (resolve-connectable ds)
(-> (get-connectable ds)
(jdbc/plan sql sql/default-opts)))
(defn get-by-id
@ -422,12 +438,16 @@
(release! conn sp)
result)
(catch Throwable cause
(rollback! sp)
(rollback! conn sp)
(throw cause))))
(::pool cfg)
(with-atomic [conn (::pool cfg)]
(f (assoc cfg ::conn conn)))
(let [result (f (assoc cfg ::conn conn))]
(when (::rollback cfg)
(l/dbg :hint "explicit rollback requested")
(rollback! conn))
result))
:else
(throw (IllegalArgumentException. "invalid arguments"))))

View file

@ -0,0 +1,677 @@
;; 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.features.components-v2
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.files.libraries-helpers :as cflh]
[app.common.files.migrations :as pmg]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.logging :as l]
[app.common.pages.changes :as cp]
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.svg :as csvg]
[app.common.svg.shapes-builder :as sbuilder]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.db :as db]
[app.media :as media]
[app.rpc.commands.files :as files]
[app.rpc.commands.media :as cmd.media]
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.util.blob :as blob]
[app.util.objects-map :as omap]
[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.semaphore :as ps]))
;; - What about use of svgo on converting graphics to components
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; END PROMESA HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:dynamic *system* nil)
(def ^:dynamic *stats* nil)
(def ^:dynamic *semaphore* nil)
(def ^:dynamic *skip-on-error* true)
(def grid-gap 50)
(defn- prepare-file-data
"Apply some specific migrations or fixes to things that are allowed in v1 but not in v2,
or that are the result of old bugs."
[file-data libraries]
(let [detached-ids (volatile! #{})
detach-shape
(fn [container shape]
; Detach a shape. If it's inside a component, add it to detached-ids, for further use.
(let [is-component? (let [root-shape (ctst/get-shape container (:id container))]
(and (some? root-shape) (nil? (:parent-id root-shape))))]
(when is-component?
(vswap! detached-ids conj (:id shape)))
(ctk/detach-shape shape)))
fix-orphan-shapes
(fn [file-data]
; Find shapes that are not listed in their parent's children list.
; Remove them, and also their children
(letfn [(fix-container [container]
(reduce fix-shape container (ctn/shapes-seq container)))
(fix-shape
[container shape]
(if-not (or (= (:id shape) uuid/zero)
(nil? (:parent-id shape)))
(let [parent (ctst/get-shape container (:parent-id shape))
exists? (d/index-of (:shapes parent) (:id shape))]
(if (nil? exists?)
(let [ids (cph/get-children-ids-with-self (:objects container) (:id shape))]
(update container :objects #(reduce dissoc % ids)))
container))
container))]
(-> file-data
(update :pages-index update-vals fix-container)
(update :components update-vals fix-container))))
remove-nested-roots
(fn [file-data]
; Remove :component-root in head shapes that are nested.
(letfn [(fix-container [container]
(update container :objects update-vals (partial fix-shape container)))
(fix-shape [container shape]
(let [parent (ctst/get-shape container (:parent-id shape))]
(if (and (ctk/instance-root? shape)
(ctn/in-any-component? (:objects container) parent))
(dissoc shape :component-root)
shape)))]
(-> file-data
(update :pages-index update-vals fix-container)
(update :components update-vals fix-container))))
add-not-nested-roots
(fn [file-data]
; Add :component-root in head shapes that are not nested.
(letfn [(fix-container [container]
(update container :objects update-vals (partial fix-shape container)))
(fix-shape [container shape]
(let [parent (ctst/get-shape container (:parent-id shape))]
(if (and (ctk/subinstance-head? shape)
(not (ctn/in-any-component? (:objects container) parent)))
(assoc shape :component-root true)
shape)))]
(-> file-data
(update :pages-index update-vals fix-container)
(update :components update-vals fix-container))))
fix-orphan-copies
(fn [file-data]
; Detach shapes that were inside a copy (have :shape-ref) but now they aren't.
(letfn [(fix-container [container]
(update container :objects update-vals (partial fix-shape container)))
(fix-shape [container shape]
(let [parent (ctst/get-shape container (:parent-id shape))]
(if (and (ctk/in-component-copy? shape)
(not (ctk/instance-head? shape))
(not (ctk/in-component-copy? parent)))
(detach-shape container shape)
shape)))]
(-> file-data
(update :pages-index update-vals fix-container)
(update :components update-vals fix-container))))
remap-refs
(fn [file-data]
; Remap shape-refs so that they point to the near main.
; At the same time, if there are any dangling ref, detach the shape and its children.
(letfn [(fix-container [container]
(reduce fix-shape container (ctn/shapes-seq container)))
(fix-shape [container shape]
(if (ctk/in-component-copy? shape)
; First look for the direct shape.
(let [root (ctn/get-component-shape (:objects container) shape)
libraries (assoc-in libraries [(:id file-data) :data] file-data)
library (get libraries (:component-file root))
component (ctkl/get-component (:data library) (:component-id root) true)
direct-shape (ctf/get-component-shape (:data library) component (:shape-ref shape))]
(if (some? direct-shape)
; If it exists, there is nothing else to do.
container
; If not found, find the near shape.
(let [near-shape (d/seek #(= (:shape-ref %) (:shape-ref shape))
(ctf/get-component-shapes (:data library) component))]
(if (some? near-shape)
; If found, update the ref to point to the near shape.
(ctn/update-shape container (:id shape) #(assoc % :shape-ref (:id near-shape)))
; If not found, it may be a fostered component. Try to locate a direct shape
; in the head component.
(let [head (ctn/get-head-shape (:objects container) shape)
library-2 (get libraries (:component-file head))
component-2 (ctkl/get-component (:data library-2) (:component-id head) true)
direct-shape-2 (ctf/get-component-shape (:data library-2) component-2 (:shape-ref shape))]
(if (some? direct-shape-2)
; If it exists, there is nothing else to do.
container
; If not found, detach shape and all children (stopping if a nested instance is reached)
(let [children (ctn/get-children-in-instance (:objects container) (:id shape))]
(reduce #(ctn/update-shape %1 (:id %2) (partial detach-shape %1))
container
children))))))))
container))]
(-> file-data
(update :pages-index update-vals fix-container)
(update :components update-vals fix-container))))
fix-copies-of-detached
(fn [file-data]
; Find any copy that is referencing a detached shape inside a component, and
; undo the nested copy, converting it into a direct copy.
(letfn [(fix-container [container]
(update container :objects update-vals fix-shape))
(fix-shape [shape]
(cond-> shape
(@detached-ids (:shape-ref shape))
(dissoc shape
:component-id
:component-file
:component-root)))]
(-> file-data
(update :pages-index update-vals fix-container)
(update :components update-vals fix-container))))]
(-> file-data
(fix-orphan-shapes)
(remove-nested-roots)
(add-not-nested-roots)
(fix-orphan-copies)
(remap-refs)
(fix-copies-of-detached))))
(defn- migrate-components
"If there is any component in the file library, add a new 'Library
backup', generate main instances for all components there and remove
shapes from library components. Mark the file with
the :components-v2 option."
[file-data libraries]
(let [components (ctkl/components-seq file-data)]
(if (empty? components)
(assoc-in file-data [:options :components-v2] true)
(let [[file-data page-id start-pos]
(ctf/get-or-add-library-page file-data grid-gap)
migrate-component-shape
(fn [shape delta component-file component-id]
(cond-> shape
(nil? (:parent-id shape))
(assoc :parent-id uuid/zero
:main-instance true
:component-root true
:component-file component-file
:component-id component-id
:type :frame ; Old groups must be converted
:fills [] ; to frames and conform to spec
:hide-in-viewer true
:rx 0
:ry 0)
(nil? (:frame-id shape))
(assoc :frame-id uuid/zero)
:always
(gsh/move delta)))
add-main-instance
(fn [file-data component position]
(let [shapes (cph/get-children-with-self (:objects component)
(:id component))
root-shape (first shapes)
orig-pos (gpt/point (:x root-shape) (:y root-shape))
delta (gpt/subtract position orig-pos)
xf-shape (map #(migrate-component-shape %
delta
(:id file-data)
(:id component)))
new-shapes
(into [] xf-shape shapes)
add-shapes
(fn [page]
(reduce (fn [page shape]
(ctst/add-shape (:id shape)
shape
page
(:frame-id shape)
(:parent-id shape)
nil ; <- As shapes are ordered, we can safely add each
true)) ; one at the end of the parent's children list.
page
new-shapes))
update-component
(fn [component]
(-> component
(assoc :main-instance-id (:id root-shape)
:main-instance-page page-id)
(dissoc :objects)))]
(-> file-data
(ctpl/update-page page-id add-shapes)
(ctkl/update-component (:id component) update-component))))
add-instance-grid
(fn [fdata]
(let [components (->> fdata
(ctkl/components-seq)
(sort-by :name)
(reverse))
positions (ctst/generate-shape-grid
(map (partial ctf/get-component-root fdata) components)
start-pos
grid-gap)]
(reduce (fn [result [component position]]
(add-main-instance result component position))
fdata
(d/zip components positions))))]
(when (some? *stats*)
(let [total (count components)]
(swap! *stats* (fn [stats]
(-> stats
(update :processed/components (fnil + 0) total)
(assoc :current/components total))))))
(-> file-data
(prepare-file-data libraries)
(add-instance-grid))))))
(defn- create-shapes-for-bitmap
"Convert a media object that contains a bitmap image into shapes,
one shape of type :image and one group that contains it."
[{:keys [name width height id mtype]} position]
(let [group-shape (cts/setup-shape
{:type :frame
:x (:x position)
:y (:y position)
:width width
:height height
:name name
:frame-id uuid/zero
:parent-id uuid/zero})
img-shape (cts/setup-shape
{:type :image
:x (:x position)
:y (:y position)
:width width
:height height
:metadata {:id id
:width width
:height height
:mtype mtype}
:name name
:frame-id uuid/zero
:parent-id (:id group-shape)})]
[group-shape [img-shape]]))
(defn- parse-datauri
[data]
(let [[mtype b64-data] (str/split data ";base64," 2)
mtype (subs mtype (inc (str/index-of mtype ":")))
data (-> b64-data bc/str->bytes bc/b64->bytes)]
[mtype data]))
(defn- extract-name
[href]
(let [query-idx (d/nilv (str/last-index-of href "?") 0)
href (if (> query-idx 0) (subs href 0 query-idx) href)
filename (->> (str/split href "/") (last))
ext-idx (str/last-index-of filename ".")]
(if (> ext-idx 0) (subs filename 0 ext-idx) filename)))
(defn- collect-and-persist-images
[svg-data file-id]
(letfn [(process-image [{:keys [href] :as item}]
(try
(let [item (if (str/starts-with? href "data:")
(let [[mtype data] (parse-datauri href)
size (alength data)
path (tmp/tempfile :prefix "penpot.media.download.")
written (io/write-to-file! data path :size size)]
(when (not= written size)
(ex/raise :type :internal
:code :mismatch-write-size
:hint "unexpected state: unable to write to file"))
(-> item
(assoc :size size)
(assoc :path path)
(assoc :filename "tempfile")
(assoc :mtype mtype)))
(let [result (cmd.media/download-image *system* href)]
(-> (merge item result)
(assoc :name (extract-name href)))))]
;; The media processing adds the data to the
;; input map and returns it.
(media/run {:cmd :info :input item}))
(catch Throwable cause
(l/warn :hint "unexpected exception on processing internal image shape (skiping)"
:cause cause)
(when-not *skip-on-error*
(throw cause)))))
(persist-image [acc {:keys [path size width height mtype href] :as item}]
(let [storage (::sto/storage *system*)
conn (::db/conn *system*)
hash (sto/calculate-hash path)
content (-> (sto/content path size)
(sto/wrap-with-hash hash))
params {::sto/content content
::sto/deduplicate? true
::sto/touched-at (:ts item)
:content-type mtype
:bucket "file-media-object"}
image (sto/put-object! storage params)
fmo-id (uuid/next)]
(db/exec-one! conn
[cmd.media/sql:create-file-media-object
fmo-id
file-id true (:name item "image")
(:id image)
nil
width
height
mtype])
(assoc acc href {:id fmo-id
:mtype mtype
:width width
:height height})))
]
(let [images (->> (csvg/collect-images svg-data)
(transduce (keep process-image)
(completing persist-image) {}))]
(assoc svg-data :image-data images))))
(defn- get-svg-content
[id]
(let [storage (::sto/storage *system*)
conn (::db/conn *system*)
fmobject (db/get conn :file-media-object {:id id})
sobject (sto/get-object storage (:media-id fmobject))]
(with-open [stream (sto/get-object-data storage sobject)]
(slurp stream))))
(defn- create-shapes-for-svg
[{:keys [id] :as mobj} file-id objects position]
(let [svg-text (get-svg-content id)
svg-data (-> (csvg/parse svg-text)
(assoc :name (:name mobj))
(collect-and-persist-images file-id))]
(sbuilder/create-svg-shapes svg-data position objects uuid/zero nil #{} false)))
(defn- process-media-object
[fdata page-id mobj position]
(let [page (ctpl/get-page fdata page-id)
file-id (get fdata :id)
[shape children]
(if (= (:mtype mobj) "image/svg+xml")
(create-shapes-for-svg mobj file-id (:objects page) position)
(create-shapes-for-bitmap mobj position))
changes
(-> (pcb/empty-changes nil)
(pcb/set-save-undo? false)
(pcb/with-page page)
(pcb/with-objects (:objects page))
(pcb/with-library-data fdata)
(pcb/delete-media (:id mobj))
(pcb/add-objects (cons shape children)))
;; NOTE: this is a workaround for `generate-add-component`, it
;; is needed because that function always starts from empty
;; changes; so in this case we need manually add all shapes to
;; the page and then use that page for the
;; `generate-add-component` function
page
(reduce (fn [page shape]
(ctst/add-shape (:id shape)
shape
page
uuid/zero
uuid/zero
nil
true))
page
(cons shape children))
[_ _ changes2]
(cflh/generate-add-component nil
[shape]
(:objects page)
(:id page)
file-id
true
nil
cfsh/prepare-create-artboard-from-selection)
changes (pcb/concat-changes changes changes2)]
(cp/process-changes fdata (:redo-changes changes) false)))
(defn- migrate-graphics
[fdata]
(let [[fdata page-id position]
(ctf/get-or-add-library-page fdata grid-gap)
media (->> (vals (:media fdata))
(map (fn [{:keys [width height] :as media}]
(let [points (-> (grc/make-rect 0 0 width height)
(grc/rect->points))]
(assoc media :points points)))))
;; FIXME: think about what to do with existing media entries ??
grid (ctst/generate-shape-grid media position grid-gap)]
(when (some? *stats*)
(let [total (count media)]
(swap! *stats* (fn [stats]
(-> stats
(update :processed/graphics (fnil + 0) total)
(assoc :current/graphics total))))))
(->> (d/zip media grid)
(reduce (fn [fdata [mobj position]]
(try
(process-media-object fdata page-id mobj position)
(catch Throwable cause
(l/warn :hint "unable to process file media object (skiping)"
:file-id (str (:id fdata))
:id (str (:id mobj))
:cause cause)
(if-not *skip-on-error*
(throw cause)
fdata))))
fdata))))
(defn- migrate-file-data
[fdata libs]
(let [migrated? (dm/get-in fdata [:options :components-v2])]
(if migrated?
fdata
(let [fdata (migrate-components fdata libs)
fdata (migrate-graphics fdata)]
(update fdata :options assoc :components-v2 true)))))
(defn- process-file
[{:keys [id] :as file}]
(let [conn (::db/conn *system*)]
(binding [pmap/*tracked* (atom {})
pmap/*load-fn* (partial files/load-pointer conn id)
cfeat/*wrap-with-pointer-map-fn*
(if (contains? (:features file) "fdata/pointer-map") pmap/wrap identity)
cfeat/*wrap-with-objects-map-fn*
(if (contains? (:features file) "fdata/objectd-map") omap/wrap identity)]
(let [libs (sequence
(map (fn [{:keys [id] :as lib}]
(binding [pmap/*load-fn* (partial files/load-pointer conn id)]
(-> (db/get conn :file {:id id})
(files/decode-row)
(files/process-pointers deref) ; ensure all pointers resolved
(pmg/migrate-file)))))
(files/get-file-libraries conn id))
libs (-> (d/index-by :id libs)
(assoc (:id file) file))
file (-> file
(update :data blob/decode)
(update :data assoc :id id)
(update :data migrate-file-data libs)
(update :features conj "components/v2"))]
(when (contains? (:features file) "fdata/pointer-map")
(files/persist-pointers! conn id))
(db/update! conn :file
{:data (blob/encode (:data file))
:features (db/create-array conn "text" (:features file))
:revn (:revn file)}
{:id (:id file)})
(dissoc file :data)))))
(defn migrate-file!
[system file-id]
(let [tpoint (dt/tpoint)
file-id (if (string? file-id)
(parse-uuid file-id)
file-id)]
(try
(l/dbg :hint "migrate:file:start" :file-id (str file-id))
(let [system (update system ::sto/storage media/configure-assets-storage)]
(db/tx-run! system
(fn [{:keys [::db/conn] :as system}]
(binding [*system* system]
(-> (db/get conn :file {:id file-id})
(update :features db/decode-pgarray #{})
(process-file))))))
(finally
(let [elapsed (tpoint)
stats (some-> *stats* deref)]
(l/dbg :hint "migrate:file:end"
:file-id (str file-id)
:components (:current/components stats 0)
:graphics (:current/graphics stats 0)
:elapsed (dt/format-duration elapsed))
(when (some? *stats*)
(swap! *stats* (fn [stats]
(let [elapsed (inst-ms elapsed)
completed (inc (get stats :processed/files 0))
total (+ (get stats :elapsed/total-by-file 0) elapsed)
avg (/ (double elapsed) completed)]
(-> stats
(update :elapsed/max-by-file (fnil max 0) elapsed)
(assoc :elapsed/avg-by-file avg)
(assoc :elapsed/total-by-file total)
(assoc :processed/files completed)))))))))))
(defn migrate-team!
[system team-id]
(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))
(try
(db/tx-run! system
(fn [{:keys [::db/conn] :as system}]
;; Lock the team
(db/exec-one! conn ["SET idle_in_transaction_session_timeout = 0"])
(db/exec-one! conn ["UPDATE team SET features = array_append(features, 'ephimeral/v2-migration') WHERE id = ?" team-id])
(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")
rows (->> (db/exec! conn [sql team-id])
(map :id))]
(run! (partial migrate-file! system) rows)
(some-> *stats* (swap! assoc :current/files (count rows)))
(let [features (-> features
(conj "components/v2")
(conj "layout/grid")
(conj "styles/v2"))]
(db/update! conn :team
{:features (db/create-array conn "text" features)}
{:id team-id})))))))
(finally
(some-> *semaphore* ps/release!)
(let [elapsed (tpoint)
stats (some-> *stats* deref)]
(l/dbg :hint "migrate:team:end"
:team-id (dm/str team-id)
:files (:current/files stats 0)
:elapsed (dt/format-duration elapsed))
(when (some? *stats*)
(swap! *stats* (fn [stats]
(let [elapsed (inst-ms elapsed)
completed (inc (get stats :processed/teams 0))
total (+ (get stats :elapsed/total-by-team 0) elapsed)
avg (/ (double elapsed) completed)]
(-> stats
(update :elapsed/max-by-team (fnil max 0) elapsed)
(assoc :elapsed/avg-by-team avg)
(assoc :elapsed/total-by-team total)
(assoc :processed/teams completed)))))))))))

View file

@ -0,0 +1,48 @@
;; 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.features.fdata
"A `fdata/*` related feature migration helpers"
(:require
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]))
(defn enable-objects-map
[file]
(-> file
(update :data (fn [data]
(-> data
(update :pages-index update-vals #(update % :objects omap/wrap))
(update :components update-vals #(update % :objects omap/wrap)))))
(update :features conj "fdata/objects-map")))
(defn enable-pointer-map
[file]
(-> file
(update :data (fn [data]
(-> data
(update :pages-index update-vals pmap/wrap)
(update :components pmap/wrap))))
(update :features conj "fdata/pointer-map")))
;; (defn enable-shape-data-type
;; [file]
;; (letfn [(update-object [object]
;; (-> object
;; (d/update-when :selrect grc/make-rect)
;; (d/update-when :svg-viewbox grc/make-rect)
;; (cts/map->Shape)))
;; (update-container [container]
;; (d/update-when container :objects update-vals update-object))]
;; (-> file
;; (update :data (fn [data]
;; (-> data
;; (update :pages-index update-vals update-container)
;; (update :components update-vals update-container))))
;; (update :features conj "fdata/shape-data-type"))))

View file

@ -333,6 +333,8 @@
{:name "0106-mod-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0106-mod-file-object-thumbnail-table.sql")}
{:name "0106-mod-team-table"
:fn (mg/resource "app/migrations/sql/0106-mod-team-table.sql")}
])
(defn apply-migrations!

View file

@ -0,0 +1 @@
ALTER TABLE team ADD COLUMN features text[] NULL DEFAULT null;

View file

@ -10,6 +10,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
@ -291,9 +292,12 @@
(defn create-profile-rels!
[conn {:keys [id] :as profile}]
(let [team (teams/create-team conn {:profile-id id
:name "Default"
:is-default true})]
(let [features (cfeat/get-enabled-features cf/flags)
team (teams/create-team conn
{:profile-id id
:name "Default"
:features features
:is-default true})]
(-> (db/update! conn :profile
{:default-team-id (:id team)
:default-project-id (:default-project-id team)}

View file

@ -8,10 +8,9 @@
(:refer-clojure :exclude [assert])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.files.defaults :as cfd]
[app.common.files.features :as ffeat]
[app.common.files.migrations :as pmg]
[app.common.fressian :as fres]
[app.common.logging :as l]
@ -20,26 +19,30 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.features.components-v2 :as features.components-v2]
[app.features.fdata :as features.fdata]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.tasks.file-gc]
[app.util.blob :as blob]
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[clojure.walk :as walk]
[cuerdas.core :as str]
[datoteka.io :as io]
[promesa.util :as pu]
[yetti.adapter :as yt]
[yetti.response :as yrs])
(:import
@ -320,7 +323,7 @@
(defn- get-file-media
[{:keys [::db/pool]} {:keys [data id] :as file}]
(dm/with-open [conn (db/open pool)]
(pu/with-open [conn (db/open pool)]
(let [ids (app.tasks.file-gc/collect-used-media data)
ids (db/create-array conn "uuid" ids)
sql (str "SELECT * FROM file_media_object WHERE id = ANY(?)")]
@ -354,7 +357,7 @@
(defn- get-libraries
[{:keys [::db/pool]} ids]
(dm/with-open [conn (db/open pool)]
(pu/with-open [conn (db/open pool)]
(let [ids (db/create-array conn "uuid" ids)]
(map :id (db/exec! pool [sql:file-libraries ids])))))
@ -366,7 +369,7 @@
" WHERE flr.file_id = ANY(?)")]
(db/exec! conn [sql ids])))))
(defn- create-or-update-file
(defn- create-or-update-file!
[conn params]
(let [sql (str "INSERT INTO file (id, project_id, name, revn, is_shared, data, created_at, modified_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?) "
@ -388,6 +391,7 @@
(def ^:dynamic *options* nil)
;; --- EXPORT WRITER
(defn- embed-file-assets
[data cfg file-id]
(letfn [(walk-map-form [form state]
@ -472,19 +476,19 @@
(defmethod write-export :default
[{:keys [::output] :as options}]
(write-header! output :v1)
(with-open [output (zstd-output-stream output :level 12)]
(with-open [output (io/data-output-stream output)]
(binding [*state* (volatile! {})]
(run! (fn [section]
(l/debug :hint "write section" :section section ::l/sync? true)
(write-label! output section)
(let [options (-> options
(assoc ::output output)
(assoc ::section section))]
(binding [*options* options]
(write-section options))))
(pu/with-open [output (zstd-output-stream output :level 12)
output (io/data-output-stream output)]
(binding [*state* (volatile! {})]
(run! (fn [section]
(l/dbg :hint "write section" :section section ::l/sync? true)
(write-label! output section)
(let [options (-> options
(assoc ::output output)
(assoc ::section section))]
(binding [*options* options]
(write-section options))))
[:v1/metadata :v1/files :v1/rels :v1/sobjects])))))
[:v1/metadata :v1/files :v1/rels :v1/sobjects]))))
(defmethod write-section :v1/metadata
[{:keys [::output ::file-ids ::include-libraries?] :as cfg}]
@ -506,23 +510,24 @@
(doseq [file-id (-> *state* deref :files)]
(let [detach? (and (not embed-assets?) (not include-libraries?))
file (cond-> (get-file cfg file-id)
detach?
(-> (ctf/detach-external-references file-id)
(dissoc :libraries))
embed-assets?
(update :data embed-file-assets cfg file-id))
file (cond-> (get-file cfg file-id)
detach?
(-> (ctf/detach-external-references file-id)
(dissoc :libraries))
embed-assets?
(update :data embed-file-assets cfg file-id))
media (get-file-media cfg file)]
media (get-file-media cfg file)]
(l/debug :hint "write penpot file"
:id file-id
:name (:name file)
:media (count media)
::l/sync? true)
(l/dbg :hint "write penpot file"
:id file-id
:name (:name file)
:features (:features file)
:media (count media)
::l/sync? true)
(doseq [item media]
(l/debug :hint "write penpot file media object" :id (:id item) ::l/sync? true))
(l/dbg :hint "write penpot file media object" :id (:id item) ::l/sync? true))
(doto output
(write-obj! file)
@ -535,7 +540,7 @@
(let [ids (-> *state* deref :files)
rels (when include-libraries?
(get-library-relations cfg ids))]
(l/debug :hint "found rels" :total (count rels) ::l/sync? true)
(l/dbg :hint "found rels" :total (count rels) ::l/sync? true)
(write-obj! output rels)))
(defmethod write-section :v1/sobjects
@ -543,21 +548,21 @@
(let [sids (-> *state* deref :sids)
storage (media/configure-assets-storage storage)]
(l/debug :hint "found sobjects"
:items (count sids)
::l/sync? true)
(l/dbg :hint "found sobjects"
:items (count sids)
::l/sync? true)
;; Write all collected storage objects
(write-obj! output sids)
(doseq [id sids]
(let [{:keys [size] :as obj} (sto/get-object storage id)]
(l/debug :hint "write sobject" :id id ::l/sync? true)
(l/dbg :hint "write sobject" :id id ::l/sync? true)
(doto output
(write-uuid! id)
(write-obj! (meta obj)))
(with-open [^InputStream stream (sto/get-object-data storage obj)]
(pu/with-open [stream (sto/get-object-data storage obj)]
(let [written (write-stream! output stream size)]
(when (not= written size)
(ex/raise :type :validation
@ -574,15 +579,16 @@
(defmulti read-import ::version)
(defmulti read-section ::section)
(s/def ::profile-id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::input io/input-stream?)
(s/def ::overwrite? (s/nilable ::us/boolean))
(s/def ::migrate? (s/nilable ::us/boolean))
(s/def ::ignore-index-errors? (s/nilable ::us/boolean))
;; FIXME: replace with schema
(s/def ::read-import-options
(s/keys :req [::db/pool ::sto/storage ::project-id ::input]
:opt [::overwrite? ::migrate? ::ignore-index-errors?]))
(s/keys :req [::db/pool ::sto/storage ::project-id ::profile-id ::input]
:opt [::overwrite? ::ignore-index-errors?]))
(defn read-import!
"Do the importation of the specified resource in penpot custom binary
@ -592,9 +598,6 @@
`::overwrite?`: if true, instead of creating new files and remapping id references,
it reuses all ids and updates existing objects; defaults to `false`.
`::migrate?`: if true, applies the migration before persisting the
file data; defaults to `false`.
`::ignore-index-errors?`: if true, do not fail on index lookup errors, can
happen with broken files; defaults to: `false`.
"
@ -604,53 +607,95 @@
(let [version (read-header! input)]
(read-import (assoc options ::version version ::timestamp timestamp))))
(defmethod read-import :v1
[{:keys [::db/pool ::input] :as options}]
(with-open [input (zstd-input-stream input)]
(with-open [input (io/data-input-stream input)]
(db/with-atomic [conn pool]
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED;"])
(binding [*state* (volatile! {:media [] :index {}})]
(run! (fn [section]
(l/debug :hint "reading section" :section section ::l/sync? true)
(assert-read-label! input section)
(let [options (-> options
(assoc ::section section)
(assoc ::input input)
(assoc ::db/conn conn))]
(binding [*options* options]
(read-section options))))
[:v1/metadata :v1/files :v1/rels :v1/sobjects])
(defn- read-import-v1
[{:keys [::db/conn ::project-id ::profile-id ::input] :as options}]
(db/exec-one! conn ["SET idle_in_transaction_session_timeout = 0"])
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
;; Knowing that the ids of the created files are in
;; index, just lookup them and return it as a set
(let [files (-> *state* deref :files)]
(into #{} (keep #(get-in @*state* [:index %])) files)))))))
(pu/with-open [input (zstd-input-stream input)
input (io/data-input-stream input)]
(binding [*state* (volatile! {:media [] :index {}})]
(let [team (teams/get-team options
:profile-id profile-id
:project-id project-id)
features (cfeat/get-team-enabled-features cf/flags team)]
;; Process all sections
(run! (fn [section]
(l/dbg :hint "reading section" :section section ::l/sync? true)
(assert-read-label! input section)
(let [options (-> options
(assoc ::enabled-features features)
(assoc ::section section)
(assoc ::input input))]
(binding [*options* options]
(read-section options))))
[:v1/metadata :v1/files :v1/rels :v1/sobjects])
;; Run all pending migrations
(doseq [[feature file-id] (-> *state* deref :pending-to-migrate)]
(case feature
"components/v2"
(features.components-v2/migrate-file! options file-id)
"fdata/shape-data-type"
nil
;; "fdata/shape-data-type"
;; (features.fdata/enable-objects-map
(ex/raise :type :internal
:code :no-migration-defined
:hint (str/ffmt "no migation for feature '%' on file importation" feature)
:feature feature)))
;; Knowing that the ids of the created files are in index,
;; just lookup them and return it as a set
(let [files (-> *state* deref :files)]
(into #{} (keep #(get-in @*state* [:index %])) files))))))
(defmethod read-import :v1
[options]
(db/tx-run! options read-import-v1))
(defmethod read-section :v1/metadata
[{:keys [::input]}]
(let [{:keys [version files]} (read-obj! input)]
(l/debug :hint "metadata readed" :version (:full version) :files files ::l/sync? true)
(l/dbg :hint "metadata readed" :version (:full version) :files files ::l/sync? true)
(vswap! *state* update :index update-index files)
(vswap! *state* assoc :version version :files files)))
(defn- postprocess-file
[data]
(let [omap-wrap ffeat/*wrap-with-objects-map-fn*
pmap-wrap ffeat/*wrap-with-pointer-map-fn*]
(-> data
(update :pages-index update-vals #(update % :objects omap-wrap))
(update :pages-index update-vals pmap-wrap)
(update :components update-vals #(d/update-when % :objects omap-wrap))
(update :components pmap-wrap))))
[file]
(cond-> file
(and (contains? cfeat/*current* "fdata/objects-map")
(not (contains? cfeat/*previous* "fdata/objects-map")))
(features.fdata/enable-objects-map)
(and (contains? cfeat/*current* "fdata/pointer-map")
(not (contains? cfeat/*previous* "fdata/pointer-map")))
(features.fdata/enable-pointer-map)))
(defmethod read-section :v1/files
[{:keys [::db/conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}]
[{:keys [::db/conn ::input ::project-id ::enabled-features ::timestamp ::overwrite?]}]
(doseq [expected-file-id (-> *state* deref :files)]
(let [file (read-obj! input)
media' (read-obj! input)
file-id (:id file)
features (files/get-default-features)]
(let [file (read-obj! input)
media' (read-obj! input)
file-id (:id file)
file-id' (lookup-index file-id)
features (-> enabled-features
(set/difference cfeat/frontend-only-features)
(set/union (cfeat/check-supported-features! (:features file))))
]
;; All features that are enabled and requires explicit migration
;; are added to the state for a posterior migration step
(doseq [feature (-> enabled-features
(set/difference cfeat/no-migration-features)
(set/difference (:features file)))]
(vswap! *state* update :pending-to-migrate (fnil conj []) [feature file-id']))
(when (not= file-id expected-file-id)
(ex/raise :type :validation
@ -667,59 +712,54 @@
(l/dbg :hint "update media references" ::l/sync? true)
(vswap! *state* update :media into (map #(update % :id lookup-index)) media')
(binding [ffeat/*current* features
ffeat/*wrap-with-objects-map-fn* (if (features "storage/objects-map") omap/wrap identity)
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)
(binding [cfeat/*current* features
cfeat/*previous* (:features file)
pmap/*tracked* (atom {})]
(l/dbg :hint "processing file"
:id file-id
:features features
:features (:features file)
:version (-> file :data :version)
::l/sync? true)
(let [file-id' (lookup-index file-id)
data (-> (:data file)
(assoc :id file-id'))
(let [params (-> file
(assoc :id file-id')
(assoc :features features)
(assoc :project-id project-id)
(assoc :created-at timestamp)
(assoc :modified-at timestamp)
(update :data (fn [data]
(-> data
(assoc :id file-id')
(cond-> (> (:version data) cfd/version)
(assoc :version cfd/version))
data (if (> (:version data) cfd/version)
(assoc data :version cfd/version)
data)
;; FIXME: We're temporarily activating all
;; migrations because a problem in the
;; environments messed up with the version
;; numbers When this problem is fixed delete
;; the following line
(assoc :version 0)
(update :pages-index relink-shapes)
(update :components relink-shapes)
(update :media relink-media)
(pmg/migrate-data))))
(postprocess-file)
(update :features #(db/create-array conn "text" %))
(update :data blob/encode))]
;; FIXME
;; We're temporarily activating all migrations because a problem in
;; the environments messed up with the version numbers
;; When this problem is fixed delete the following line
data (-> data (assoc :version 0))
data (-> data
(cond-> migrate? (pmg/migrate-data))
(update :pages-index relink-shapes)
(update :components relink-shapes)
(update :media relink-media)
(postprocess-file))
params {:id file-id'
:project-id project-id
:features (db/create-array conn "text" features)
:name (:name file)
:revn (:revn file)
:is-shared (:is-shared file)
:data (blob/encode data)
:created-at timestamp
:modified-at timestamp}]
(l/debug :hint "create file" :id file-id' ::l/sync? true)
(l/dbg :hint "create file" :id file-id' ::l/sync? true)
(if overwrite?
(create-or-update-file conn params)
(create-or-update-file! conn params)
(db/insert! conn :file params))
(files/persist-pointers! conn file-id')
(when overwrite?
(db/delete! conn :file-thumbnail {:file-id file-id'})))))))
(db/delete! conn :file-thumbnail {:file-id file-id'}))
file-id')))))
(defmethod read-section :v1/rels
[{:keys [::db/conn ::input ::timestamp]}]
@ -734,10 +774,10 @@
(if (contains? ids library-file-id)
(do
(l/debug :hint "create file library link"
:file-id (:file-id rel)
:lib-id (:library-file-id rel)
::l/sync? true)
(l/dbg :hint "create file library link"
:file-id (:file-id rel)
:lib-id (:library-file-id rel)
::l/sync? true)
(db/insert! conn :file-library-rel rel))
(l/warn :hint "ignoring file library link"
@ -759,7 +799,7 @@
:code :inconsistent-penpot-file
:hint "the penpot file seems corrupt, found unexpected uuid (storage-object-id)"))
(l/debug :hint "readed storage object" :id id ::l/sync? true)
(l/dbg :hint "readed storage object" :id id ::l/sync? true)
(let [[size resource] (read-stream! input)
hash (sto/calculate-hash resource)
@ -773,14 +813,14 @@
sobject (sto/put-object! storage params)]
(l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true)
(l/dbg :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true)
(vswap! *state* update :index assoc id (:id sobject)))))
(doseq [item (:media @*state*)]
(l/debug :hint "inserting file media object"
:id (:id item)
:file-id (:file-id item)
::l/sync? true)
(l/dbg :hint "inserting file media object"
:id (:id item)
:file-id (:file-id item)
::l/sync? true)
(let [file-id (lookup-index (:file-id item))]
(if (= file-id (:file-id item))
@ -886,7 +926,7 @@
cs (volatile! nil)]
(try
(l/info :hint "start exportation" :export-id id)
(dm/with-open [output (io/output-stream output)]
(pu/with-open [output (io/output-stream output)]
(binding [*position* (atom 0)]
(write-export! (assoc cfg ::output output))))
@ -909,7 +949,7 @@
(defn export-to-tmpfile!
[cfg]
(let [path (tmp/tempfile :prefix "penpot.export.")]
(dm/with-open [output (io/output-stream path)]
(pu/with-open [output (io/output-stream path)]
(export! cfg output)
path)))
@ -921,7 +961,7 @@
(l/info :hint "import: started" :import-id id)
(try
(binding [*position* (atom 0)]
(dm/with-open [input (io/input-stream input)]
(pu/with-open [input (io/input-stream input)]
(read-import! (assoc cfg ::input input))))
(catch Throwable cause
@ -980,6 +1020,7 @@
(let [ids (import! (assoc cfg
::input (:path file)
::project-id project-id
::profile-id profile-id
::ignore-index-errors? true))]
(db/update! conn :project

View file

@ -9,11 +9,11 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.files.migrations :as pmg]
[app.common.pages.helpers :as cph]
[app.common.schema :as sm]
[app.common.schema.desc-js-like :as-alias smdj]
[app.common.schema.generators :as sg]
[app.common.spec :as us]
[app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf]
@ -43,23 +43,6 @@
(when media-id
(str (cf/get :public-uri) "/assets/by-id/" media-id)))
(def supported-features
#{"storage/objects-map"
"storage/pointer-map"
"internal/shape-record"
"internal/geom-record"
"components/v2"})
(defn get-default-features
[]
(cond-> #{"internal/shape-record"
"internal/geom-record"}
(contains? cf/flags :fdata-storage-pointer-map)
(conj "storage/pointer-map")
(contains? cf/flags :fdata-storage-objects-map)
(conj "storage/objects-map")))
;; --- SPECS
(s/def ::features ::us/set-of-strings)
@ -181,28 +164,10 @@
:code :object-not-found
:hint "not found"))))
;; --- HELPERS
(defn get-team-id
[conn project-id]
(:team-id (db/get-by-id conn :project project-id {:columns [:team-id]})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FEATURES: pointer-map
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn check-features-compatibility!
"Function responsible to check if provided features are supported by
the current backend"
[features]
(let [not-supported (set/difference features supported-features)]
(when (seq not-supported)
(ex/raise :type :restriction
:code :features-not-supported
:feature (first not-supported)
:hint (format "features %s not supported" (str/join "," (map name not-supported)))))
features))
(defn load-pointer
[conn file-id id]
(let [row (db/get conn :file-data-fragment
@ -253,73 +218,16 @@
(into #{} (comp (filter pmap/pointer-map?)
(map pmap/get-id)))))
(declare get-file-libraries)
;; FIXME: file locking
(defn- process-components-v2-feature
"A special case handling of the components/v2 feature."
[conn {:keys [features data] :as file}]
(let [libraries (-> (->> (get-file-libraries conn (:id file)) ; This may be slow, but it's executed only once,
(map #(db/get conn :file {:id (:id %)})) ; in the migration to components-v2
(map #(update % :data blob/decode))
(d/index-by :id))
(assoc (:id file) file))
data (ctf/migrate-to-components-v2 data libraries)
features (conj features "components/v2")]
(-> file
(assoc ::pmg/migrated true)
(assoc :features features)
(assoc :data data))))
(defn handle-file-features!
[conn {:keys [features] :as file} client-features]
;; Check features compatibility between the currently supported features on
;; the current backend instance and the file retrieved from the database
(check-features-compatibility! features)
(cond-> file
(and (contains? features "components/v2")
(not (contains? client-features "components/v2")))
(as-> file (ex/raise :type :restriction
:code :feature-mismatch
:feature "components/v2"
:hint "file has 'components/v2' feature enabled but frontend didn't specifies it"
:file-id (:id file)))
;; This operation is needed because the components migration generates a new
;; page with random id which is returned to the client; without persisting
;; the migration this can cause that two simultaneous clients can have a
;; different view of the file data and end persisting two pages with main
;; components and breaking the whole file."
(and (contains? client-features "components/v2")
(not (contains? features "components/v2")))
(as-> file (process-components-v2-feature conn file))
;; This operation is needed for backward comapatibility with frontends that
;; does not support pointer-map resolution mechanism; this just resolves the
;; pointers on backend and return a complete file.
(and (contains? features "storage/pointer-map")
(not (contains? client-features "storage/pointer-map")))
(process-pointers deref)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUERY COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- COMMAND QUERY: get-file (by id)
(def schema:features
[:schema
{:title "FileFeatures"
::smdj/inline true
:gen/gen (sg/subseq supported-features)}
::sm/set-of-strings])
(def schema:file
[:map {:title "File"}
[:id ::sm/uuid]
[:features schema:features]
[:features ::cfeat/features]
[:has-media-trimmed :boolean]
[:comment-thread-seqn {:min 0} :int]
[:name :string]
@ -341,18 +249,21 @@
(def schema:get-file
[:map {:title "get-file"}
[:features {:optional true} schema:features]
[:features {:optional true} ::cfeat/features]
[:id ::sm/uuid]
[:project-id {:optional true} ::sm/uuid]])
(defn get-file
([conn id client-features]
(get-file conn id client-features nil))
([conn id client-features project-id]
;; here we check if client requested features are supported
(check-features-compatibility! client-features)
([conn id] (get-file conn id nil))
([conn id project-id]
(dm/assert!
"expected raw connection"
(db/connection? conn))
(binding [pmap/*load-fn* (partial load-pointer conn id)
pmap/*tracked* (atom {})]
pmap/*tracked* (atom {})
cfeat/*new* (atom #{})]
(let [params (merge {:id id}
(when (some? project-id)
@ -360,22 +271,21 @@
file (-> (db/get conn :file params)
(decode-row)
(pmg/migrate-file))
file (handle-file-features! conn file client-features)]
(pmg/migrate-file))]
;; NOTE: when file is migrated, we break the rule of no perform
;; mutations on get operations and update the file with all
;; migrations applied
(when (pmg/migrated? file)
(let [features (db/create-array conn "text" (:features file))]
(if (pmg/migrated? file)
(let [features (set/union (deref cfeat/*new*) (:features file))]
(db/update! conn :file
{:data (blob/encode (:data file))
:features features}
:features (db/create-array conn "text" features)}
{:id id})
(persist-pointers! conn id)))
(persist-pointers! conn id)
(assoc file :features features))
file))))
file)))))
(defn get-minimal-file
[{:keys [::db/pool] :as cfg} id]
@ -392,14 +302,32 @@
::cond/key-fn get-file-etag
::sm/params schema:get-file
::sm/result schema:file-with-permissions}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features project-id] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id id)]
(check-read-permissions! perms)
(let [file (-> (get-file conn id features project-id)
(assoc :permissions perms))]
(vary-meta file assoc ::cond/key (get-file-etag params file))))))
[cfg {:keys [::rpc/profile-id id project-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(let [perms (get-permissions conn profile-id id)]
(check-read-permissions! perms)
(let [team (teams/get-team cfg
:profile-id profile-id
:project-id project-id
:file-id id)
file (-> (get-file conn id project-id)
(assoc :permissions perms))
_ (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
;; This operation is needed for backward comapatibility with frontends that
;; does not support pointer-map resolution mechanism; this just resolves the
;; pointers on backend and return a complete file.
file (if (and (contains? (:features file) "fdata/pointer-map")
(not (contains? (:features params) "fdata/pointer-map")))
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(process-pointers file deref))
file)]
(vary-meta file assoc ::cond/key (get-file-etag params file)))))))
;; --- COMMAND QUERY: get-file-fragment (by id)
@ -422,7 +350,7 @@
(update :content blob/decode)))
(sv/defmethod ::get-file-fragment
"Retrieve a file by its ID. Only authenticated users."
"Retrieve a file fragment by its ID. Only authenticated users."
{::doc/added "1.17"
::sm/params schema:get-file-fragment
::sm/result schema:file-fragment}
@ -477,7 +405,6 @@
(projects/check-read-permissions! conn profile-id project-id)
(get-project-files conn project-id)))
;; --- COMMAND QUERY: has-file-libraries
(declare get-has-file-libraries)
@ -528,30 +455,41 @@
(update page :objects update-vals #(dissoc % :thumbnail)))
(defn get-page
[conn {:keys [file-id page-id object-id features]}]
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id page-id object-id] :as params}]
(when (and (uuid? object-id)
(not (uuid? page-id)))
(ex/raise :type :validation
:code :params-validation
:hint "page-id is required when object-id is provided"))
(let [file (get-file conn file-id features)
page-id (or page-id (-> file :data :pages first))
page (dm/get-in file [:data :pages-index page-id])
page (if (pmap/pointer-map? page)
(let [team (teams/get-team cfg
:profile-id profile-id
:file-id file-id)
file (get-file conn file-id)
_ (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
page (binding [pmap/*load-fn* (partial load-pointer conn file-id)]
(let [page-id (or page-id (-> file :data :pages first))
page (dm/get-in file [:data :pages-index page-id])]
(if (pmap/pointer-map? page)
(deref page)
page)]
page)))]
(cond-> (prune-thumbnails page)
(uuid? object-id)
(prune-objects object-id))))
(def schema:get-page
[:map {:title "GetPage"}
[:map {:title "get-page"}
[:file-id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]
[:object-id {:optional true} ::sm/uuid]
[:features {:optional true} schema:features]])
[:features {:optional true} ::cfeat/features]])
(sv/defmethod ::get-page
"Retrieves the page data from file and returns it. If no page-id is
@ -565,12 +503,11 @@
Mainly used for rendering purposes."
{::doc/added "1.17"
::sm/params schema:get-page}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
(dm/with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id file-id share-id)]
(check-read-permissions! perms)
(binding [pmap/*load-fn* (partial load-pointer conn file-id)]
(get-page conn params)))))
[cfg {:keys [::rpc/profile-id file-id share-id] :as params}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(check-read-permissions! conn profile-id file-id share-id)
(get-page cfg (assoc params :profile-id profile-id)))))
;; --- COMMAND QUERY: get-team-shared-files
@ -593,6 +530,7 @@
and p.team_id = ?
order by f.modified_at desc")
;; FIXME: i'm not sure about feature handling here... ???
(defn get-team-shared-files
[conn team-id]
(letfn [(assets-sample [assets limit]
@ -626,19 +564,19 @@
(map #(assoc % :library-summary (library-summary %)))
(map #(dissoc % :data)))))))
(s/def ::get-team-shared-files
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(def ^:private schema:get-team-shared-files
[:map {:title "get-team-shared-files"}
[:team-id ::sm/uuid]])
(sv/defmethod ::get-team-shared-files
"Get all file (libraries) for the specified team."
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:get-team-shared-files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(get-team-shared-files conn team-id)))
;; --- COMMAND QUERY: get-file-libraries
(def ^:private sql:get-file-libraries
@ -669,17 +607,20 @@
[conn file-id]
(into []
(comp
;; FIXME: :is-indirect set to false to all rows looks
;; completly useless
(map #(assoc % :is-indirect false))
(map decode-row))
(db/exec! conn [sql:get-file-libraries file-id])))
(s/def ::get-file-libraries
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]))
(def ^:private schema:get-file-libraries
[:map {:title "get-file-libraries"}
[:file-id ::sm/uuid]])
(sv/defmethod ::get-file-libraries
"Get libraries used by the specified file."
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:get-file-libraries}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
@ -700,12 +641,14 @@
[conn file-id]
(db/exec! conn [sql:library-using-files file-id]))
(s/def ::get-library-file-references
(s/keys :req [::rpc/profile-id] :req-un [::file-id]))
(def ^:private schema:get-library-file-references
[:map {:title "get-library-file-references"}
[:file-id ::sm/uuid]])
(sv/defmethod ::get-library-file-references
"Returns all the file references that use specified file (library) id."
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:get-library-file-references}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
@ -745,12 +688,13 @@
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(dissoc row :media-id))))))
(s/def ::get-team-recent-files
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(def ^:private schema:get-team-recent-files
[:map {:title "get-team-recent-files"}
[:team-id ::sm/uuid]])
(sv/defmethod ::get-team-recent-files
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:get-team-recent-files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
@ -763,15 +707,26 @@
"Retrieve a file summary by its ID. Only authenticated users."
{::doc/added "1.20"
::sm/params schema:get-file}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features project-id] :as params}]
(db/with-atomic [conn pool]
(check-read-permissions! conn profile-id id)
(let [file (get-file conn id features project-id)]
{:name (:name file)
:components-count (count (ctkl/components-seq (:data file)))
:graphics-count (count (get-in file [:data :media] []))
:colors-count (count (get-in file [:data :colors] []))
:typography-count (count (get-in file [:data :typographies] []))})))
[cfg {:keys [::rpc/profile-id id project-id] :as params}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(check-read-permissions! conn profile-id id)
(let [team (teams/get-team cfg
:profile-id profile-id
:project-id project-id
:file-id id)
file (get-file conn id project-id)]
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
{:name (:name file)
:components-count (count (ctkl/components-seq (:data file)))
:graphics-count (count (get-in file [:data :media] []))
:colors-count (count (get-in file [:data :colors] []))
:typography-count (count (get-in file [:data :typographies] []))}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS
@ -927,13 +882,15 @@
[conn {:keys [file-id library-id] :as params}]
(db/exec-one! conn [sql:link-file-to-library file-id library-id]))
(s/def ::link-file-to-library
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::library-id]))
(def ^:private schema:link-file-to-library
[:map {:title "link-file-to-library"}
[:file-id ::sm/uuid]
[:library-id ::sm/uuid]])
(sv/defmethod ::link-file-to-library
{::doc/added "1.17"
::webhooks/event? true}
::webhooks/event? true
::sm/params schema:link-file-to-library}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
(when (= file-id library-id)
(ex/raise :type :validation
@ -952,13 +909,15 @@
{:file-id file-id
:library-file-id library-id}))
(s/def ::unlink-file-from-library
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::library-id]))
(def ^:private schema:unlink-file-to-library
[:map {:title "unlink-file-to-library"}
[:file-id ::sm/uuid]
[:library-id ::sm/uuid]])
(sv/defmethod ::unlink-file-from-library
{::doc/added "1.17"
::webhooks/event? true}
::webhooks/event? true
::sm/params schema:unlink-file-to-library}
[{: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)
@ -974,15 +933,15 @@
{:file-id file-id
:library-file-id library-id}))
(s/def ::update-file-library-sync-status
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::library-id]))
;; TODO: improve naming
(def ^:private schema:update-file-library-sync-status
[:map {:title "update-file-library-sync-status"}
[:file-id ::sm/uuid]
[:library-id ::sm/uuid]])
(sv/defmethod ::update-file-library-sync-status
"Update the synchronization status of a file->library link"
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:update-file-library-sync-status}
[{: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)

View file

@ -7,15 +7,18 @@
(ns app.rpc.commands.files-create
(:require
[app.common.data :as d]
[app.common.files.features :as ffeat]
[app.common.features :as cfeat]
[app.common.schema :as sm]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.permissions :as perms]
[app.rpc.quotes :as quotes]
@ -24,7 +27,7 @@
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
[clojure.set :as set]))
(defn create-file-role!
[conn {:keys [file-id profile-id role]}]
@ -34,27 +37,27 @@
(db/insert! conn :file-profile-rel))))
(defn create-file
[conn {:keys [id name project-id is-shared revn
modified-at deleted-at create-page
ignore-sync-until features]
:or {is-shared false revn 0 create-page true}
:as params}]
[{:keys [::db/conn] :as cfg}
{:keys [id name project-id is-shared revn
modified-at deleted-at create-page
ignore-sync-until features]
:or {is-shared false revn 0 create-page true}
:as params}]
(let [id (or id (uuid/next))
features (->> features
(into (files/get-default-features))
(files/check-features-compatibility!))
pointers (atom {})
data (binding [pmap/*tracked* pointers
ffeat/*current* features
ffeat/*wrap-with-objects-map-fn* (if (features "storate/objects-map") omap/wrap identity)
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)]
cfeat/*current* features
cfeat/*wrap-with-objects-map-fn* (if (features "fdata/objects-map") omap/wrap identity)
cfeat/*wrap-with-pointer-map-fn* (if (features "fdata/pointer-map") pmap/wrap identity)]
(if create-page
(ctf/make-file-data id)
(ctf/make-file-data id nil)))
features (db/create-array conn "text" features)
features (->> (set/difference features cfeat/frontend-only-features)
(db/create-array conn "text"))
file (db/insert! conn :file
(d/without-nils
{:id id
@ -80,29 +83,58 @@
(files/decode-row file)))
(s/def ::create-file
(s/keys :req [::rpc/profile-id]
:req-un [::files/name
::files/project-id]
:opt-un [::files/id
::files/is-shared
::files/features]))
(def ^:private schema:create-file
[:map {:title "create-file"}
[:name :string]
[:project-id ::sm/uuid]
[:id {:optional true} ::sm/uuid]
[:is-shared {:optional true} :boolean]
[:features {:optional true} ::cfeat/features]])
(sv/defmethod ::create-file
{::doc/added "1.17"
::doc/module :files
::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(db/with-atomic [conn pool]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team-id (files/get-team-id conn project-id)
params (assoc params :profile-id profile-id)]
::webhooks/event? true
::sm/params schema:create-file}
[cfg {:keys [::rpc/profile-id project-id] :as params}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team cfg
:profile-id profile-id
:project-id project-id)
team-id (:id team)
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id
::quotes/profile-id profile-id
::quotes/project-id project-id}))
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)))
(-> (create-file conn params)
(vary-meta assoc ::audit/props {:team-id team-id})))))
;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file
features (-> (:features params #{})
(set/intersection cfeat/no-migration-features)
(set/union features))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))]
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id
::quotes/profile-id profile-id
::quotes/project-id project-id}))
;; When newly computed features does not match exactly with
;; the features defined on team row, we update it.
(when (not= features (:features team))
(let [features (db/create-array conn "text" features)]
(db/update! conn :team
{:features features}
{:id team-id})))
(-> (create-file cfg params)
(vary-meta assoc ::audit/props {:team-id team-id}))))))

View file

@ -9,12 +9,14 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.thumbnails :as thc]
[app.common.types.shape-tree :as ctt]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.loggers.audit :as-alias audit]
@ -22,6 +24,7 @@
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
@ -237,7 +240,7 @@
(def ^:private schema:get-file-data-for-thumbnail
[:map {:title "get-file-data-for-thumbnail"}
[:file-id ::sm/uuid]
[:features {:optional true} files/schema:features]])
[:features {:optional true} ::cfeat/features]])
(def ^:private schema:partial-file
[:map {:title "PartialFile"}
@ -252,17 +255,23 @@
::doc/module :files
::sm/params schema:get-file-data-for-thumbnail
::sm/result schema:partial-file}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
(dm/with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
;; NOTE: we force here the "storage/pointer-map" feature, because
;; it used internally only and is independent if user supports it
;; or not.
(let [feat (into #{"storage/pointer-map"} features)
file (files/get-file conn file-id feat)]
{:file-id file-id
:revn (:revn file)
:page (get-file-data-for-thumbnail conn file)})))
[cfg {:keys [::rpc/profile-id file-id] :as params}]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-read-permissions! conn profile-id file-id)
(let [team (teams/get-team cfg
:profile-id profile-id
:file-id file-id)
file (files/get-file conn file-id)]
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
{:file-id file-id
:revn (:revn file)
:page (get-file-data-for-thumbnail conn file)}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS

View file

@ -8,18 +8,17 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.files.features :as ffeat]
[app.common.features :as cfeat]
[app.common.files.migrations :as pmg]
[app.common.files.validate :as val]
[app.common.logging :as l]
[app.common.pages :as cp]
[app.common.pages.changes :as cpc]
[app.common.schema :as sm]
[app.common.schema.generators :as smg]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.features.fdata :refer [enable-pointer-map enable-objects-map]]
[app.loggers.audit :as audit]
[app.loggers.webhooks :as webhooks]
[app.metrics :as mtx]
@ -27,43 +26,42 @@
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit]
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.util.blob :as blob]
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]))
[app.util.time :as dt]
[clojure.set :as set]))
;; --- SCHEMA
(sm/def! ::changes
(def ^:private schema:changes
[:vector ::cpc/change])
(sm/def! ::change-with-metadata
(def ^:private schema:change-with-metadata
[:map {:title "ChangeWithMetadata"}
[:changes ::changes]
[:changes schema:changes]
[:hint-origin {:optional true} :keyword]
[:hint-events {:optional true} [:vector :string]]])
(sm/def! ::update-file-params
[:map {:title "UpdateFileParams"}
(def ^:private schema:update-file
[:map {:title "update-file"}
[:id ::sm/uuid]
[:session-id ::sm/uuid]
[:revn {:min 0} :int]
[:features {:optional true
:gen/max 3
:gen/gen (smg/subseq files/supported-features)}
::sm/set-of-strings]
[:changes {:optional true} ::changes]
[:features {:optional true} ::cfeat/features]
[:changes {:optional true} schema:changes]
[:changes-with-metadata {:optional true}
[:vector ::change-with-metadata]]
[:vector schema:change-with-metadata]]
[:skip-validate {:optional true} :boolean]])
(sm/def! ::update-file-result
[:vector {:title "UpdateFileResults"}
[:map {:title "UpdateFileResult"}
[:changes ::changes]
(def ^:private schema:update-file-result
[:vector {:title "update-file-result"}
[:map
[:changes schema:changes]
[:file-id ::sm/uuid]
[:id ::sm/uuid]
[:revn {:min 0} :int]
@ -112,7 +110,7 @@
(fn [{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
(binding [pmap/*tracked* (atom {})
pmap/*load-fn* (partial files/load-pointer conn id)
ffeat/*wrap-with-pointer-map-fn* pmap/wrap]
cfeat/*wrap-with-pointer-map-fn* pmap/wrap]
(let [result (f cfg file)]
(files/persist-pointers! conn id)
result))))
@ -120,7 +118,7 @@
(defn- wrap-with-objects-map-context
[f]
(fn [cfg file]
(binding [ffeat/*wrap-with-objects-map-fn* omap/wrap]
(binding [cfeat/*wrap-with-objects-map-fn* omap/wrap]
(f cfg file))))
(declare get-lagged-changes)
@ -141,81 +139,95 @@
::webhooks/batch-timeout (dt/duration "2m")
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
::sm/params ::update-file-params
::sm/result ::update-file-result
::sm/params schema:update-file
::sm/result schema:update-file-result
::doc/module :files
::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id)
(db/xact-lock! conn id)
[cfg {:keys [::rpc/profile-id id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id id)
(db/xact-lock! conn id)
(let [cfg (assoc cfg ::db/conn conn)
params (assoc params :profile-id profile-id)
tpoint (dt/tpoint)]
(-> (update-file cfg params)
(rph/with-defer #(let [elapsed (tpoint)]
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
(let [file (get-file conn id)
team (teams/get-team cfg
:profile-id profile-id
:team-id (:team-id file))
features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
params (assoc params
:profile-id profile-id
:features features
:team team
:file file)
tpoint (dt/tpoint)]
;; When newly computed features does not match exactly with
;; the features defined on team row, we update it.
(when (not= features (:features team))
(let [features (db/create-array conn "text" features)]
(db/update! conn :team
{:features features}
{:id (:id team)})))
(-> (update-file cfg params)
(rph/with-defer #(let [elapsed (tpoint)]
(l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))))
(defn update-file
[{:keys [::db/conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata skip-validate] :as params}]
(let [file (get-file conn id)
features (->> (concat (:features file)
(:features params))
(into (files/get-default-features))
(files/check-features-compatibility!))]
[{:keys [::db/conn ::mtx/metrics] :as cfg} {:keys [id file features changes changes-with-metadata skip-validate] :as params}]
(binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(let [update-fn (cond-> update-file*
(contains? features "fdata/pointer-map")
(wrap-with-pointer-map-context)
(files/check-edition-permissions! conn profile-id (:id file))
(contains? features "fdata/objects-map")
(wrap-with-objects-map-context))
(binding [ffeat/*current* features
ffeat/*previous* (:features file)]
;; TODO: this ruins performance.
;; We must find some other way to do general validation.
libraries (when (and (contains? cf/flags :file-validation)
(not skip-validate))
(let [libs (->> (files/get-file-libraries conn (:id file))
(map #(get-file conn (:id %)))
(map #(update % :data blob/decode))
(d/index-by :id))]
(assoc libs (:id file) file)))
(let [update-fn (cond-> update-file*
(contains? features "storage/pointer-map")
(wrap-with-pointer-map-context)
changes (if changes-with-metadata
(->> changes-with-metadata (mapcat :changes) vec)
(vec changes))
(contains? features "storage/objects-map")
(wrap-with-objects-map-context))
features (-> features
(set/difference cfeat/frontend-only-features)
(set/union (:features file)))]
file (assoc file :features features)
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation
:code :revn-conflict
:hint "The incoming revision number is greater that stored version."
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
;; TODO: this ruins performance.
;; We must find some other way to do general validation.
libraries (when (and (cf/flags :file-validation)
(not skip-validate))
(-> (->> (files/get-file-libraries conn (:id file))
(map #(get-file conn (:id %)))
(map #(update % :data blob/decode))
(d/index-by :id))
(assoc (:id file) file)))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
changes (if changes-with-metadata
(->> changes-with-metadata (mapcat :changes) vec)
(vec changes))
params (-> params
(assoc :file file)
(assoc :libraries libraries)
(assoc :changes changes)
(assoc ::created-at (dt/now)))]
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation
:code :revn-conflict
:hint "The incoming revision number is greater that stored version."
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
(when (not= features (:features file))
(let [features (db/create-array conn "text" features)]
(db/update! conn :file
{:features features}
{:id id})))
(when (not= features (:features file))
(let [features (db/create-array conn "text" features)]
(db/update! conn :file
{:features features}
{:id id})))
(let [file (assoc file :features features)
params (-> params
(assoc :file file)
(assoc :libraries libraries)
(assoc :changes changes)
(assoc ::created-at (dt/now)))]
(-> (update-fn cfg params)
(vary-meta assoc ::audit/replace-props
{:id (:id file)
@ -230,7 +242,7 @@
;; to be executed on a separated executor for avoid to do the
;; CPU intensive operation on vthread.
file (-> (climit/configure cfg :update-file)
(climit/submit! (partial update-file-data conn file libraries changes skip-validate)))]
(climit/submit! (partial update-file-data file libraries changes skip-validate)))]
(db/insert! conn :file-change
{:id (uuid/next)
@ -264,39 +276,36 @@
(get-lagged-changes conn params))))
(defn- update-file-data
[conn file libraries changes skip-validate]
[file libraries changes skip-validate]
(let [validate (fn [file]
(when (and (cf/flags :file-validation)
(not skip-validate))
(val/validate-file file libraries :throw? true)))
do-migrate-v2 (fn [file]
;; When migrating to components-v2 we need the libraries even
;; if the validations are disabled.
(let [libraries (or (seq libraries)
(-> (->> (files/get-file-libraries conn (:id file))
(map #(get-file conn (:id %)))
(map #(update % :data blob/decode))
(d/index-by :id))
(assoc (:id file) file)))]
(ctf/migrate-to-components-v2 file libraries)))]
(-> file
(update :revn inc)
(update :data (fn [data]
(cond-> data
:always
(-> (blob/decode)
(assoc :id (:id file))
(pmg/migrate-data))
file (-> file
(update :revn inc)
(update :data (fn [data]
(cond-> data
:always
(-> (blob/decode)
(assoc :id (:id file))
(pmg/migrate-data))
(and (contains? ffeat/*current* "components/v2")
(not (contains? ffeat/*previous* "components/v2")))
(do-migrate-v2)
:always
(cp/process-changes changes))))
(d/tap-r validate))
:always
(cp/process-changes changes))))
(d/tap-r validate)
(update :data blob/encode))))
file (if (and (contains? cfeat/*current* "fdata/objects-map")
(not (contains? cfeat/*previous* "fdata/objects-map")))
(enable-objects-map file)
file)
file (if (and (contains? cfeat/*current* "fdata/pointer-map")
(not (contains? cfeat/*previous* "fdata/pointer-map")))
(enable-pointer-map file)
file)
]
(update file :data blob/encode)))
(defn- take-snapshot?
"Defines the rule when file `data` snapshot should be saved."
@ -325,7 +334,7 @@
(vec)))
(defn- send-notifications!
[{:keys [::db/conn] :as cfg} {:keys [file changes session-id] :as params}]
[cfg {:keys [file team changes session-id] :as params}]
(let [lchanges (filter library-change? changes)
msgbus (::mbus/msgbus cfg)]
@ -339,14 +348,12 @@
:changes changes})
(when (and (:is-shared file) (seq lchanges))
(let [team-id (or (:team-id file)
(files/get-team-id conn (:project-id file)))]
(mbus/pub! msgbus
:topic team-id
:message {:type :library-change
:profile-id (:profile-id params)
:file-id (:id file)
:session-id session-id
:revn (:revn file)
:modified-at (dt/now)
:changes lchanges})))))
(mbus/pub! msgbus
:topic (:id team)
:message {:type :library-change
:profile-id (:profile-id params)
:file-id (:id file)
:session-id session-id
:revn (:revn file)
:modified-at (dt/now)
:changes lchanges}))))

View file

@ -78,13 +78,13 @@
::audit/profile-id (:id profile)}))))))
(defn- login-or-register
[{:keys [::db/pool] :as cfg} info]
(db/with-atomic [conn pool]
(or (some->> (:email info)
(profile/get-profile-by-email conn)
(profile/decode-row))
(->> (assoc info :is-active true :is-demo false)
(auth/create-profile! conn)
(auth/create-profile-rels! conn)
(profile/strip-private-attrs)))))
[cfg info]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(or (some->> (:email info)
(profile/get-profile-by-email conn)
(profile/decode-row))
(->> (assoc info :is-active true :is-demo false)
(auth/create-profile! conn)
(auth/create-profile-rels! conn)
(profile/strip-private-attrs))))))

View file

@ -9,9 +9,9 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.files.migrations :as pmg]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.loggers.webhooks :as-alias webhooks]
@ -27,7 +27,6 @@
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[clojure.walk :as walk]
[promesa.exec :as px]))
@ -35,21 +34,16 @@
(declare duplicate-file)
(s/def ::id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::duplicate-file
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]
:opt-un [::name]))
(def ^:private schema:duplicate-file
[:map {:title "duplicate-file"}
[:file-id ::sm/uuid]
[:name {:optional true} :string]])
(sv/defmethod ::duplicate-file
"Duplicate a single file in the same team."
{::doc/added "1.16"
::webhooks/event? true}
::webhooks/event? true
::sm/params schema:duplicate-file}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(duplicate-file conn (assoc params :profile-id profile-id))))
@ -125,14 +119,14 @@
(files/persist-pointers! conn file-id)
data)))))))
(def sql:retrieve-used-libraries
(def sql:get-used-libraries
"select flr.*
from file_library_rel as flr
inner join file as l on (flr.library_file_id = l.id)
where flr.file_id = ?
and l.deleted_at is null")
(def sql:retrieve-used-media-objects
(def sql:get-used-media-objects
"select fmo.*
from file_media_object as fmo
inner join storage_object as so on (fmo.media_id = so.id)
@ -141,8 +135,8 @@
(defn duplicate-file*
[conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag]}]
(let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)]))
fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]))
(let [flibs (or flibs (db/exec! conn [sql:get-used-libraries (:id file)]))
fmeds (or fmeds (db/exec! conn [sql:get-used-media-objects (:id file)]))
;; memo uniform creation/modification date
now (dt/now)
@ -216,15 +210,16 @@
(declare duplicate-project)
(s/def ::duplicate-project
(s/keys :req [::rpc/profile-id]
:req-un [::project-id]
:opt-un [::name]))
(def ^:private schema:duplicate-project
[:map {:title "duplicate-project"}
[:project-id ::sm/uuid]
[:name {:optional true} :string]])
(sv/defmethod ::duplicate-project
"Duplicate an entire project with all the files"
{::doc/added "1.16"
::webhooks/event? true}
::webhooks/event? true
::sm/params schema:duplicate-project}
[{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool]
(duplicate-project conn (assoc params :profile-id (::rpc/profile-id params)))))
@ -275,7 +270,7 @@
;; --- COMMAND: Move file
(def sql:retrieve-files
(def sql:get-files
"select id, project_id from file where id = ANY(?)")
(def sql:move-files
@ -297,14 +292,19 @@
and rel.library_file_id = br.library_file_id")
(defn move-files
[conn {:keys [profile-id ids project-id] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [profile-id ids project-id] :as params}]
(let [fids (db/create-array conn "uuid" ids)
files (db/exec! conn [sql:retrieve-files fids])
files (db/exec! conn [sql:get-files fids])
source (into #{} (map :project-id) files)
pids (->> (conj source project-id)
(db/create-array conn "uuid"))]
(when (contains? source project-id)
(ex/raise :type :validation
:code :cant-move-to-same-project
:hint "Unable to move a file to the same project"))
;; Check if we have permissions on the destination project
(proj/check-edition-permissions! conn profile-id project-id)
@ -312,10 +312,10 @@
(doseq [project-id source]
(proj/check-edition-permissions! conn profile-id project-id))
(when (contains? source project-id)
(ex/raise :type :validation
:code :cant-move-to-same-project
:hint "Unable to move a file to the same project"))
;; Check the team compatibility
(let [orig-team (teams/get-team cfg :profile-id profile-id :project-id (first source))
dest-team (teams/get-team cfg :profile-id profile-id :project-id project-id)]
(cfeat/check-teams-compatibility! orig-team dest-team))
;; move all files to the project
(db/exec-one! conn [sql:move-files project-id fids])
@ -337,36 +337,41 @@
nil))
(s/def ::ids (s/every ::us/uuid :kind set?))
(s/def ::move-files
(s/keys :req [::rpc/profile-id]
:req-un [::ids ::project-id]))
(def ^:private schema:move-files
[:map {:title "move-files"}
[:ids ::sm/set-of-uuid]
[:project-id ::sm/uuid]])
(sv/defmethod ::move-files
"Move a set of files from one project to other."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(move-files conn (assoc params :profile-id profile-id))))
::webhooks/event? true
::sm/params schema:move-files}
[cfg {:keys [::rpc/profile-id] :as params}]
(db/tx-run! cfg #(move-files % (assoc params :profile-id profile-id))))
;; --- COMMAND: Move project
(defn move-project
[conn {:keys [profile-id team-id project-id] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id project-id] :as params}]
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})
pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]})
(map :id)
(db/create-array conn "uuid"))]
(teams/check-edition-permissions! conn profile-id (:team-id project))
(teams/check-edition-permissions! conn profile-id team-id)
(when (= team-id (:team-id project))
(ex/raise :type :validation
:code :cant-move-to-same-team
:hint "Unable to move a project to same team"))
(teams/check-edition-permissions! conn profile-id (:team-id project))
(teams/check-edition-permissions! conn profile-id team-id)
;; Check the teams compatibility
(let [orig-team (teams/get-team cfg :profile-id profile-id :team-id (:team-id project))
dest-team (teams/get-team cfg :profile-id profile-id :team-id team-id)]
(cfeat/check-teams-compatibility! orig-team dest-team))
;; move project to the destination team
(db/update! conn :project
{:team-id team-id}
@ -377,17 +382,18 @@
nil))
(s/def ::move-project
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::project-id]))
(def ^:private schema:move-project
[:map {:title "move-project"}
[:team-id ::sm/uuid]
[:project-id ::sm/uuid]])
(sv/defmethod ::move-project
"Move projects between teams."
"Move projects between teams"
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(move-project conn (assoc params :profile-id profile-id))))
::webhooks/event? true
::sm/params schema:move-project}
[cfg {:keys [::rpc/profile-id] :as params}]
(db/tx-run! cfg #(move-project % (assoc params :profile-id profile-id))))
;; --- COMMAND: Clone Template
@ -409,6 +415,7 @@
(dissoc ::db/conn)
(assoc ::binfile/input template)
(assoc ::binfile/project-id (:id project))
(assoc ::binfile/profile-id profile-id)
(assoc ::binfile/ignore-index-errors? true)
(assoc ::binfile/migrate? true)
(binfile/import!))))
@ -430,14 +437,6 @@
;; --- COMMAND: Get list of builtin templates
(s/def ::retrieve-list-of-builtin-templates any?)
(sv/defmethod ::retrieve-list-of-builtin-templates
{::doc/added "1.10"
::doc/deprecated "1.19"}
[cfg _params]
(mapv #(select-keys % [:id :name]) (::setup/templates cfg)))
(sv/defmethod ::get-builtin-templates
{::doc/added "1.19"}
[cfg _params]

View file

@ -60,7 +60,7 @@
(files/check-edition-permissions! pool profile-id file-id)
(media/validate-media-type! content)
(media/validate-media-size! content)
(let [object (create-file-media-object cfg params)
(let [object (db/run! cfg #(create-file-media-object % params))
props {:name (:name params)
:file-id file-id
:is-local (:is-local params)
@ -142,7 +142,7 @@
(assoc ::image (process-main-image info)))))
(defn create-file-media-object
[{:keys [::sto/storage ::db/pool] :as cfg}
[{:keys [::sto/storage ::db/conn] :as cfg}
{:keys [id file-id is-local name content]}]
(let [result (-> (climit/configure cfg :process-image)
@ -152,7 +152,7 @@
thumb (when-let [params (::thumb result)]
(sto/put-object! storage params))]
(db/exec-one! pool [sql:create-file-media-object
(db/exec-one! conn [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name
(:id image)
@ -176,9 +176,9 @@
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(create-file-media-object-from-url cfg params)))
(db/run! cfg #(create-file-media-object-from-url % params))))
(defn- download-image
(defn download-image
[{:keys [::http/client]} uri]
(letfn [(parse-and-validate [{:keys [headers] :as response}]
(let [size (some-> (get headers "content-length") d/parse-integer)
@ -209,7 +209,6 @@
{:method :get :uri uri}
{:response-type :input-stream :sync? true})
{:keys [size mtype]} (parse-and-validate response)
path (tmp/tempfile :prefix "penpot.media.download.")
written (io/write-to-file! body path :size size)]
@ -223,7 +222,6 @@
:path path
:mtype mtype})))
(defn- create-file-media-object-from-url
[cfg {:keys [url name] :as params}]
(let [content (download-image cfg url)

View file

@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.spec :as us]
@ -79,20 +80,25 @@
(def check-read-permissions!
(perms/make-check-fn has-read-permissions?))
(defn decode-row
[{:keys [features] :as row}]
(when row
(cond-> row
features (assoc :features (db/decode-pgarray features #{})))))
;; --- Query: Teams
(declare retrieve-teams)
(declare get-teams)
(def counter (volatile! 0))
(s/def ::get-teams
(s/keys :req [::rpc/profile-id]))
(def ^:private schema:get-teams
[:map {:title "get-teams"}])
(sv/defmethod ::get-teams
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:get-teams}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(dm/with-open [conn (db/open pool)]
(retrieve-teams conn profile-id)))
(get-teams conn profile-id)))
(def sql:teams
"select t.*,
@ -119,37 +125,65 @@
(dissoc :is-owner :is-admin :can-edit)
(assoc :permissions permissions))))
(defn retrieve-teams
(defn get-teams
[conn profile-id]
(let [profile (profile/get-profile conn profile-id)]
(->> (db/exec! conn [sql:teams (:default-team-id profile) profile-id])
(mapv process-permissions))))
(map decode-row)
(map process-permissions)
(vec))))
;; --- Query: Team (by ID)
(declare retrieve-team)
(declare get-team)
(s/def ::get-team
(s/keys :req [::rpc/profile-id]
:req-un [::id]))
(def ^:private schema:get-team
[:map {:title "get-team"}
[:id ::sm/uuid]])
(sv/defmethod ::get-team
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
(dm/with-open [conn (db/open pool)]
(retrieve-team conn profile-id id)))
{::doc/added "1.17"
::sm/params schema:get-team}
[cfg {:keys [::rpc/profile-id id]}]
(db/tx-run! cfg #(get-team % :profile-id profile-id :team-id id)))
(defn retrieve-team
[conn profile-id team-id]
(let [profile (profile/get-profile conn profile-id)
sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?")
result (db/exec-one! conn [sql (:default-team-id profile) profile-id team-id])]
(defn get-team
[conn & {:keys [profile-id team-id project-id file-id] :as params}]
(dm/assert!
"profile-id is mandatory"
(uuid? profile-id))
(let [{:keys [default-team-id] :as profile} (profile/get-profile conn profile-id)
result (cond
(some? team-id)
(let [sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?")]
(db/exec-one! conn [sql default-team-id profile-id team-id]))
(some? project-id)
(let [sql (str "WITH teams AS (" sql:teams ") "
"SELECT t.* FROM teams AS t "
" JOIN project AS p ON (p.team_id = t.id) "
" WHERE p.id=?")]
(db/exec-one! conn [sql default-team-id profile-id project-id]))
(some? file-id)
(let [sql (str "WITH teams AS (" sql:teams ") "
"SELECT t.* FROM teams AS t "
" JOIN project AS p ON (p.team_id = t.id) "
" JOIN file AS f ON (f.project_id = p.id) "
" WHERE f.id=?")]
(db/exec-one! conn [sql default-team-id profile-id file-id]))
:else
(throw (IllegalArgumentException. "invalid arguments")))]
(when-not result
(ex/raise :type :not-found
:code :team-does-not-exist))
(process-permissions result)))
(-> result
(decode-row)
(process-permissions))))
;; --- Query: Team Members
@ -165,44 +199,48 @@
join profile as p on (p.id = tp.profile_id)
where tp.team_id = ?")
(defn retrieve-team-members
(defn get-team-members
[conn team-id]
(db/exec! conn [sql:team-members team-id]))
(s/def ::team-id ::us/uuid)
(s/def ::get-team-members
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(def ^:private schema:get-team-memebrs
[:map {:title "get-team-members"}
[:team-id ::sm/uuid]])
(sv/defmethod ::get-team-members
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:get-team-memebrs}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(retrieve-team-members conn team-id)))
(get-team-members conn team-id)))
;; --- Query: Team Users
(declare retrieve-users)
(declare retrieve-team-for-file)
(declare get-users)
(declare get-team-for-file)
(s/def ::get-team-users
(s/and (s/keys :req [::rpc/profile-id]
:opt-un [::team-id ::file-id])
#(or (:team-id %) (:file-id %))))
(def ^:private schema:get-team-users
[:and {:title "get-team-users"}
[:map
[:team-id {:optional true} ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]]
[:fn #(or (contains? % :team-id)
(contains? % :file-id))]])
(sv/defmethod ::get-team-users
{::doc/added "1.17"}
"Get team users by team-id or by file-id"
{::doc/added "1.17"
::sm/params schema:get-team-users}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id]}]
(dm/with-open [conn (db/open pool)]
(if team-id
(do
(check-read-permissions! conn profile-id team-id)
(retrieve-users conn team-id))
(let [{team-id :id} (retrieve-team-for-file conn file-id)]
(get-users conn team-id))
(let [{team-id :id} (get-team-for-file conn file-id)]
(check-read-permissions! conn profile-id team-id)
(retrieve-users conn team-id)))))
(get-users conn team-id)))))
;; This is a similar query to team members but can contain more data
;; because some user can be explicitly added to project or file (not
@ -233,44 +271,44 @@
join file as f on (p.id = f.project_id)
where f.id = ?")
(defn retrieve-users
(defn get-users
[conn team-id]
(db/exec! conn [sql:team-users team-id team-id team-id]))
(defn retrieve-team-for-file
(defn get-team-for-file
[conn file-id]
(->> [sql:team-by-file file-id]
(db/exec-one! conn)))
;; --- Query: Team Stats
(declare retrieve-team-stats)
(declare get-team-stats)
(s/def ::get-team-stats
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(def ^:private schema:get-team-stats
[:map {:title "get-team-stats"}
[:team-id ::sm/uuid]])
(sv/defmethod ::get-team-stats
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:get-team-stats}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(retrieve-team-stats conn team-id)))
(get-team-stats conn team-id)))
(def sql:team-stats
"select (select count(*) from project where team_id = ?) as projects,
(select count(*) from file as f join project as p on (p.id = f.project_id) where p.team_id = ?) as files")
(defn retrieve-team-stats
(defn get-team-stats
[conn team-id]
(db/exec-one! conn [sql:team-stats team-id team-id]))
;; --- Query: Team invitations
(s/def ::get-team-invitations
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(def ^:private schema:get-team-invitations
[:map {:title "get-team-invitations"}
[:team-id ::sm/uuid]])
(def sql:team-invitations
"select email_to as email, role, (valid_until < now()) as expired
@ -282,7 +320,8 @@
(mapv #(update % :role keyword))))
(sv/defmethod ::get-team-invitations
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:get-team-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
@ -297,40 +336,50 @@
(declare ^:private create-team-role)
(declare ^:private create-team-default-project)
(s/def ::create-team
(s/keys :req [::rpc/profile-id]
:req-un [::name]
:opt-un [::id]))
(def ^:private schema:create-team
[:map {:title "create-team"}
[:name :string]
[:features {:optional true} ::cfeat/features]
[:id {:optional true} ::sm/uuid]])
(sv/defmethod ::create-team
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id})
{::doc/added "1.17"
::sm/params schema:create-team}
[cfg {:keys [::rpc/profile-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id})
(create-team conn (assoc params :profile-id profile-id))))
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))]
(create-team cfg (assoc params
:profile-id profile-id
:features features))))))
(defn create-team
"This is a complete team creation process, it creates the team
object and all related objects (default role and default project)."
[conn params]
(let [team (create-team* conn params)
[cfg-or-conn params]
(let [conn (db/get-connection cfg-or-conn)
team (create-team* conn params)
params (assoc params
:team-id (:id team)
:role :owner)
:team-id (:id team)
:role :owner)
project (create-team-default-project conn params)]
(create-team-role conn params)
(assoc team :default-project-id (:id project))))
(defn- create-team*
[conn {:keys [id name is-default] :as params}]
[conn {:keys [id name is-default features] :as params}]
(let [id (or id (uuid/next))
is-default (if (boolean? is-default) is-default false)]
(db/insert! conn :team
{:id id
:name name
:is-default is-default})))
is-default (if (boolean? is-default) is-default false)
features (db/create-array conn "text" features)
team (db/insert! conn :team
{:id id
:name name
:features features
:is-default is-default})]
(decode-row team)))
(defn- create-team-role
[conn {:keys [profile-id team-id role] :as params}]
@ -396,7 +445,7 @@
(defn leave-team
[conn {:keys [profile-id id reassign-to]}]
(let [perms (get-permissions conn profile-id id)
members (retrieve-team-members conn id)]
members (get-team-members conn id)]
(cond
;; we can only proceed if there are more members in the team
@ -480,10 +529,15 @@
(s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid)
(s/def ::role #{:owner :admin :editor})
;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/penpot/issue/1083
;; (s/def ::role #{:owner :admin :editor :viewer})
(s/def ::role #{:owner :admin :editor})
(def valid-roles
#{:owner :admin :editor #_:viewer})
(def schema:role
[::sm/one-of valid-roles])
(defn role->params
[role]
@ -500,7 +554,7 @@
;; convenience, if this becomes a bottleneck or problematic,
;; we will change it to more efficient fetch mechanisms.
(let [perms (get-permissions conn profile-id team-id)
members (retrieve-team-members conn team-id)
members (get-team-members conn team-id)
member (d/seek #(= member-id (:id %)) members)
is-owner? (:is-owner perms)
@ -596,7 +650,7 @@
(defn update-team-photo
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id team-id] :as params}]
(let [team (retrieve-team pool profile-id team-id)
(let [team (get-team pool profile-id team-id)
photo (profile/upload-photo cfg params)]
(db/with-atomic [conn pool]
@ -784,14 +838,24 @@
(s/merge ::create-team
(s/keys :req-un [::emails ::role])))
(def ^:private schema:create-team-with-invitations
[:map {:title "create-team-with-invitations"}
[:name :string]
[:features {:optional true} ::cfeat/features]
[:id {:optional true} ::sm/uuid]
[:emails ::sm/set-of-emails]
[:role schema:role]])
(sv/defmethod ::create-team-with-invitations
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:create-team-with-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [params (assoc params :profile-id profile-id)
team (create-team conn params)
profile (db/get-by-id conn :profile profile-id)
cfg (assoc cfg ::db/conn conn)]
cfg (assoc cfg ::db/conn conn)
team (create-team cfg params)
profile (db/get-by-id conn :profile profile-id)]
;; Create invitations for all provided emails.
(->> emails

View file

@ -8,6 +8,7 @@
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.schema :as sm]
[app.db :as db]
[app.rpc :as-alias rpc]
@ -83,7 +84,7 @@
[:map {:title "get-view-only-bundle"}
[:file-id ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]
[:features {:optional true} files/schema:features]])
[:features {:optional true} ::cfeat/features]])
(sv/defmethod ::get-view-only-bundle
{::rpc/auth false

View file

@ -12,7 +12,7 @@
[app.auth :refer [derive-password]]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.files.features :as ffeat]
[app.common.features :as cfeat]
[app.common.files.migrations :as pmg]
[app.common.logging :as l]
[app.common.pages :as cp]
@ -100,10 +100,10 @@
(binding [*conn* conn
pmap/*tracked* (atom {})
pmap/*load-fn* (partial files/load-pointer conn id)
ffeat/*wrap-with-pointer-map-fn*
(if (contains? (:features file) "storage/pointer-map") pmap/wrap identity)
ffeat/*wrap-with-objects-map-fn*
(if (contains? (:features file) "storage/objectd-map") omap/wrap identity)]
cfeat/*wrap-with-pointer-map-fn*
(if (contains? (:features file) "fdata/pointer-map") pmap/wrap identity)
cfeat/*wrap-with-objects-map-fn*
(if (contains? (:features file) "fdata/objectd-map") omap/wrap identity)]
(let [file (-> file
(update :data blob/decode)
(cond-> migrate? (update :data pmg/migrate-data))
@ -118,7 +118,7 @@
:features features}
{:id id})
(when (contains? (:features file) "storage/pointer-map")
(when (contains? (:features file) "fdata/pointer-map")
(files/persist-pointers! conn id))))
(dissoc file :data))))))
@ -161,10 +161,10 @@
(binding [*conn* conn
pmap/*tracked* (atom {})
pmap/*load-fn* (partial files/load-pointer conn (:id file))
ffeat/*wrap-with-pointer-map-fn*
(if (contains? (:features file) "storage/pointer-map") pmap/wrap identity)
ffeat/*wrap-with-objects-map-fn*
(if (contains? (:features file) "storage/objects-map") omap/wrap identity)]
cfeat/*wrap-with-pointer-map-fn*
(if (contains? (:features file) "fdata/pointer-map") pmap/wrap identity)
cfeat/*wrap-with-objects-map-fn*
(if (contains? (:features file) "fdata/objects-map") omap/wrap identity)]
(try
(on-file file)
(catch Throwable cause
@ -209,10 +209,10 @@
(binding [*conn* conn
pmap/*tracked* (atom {})
pmap/*load-fn* (partial files/load-pointer conn (:id file))
ffeat/*wrap-with-pointer-map-fn*
(if (contains? (:features file) "storage/pointer-map") pmap/wrap identity)
ffeat/*wrap-with-objects-map-fn*
(if (contains? (:features file) "storage/objectd-map") omap/wrap identity)]
cfeat/*wrap-with-pointer-map-fn*
(if (contains? (:features file) "fdata/pointer-map") pmap/wrap identity)
cfeat/*wrap-with-objects-map-fn*
(if (contains? (:features file) "fdata/objectd-map") omap/wrap identity)]
(on-file file))
(catch Throwable cause
((or on-error on-error*) cause file))))

View file

@ -10,16 +10,18 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.logging :as l]
[app.common.pprint :as p]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.features.fdata :as features.fdata]
[app.msgbus :as mbus]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
[app.rpc.commands.files-snapshot :as fsnap]
[app.rpc.commands.profile :as profile]
[app.srepl.fixes :as f]
[app.srepl.helpers :as h]
[app.storage :as sto]
@ -110,41 +112,57 @@
(defn enable-objects-map-feature-on-file!
[system & {:keys [save? id]}]
(letfn [(update-file [{:keys [features] :as file}]
(if (contains? features "storage/objects-map")
file
(-> file
(update :data migrate)
(update :features conj "storage/objects-map"))))
(migrate [data]
(-> data
(update :pages-index update-vals #(update % :objects omap/wrap))
(update :components update-vals #(update % :objects omap/wrap))))]
(h/update-file! system
:id id
:update-fn update-file
:save? save?)))
(h/update-file! system
:id id
:update-fn features.fdata/enable-objects-map
:save? save?))
(defn enable-pointer-map-feature-on-file!
[system & {:keys [save? id]}]
(letfn [(update-file [{:keys [features] :as file}]
(if (contains? features "storage/pointer-map")
file
(-> file
(update :data migrate)
(update :features conj "storage/pointer-map"))))
(h/update-file! system
:id id
:update-fn features.fdata/enable-pointer-map
:save? save?))
(migrate [data]
(-> data
(update :pages-index update-vals pmap/wrap)
(update :components pmap/wrap)))]
(defn enable-team-feature!
[system team-id feature]
(dm/verify!
"feature should be supported"
(contains? cfeat/supported-features feature))
(h/update-file! system
:id id
:update-fn update-file
:save? save?)))
(let [team-id (if (string? team-id)
(parse-uuid team-id)
team-id)]
(db/tx-run! system
(fn [{:keys [::db/conn]}]
(let [team (-> (db/get conn :team {:id team-id})
(update :features db/decode-pgarray #{}))
features (conj (:features team) feature)]
(when (not= features (:features team))
(db/update! conn :team
{:features (db/create-array conn "text" features)}
{:id team-id})
:enabled))))))
(defn disable-team-feature!
[system team-id feature]
(dm/verify!
"feature should be supported"
(contains? cfeat/supported-features feature))
(let [team-id (if (string? team-id)
(parse-uuid team-id)
team-id)]
(db/tx-run! system
(fn [{:keys [::db/conn]}]
(let [team (-> (db/get conn :team {:id team-id})
(update :features db/decode-pgarray #{}))
features (disj (:features team) feature)]
(when (not= features (:features team))
(db/update! conn :team
{:features (db/create-array conn "text" features)}
{:id team-id})
:disabled))))))
(defn enable-storage-features-on-file!
[system & {:as params}]

View file

@ -29,7 +29,7 @@
(defmethod ig/prep-key ::cleaner
[_ cfg]
(assoc cfg ::min-age (dt/duration "30m")))
(assoc cfg ::min-age (dt/duration "60m")))
(defmethod ig/init-key ::cleaner
[_ cfg]

View file

@ -298,7 +298,7 @@
(clean-file-thumbnails! cfg id revn)
(clean-deleted-components! conn id data)
(when (contains? features "storage/pointer-map")
(when (contains? features "fdata/pointer-map")
(clean-data-fragments! conn id data))
;; Mark file as trimmed

View file

@ -73,7 +73,7 @@
IPointerMap
(load! [_]
(l/trace :hint "pointer-map:load" :id id)
(l/trace :hint "pointer-map:load" :id (str id))
(when-not *load-fn*
(throw (UnsupportedOperationException. "load is not supported when *load-fn* is not bind")))

View file

@ -10,11 +10,12 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.flags :as flags]
[app.common.pages :as cp]
[app.common.pprint :as pp]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@ -66,8 +67,9 @@
:enable-email-verification
:enable-smtp
:enable-quotes
:enable-fdata-storage-pointer-map
:enable-fdata-storage-objets-map
:enable-feature-fdata-pointer-map
:enable-feature-fdata-objets-map
:enable-feature-components-v2
:disable-file-validation])
(def test-init-sql
@ -206,65 +208,72 @@
;; --- FACTORIES
(defn create-profile*
([i] (create-profile* *pool* i {}))
([i params] (create-profile* *pool* i params))
([pool i params]
([i] (create-profile* *system* i {}))
([i params] (create-profile* *system* i params))
([system i params]
(let [params (merge {:id (mk-uuid "profile" i)
:fullname (str "Profile " i)
:email (str "profile" i ".test@nodomain.com")
:password "123123"
:is-demo false}
params)]
(dm/with-open [conn (db/open pool)]
(->> params
(cmd.auth/create-profile! conn)
(cmd.auth/create-profile-rels! conn))))))
(db/run! system
(fn [{:keys [::db/conn]}]
(->> params
(cmd.auth/create-profile! conn)
(cmd.auth/create-profile-rels! conn)))))))
(defn create-project*
([i params] (create-project* *pool* i params))
([pool i {:keys [profile-id team-id] :as params}]
([i params] (create-project* *system* i params))
([system i {:keys [profile-id team-id] :as params}]
(us/assert uuid? profile-id)
(us/assert uuid? team-id)
(dm/with-open [conn (db/open pool)]
(->> (merge {:id (mk-uuid "project" i)
:name (str "project" i)}
params)
(#'teams/create-project conn)))))
(db/run! system
(fn [{:keys [::db/conn]}]
(->> (merge {:id (mk-uuid "project" i)
:name (str "project" i)}
params)
(#'teams/create-project conn))))))
(defn create-file*
([i params]
(create-file* *pool* i params))
([pool i {:keys [profile-id project-id] :as params}]
(us/assert uuid? profile-id)
(us/assert uuid? project-id)
(db/with-atomic [conn (db/open pool)]
(files.create/create-file conn
(merge {:id (mk-uuid "file" i)
:name (str "file" i)
:components-v2 true}
params)))))
(create-file* *system* i params))
([system i {:keys [profile-id project-id] :as params}]
(dm/assert! "expected uuid" (uuid? profile-id))
(dm/assert! "expected uuid" (uuid? project-id))
(db/run! system
(fn [system]
(let [features (cfeat/get-enabled-features cf/flags)]
(files.create/create-file system
(merge {:id (mk-uuid "file" i)
:name (str "file" i)
:features features}
params)))))))
(defn mark-file-deleted*
([params] (mark-file-deleted* *pool* params))
([params] (mark-file-deleted* *system* params))
([conn {:keys [id] :as params}]
(#'files/mark-file-deleted! conn {:id id})))
(defn create-team*
([i params] (create-team* *pool* i params))
([pool i {:keys [profile-id] :as params}]
([i params] (create-team* *system* i params))
([system i {:keys [profile-id] :as params}]
(us/assert uuid? profile-id)
(dm/with-open [conn (db/open pool)]
(let [id (mk-uuid "team" i)]
(dm/with-open [conn (db/open system)]
(let [id (mk-uuid "team" i)
features (cfeat/get-enabled-features cf/flags)]
(teams/create-team conn {:id id
:profile-id profile-id
:features features
:name (str "team" i)})))))
(defn create-file-media-object*
([params] (create-file-media-object* *pool* params))
([pool {:keys [name width height mtype file-id is-local media-id]
([params] (create-file-media-object* *system* params))
([system {:keys [name width height mtype file-id is-local media-id]
:or {name "sample" width 100 height 100 mtype "image/svg+xml" is-local true}}]
(dm/with-open [conn (db/open pool)]
(dm/with-open [conn (db/open system)]
(db/insert! conn :file-media-object
{:id (uuid/next)
:file-id file-id
@ -276,14 +285,14 @@
:mtype mtype}))))
(defn link-file-to-library*
([params] (link-file-to-library* *pool* params))
([pool {:keys [file-id library-id] :as params}]
(dm/with-open [conn (db/open pool)]
([params] (link-file-to-library* *system* params))
([system {:keys [file-id library-id] :as params}]
(dm/with-open [conn (db/open system)]
(#'files/link-file-to-library conn {:file-id file-id :library-id library-id}))))
(defn create-complaint-for
[pool {:keys [id created-at type]}]
(dm/with-open [conn (db/open pool)]
[system {:keys [id created-at type]}]
(dm/with-open [conn (db/open system)]
(db/insert! conn :profile-complaint-report
{:profile-id id
:created-at (or created-at (dt/now))
@ -291,8 +300,8 @@
:content (db/tjson {})})))
(defn create-global-complaint-for
[pool {:keys [email type created-at]}]
(dm/with-open [conn (db/open pool)]
[system {:keys [email type created-at]}]
(dm/with-open [conn (db/open system)]
(db/insert! conn :global-complaint-report
{:email email
:type (name type)
@ -300,71 +309,72 @@
:content (db/tjson {})})))
(defn create-team-role*
([params] (create-team-role* *pool* params))
([pool {:keys [team-id profile-id role] :or {role :owner}}]
(dm/with-open [conn (db/open pool)]
([params] (create-team-role* *system* params))
([system {:keys [team-id profile-id role] :or {role :owner}}]
(dm/with-open [conn (db/open system)]
(#'teams/create-team-role conn {:team-id team-id
:profile-id profile-id
:role role}))))
(defn create-project-role*
([params] (create-project-role* *pool* params))
([pool {:keys [project-id profile-id role] :or {role :owner}}]
(dm/with-open [conn (db/open pool)]
([params] (create-project-role* *system* params))
([system {:keys [project-id profile-id role] :or {role :owner}}]
(dm/with-open [conn (db/open system)]
(#'teams/create-project-role conn {:project-id project-id
:profile-id profile-id
:role role}))))
(defn create-file-role*
([params] (create-file-role* *pool* params))
([pool {:keys [file-id profile-id role] :or {role :owner}}]
(dm/with-open [conn (db/open pool)]
([params] (create-file-role* *system* params))
([system {:keys [file-id profile-id role] :or {role :owner}}]
(dm/with-open [conn (db/open system)]
(files.create/create-file-role! conn {:file-id file-id
:profile-id profile-id
:role role}))))
(defn update-file*
([params] (update-file* *pool* params))
([pool {:keys [file-id changes session-id profile-id revn]
([params] (update-file* *system* params))
([system {:keys [file-id changes session-id profile-id revn]
:or {session-id (uuid/next) revn 0}}]
(dm/with-open [conn (db/open pool)]
(let [features #{"components/v2"}
cfg (-> (select-keys *system* [::mbus/msgbus ::mtx/metrics])
(assoc ::db/conn conn))]
(files.update/update-file cfg
{:id file-id
:revn revn
:features features
:changes changes
:session-id session-id
:profile-id profile-id})))))
(db/tx-run! system (fn [{:keys [::db/conn] :as system}]
(let [file (files.update/get-file conn file-id)]
(files.update/update-file system
{:id file-id
:revn revn
:file file
:features (:features file)
:changes changes
:session-id session-id
:profile-id profile-id}))))))
(declare command!)
(defn update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
(let [params {::type :update-file
::rpc/profile-id profile-id
:id file-id
:session-id (uuid/random)
:revn revn
:components-v2 true
:changes changes}
out (command! params)]
(let [features (cfeat/get-enabled-features cf/flags)
params {::type :update-file
::rpc/profile-id profile-id
:id file-id
:session-id (uuid/random)
:revn revn
:features features
:changes changes}
out (command! params)]
(t/is (nil? (:error out)))
(:result out)))
(defn create-webhook*
([params] (create-webhook* *pool* params))
([pool {:keys [team-id id uri mtype is-active]
:or {is-active true
mtype "application/json"
uri "http://example.com/webhook"}}]
(db/insert! pool :webhook
{:id (or id (uuid/next))
:team-id team-id
:uri uri
:is-active is-active
:mtype mtype})))
([params] (create-webhook* *system* params))
([system {:keys [team-id id uri mtype is-active]
:or {is-active true
mtype "application/json"
uri "http://example.com/webhook"}}]
(db/run! system (fn [{:keys [::db/conn]}]
(db/insert! conn :webhook
{:id (or id (uuid/next))
:team-id team-id
:uri uri
:is-active is-active
:mtype mtype})))))
;; --- RPC HELPERS

View file

@ -6,6 +6,7 @@
(ns backend-tests.rpc-cond-middleware-test
(:require
[app.common.features :as cfeat]
[app.common.uuid :as uuid]
[app.db :as db]
[app.http :as http]
@ -27,7 +28,9 @@
:project-id (:id project)})
params {::th/type :get-file
:id (:id file1)
::rpc/profile-id (:id profile)}]
::rpc/profile-id (:id profile)
:features cfeat/supported-features
}]
(binding [cond/*enabled* true]
(let [{:keys [error result]} (th/command! params)]
@ -36,7 +39,7 @@
(t/is (contains? (meta result) :app.http/headers))
(t/is (contains? (meta result) :app.rpc.cond/key))
(let [etag (-> result meta :app.http/headers (get "etag"))
(let [etag (-> result meta :app.http/headers (get "etag"))
{:keys [error result]} (th/command! (assoc params ::cond/key etag))]
(t/is (nil? error))
(t/is (fn? result))

View file

@ -6,9 +6,14 @@
(ns backend-tests.rpc-file-test
(:require
[app.common.features :as cfeat]
[app.common.pprint :as pp]
[app.common.thumbnails :as thc]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.db :as db]
[app.db.sql :as sql]
[app.http :as http]
[app.rpc :as-alias rpc]
[app.storage :as sto]
[app.util.time :as dt]
@ -127,7 +132,7 @@
:id file-id
:session-id (uuid/random)
:revn revn
:components-v2 true
:features cfeat/supported-features
:changes changes}
out (th/command! params)]
;; (th/print-result! out)
@ -248,7 +253,7 @@
:id file-id
:session-id (uuid/random)
:revn revn
:components-v2 true
:features cfeat/supported-features
:changes changes}
out (th/command! params)]
;; (th/print-result! out)
@ -596,10 +601,11 @@
(let [data {::th/type :get-page
::rpc/profile-id (:id prof)
:file-id (:id file)
:components-v2 true}
:features cfeat/supported-features}
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (map? result))
(t/is (contains? result :objects))
(t/is (contains? (:objects result) frame1-id))
@ -614,7 +620,7 @@
::rpc/profile-id (:id prof)
:file-id (:id file)
:page-id page-id
:components-v2 true}
:features cfeat/supported-features}
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (map? result))
@ -631,7 +637,7 @@
:file-id (:id file)
:page-id page-id
:object-id frame1-id
:components-v2 true}
:features cfeat/supported-features}
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
@ -648,7 +654,7 @@
::rpc/profile-id (:id prof)
:file-id (:id file)
:object-id frame1-id
:components-v2 true}
:features cfeat/supported-features}
out (th/command! data)]
;; (th/print-result! out)
@ -675,9 +681,10 @@
(let [data {::th/type :get-file-data-for-thumbnail
::rpc/profile-id (:id prof)
:file-id (:id file)
:components-v2 true}
:features cfeat/supported-features}
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (map? result))
(t/is (contains? result :page))
(t/is (contains? result :revn))
@ -702,7 +709,7 @@
(let [data {::th/type :get-file-data-for-thumbnail
::rpc/profile-id (:id prof)
:file-id (:id file)
:components-v2 true}
:features cfeat/supported-features}
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (map? result))

View file

@ -622,9 +622,9 @@
(t/is (uuid? (first result)))
(t/is (= 1 (count result))))))
(t/deftest retrieve-list-of-buitin-templates
(t/deftest get-list-of-buitin-templates
(let [prof (th/create-profile* 1 {:is-active true})
data {::th/type :retrieve-list-of-builtin-templates
data {::th/type :get-builtin-templates
::rpc/profile-id (:id prof)}
out (th/command! data)]
;; (th/print-result! out)

View file

@ -28,7 +28,9 @@
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2022.06.16-403"}
funcool/promesa {:mvn/version "11.0.678"}
funcool/promesa {:git/sha "658c429c56c11c33da7594fa2ef53f4e6afedac4"
:git/url "https://github.com/funcool/promesa"}
funcool/datoteka {:mvn/version "3.0.66"
:exclusions [funcool/promesa]}

View file

@ -0,0 +1,236 @@
;; 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.features
(:require
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.schema.desc-js-like :as-alias smdj]
[app.common.schema.generators :as smg]
[clojure.set :as set]
[cuerdas.core :as str]))
;; The default behavior when a user interacts with penpot and runtime
;; and global features:
;;
;; - If user enables on runtime a frontend-only feature, this feature
;; and creates and/or modifies files, the feature is only availble
;; until next refresh (it is not persistent)
;;
;; - If user enables on runtime a non-migration feature, on modifying
;; a file or creating a new one, the feature becomes persistent on
;; the file and the team. All the other files of the team eventually
;; will have that feature assigned (on file modification)
;;
;; - If user enables on runtime a migration feature, that feature will
;; be ignored until a migration is explicitly executed or team
;; explicitly marked with that feature.
;;
;; The features stored on the file works as metadata information about
;; features enabled on the file and for compatibility check when a
;; user opens the file. The features stored on global, runtime or team
;; works as activators.
(def ^:dynamic *previous* #{})
(def ^:dynamic *current* #{})
(def ^:dynamic *new* nil)
(def ^:dynamic *wrap-with-objects-map-fn* identity)
(def ^:dynamic *wrap-with-pointer-map-fn* identity)
;; A set of supported features
(def supported-features
#{"fdata/objects-map"
"fdata/pointer-map"
"fdata/shape-data-type"
"components/v2"
"styles/v2"
"layout/grid"})
;; A set of features enabled by default for each file, they are
;; implicit and are enabled by default and can't be disabled
(def default-enabled-features
#{"fdata/shape-data-type"})
;; A set of features which only affects on frontend and can be enabled
;; and disabled freely by the user any time. This features does not
;; persist on file features field but can be permanently enabled on
;; team feature field
(def frontend-only-features
#{"styles/v2"})
;; Features that are mainly backend only or there are a proper
;; fallback when frontend reports no support for it
(def backend-only-features
#{"fdata/objects-map"
"fdata/pointer-map"})
;; This is a set of features that does not require an explicit
;; migration like components/v2 or the migration is not mandatory to
;; be applied (per example backend can operate in both modes with or
;; without migration applied)
(def no-migration-features
(-> #{"fdata/objects-map"
"fdata/pointer-map"
"layout/grid"}
(into frontend-only-features)))
(sm/def! ::features
[:schema
{:title "FileFeatures"
::smdj/inline true
:gen/gen (smg/subseq supported-features)}
::sm/set-of-strings])
(defn- flag->feature
"Translate a flag to a feature name"
[flag]
(case flag
:feature-components-v2 "components/v2"
:feature-new-css-system "styles/v2"
:feature-grid-layout "layout/grid"
:feature-fdata-objects-map "fdata/objects-map"
:feature-fdata-pointer-map "fdata/pointer-map"
nil))
(def xf-supported-features
(filter (partial contains? supported-features)))
(def xf-remove-ephimeral
(remove #(str/starts-with? % "ephimeral/")))
(def xf-flag-to-feature
(keep flag->feature))
(defn get-enabled-features
"Get the globally enabled fratures set."
[flags]
(into default-enabled-features xf-flag-to-feature flags))
(defn get-team-enabled-features
"Get the team enabled features.
Team features are defined as: all features found on team plus all
no-migration features enabled globally."
[flags team]
(let [enabled-features (into #{} xf-flag-to-feature flags)
team-features (into #{} xf-remove-ephimeral (:features team))]
(-> enabled-features
(set/intersection no-migration-features)
(set/union default-enabled-features)
(set/union team-features))))
(defn check-client-features!
"Function used for check feature compability between currently enabled
features set on backend with the enabledq featured set by the
frontend client"
[enabled-features client-features]
(when (set? client-features)
(let [not-supported (-> enabled-features
(set/difference client-features)
(set/difference backend-only-features))]
(when (seq not-supported)
(ex/raise :type :restriction
:code :feature-not-supported
:feature (first not-supported)
:hint (str/ffmt "client declares no support for '%' features"
(str/join "," not-supported)))))
(let [not-supported (set/difference client-features supported-features)]
(when (seq not-supported)
(ex/raise :type :restriction
:code :feature-not-supported
:feature (first not-supported)
:hint (str/ffmt "backend does not support '%' features requested by client"
(str/join "," not-supported))))))
enabled-features)
(defn check-supported-features!
"Check if a given set of features are supported by this
backend. Usually used for check if imported file features are
supported by the current backend"
[enabled-features]
(let [not-supported (set/difference enabled-features supported-features)]
(when (seq not-supported)
(ex/raise :type :restriction
:code :features-mismatch
:feature (first not-supported)
:hint (str/ffmt "features '%' not supported"
(str/join "," not-supported)))))
enabled-features)
(defn check-file-features!
"Function used for check feature compability between currently
enabled features set on backend with the provided featured set by
the penpot file"
([enabled-features file-features]
(check-file-features! enabled-features file-features #{}))
([enabled-features file-features client-features]
(let [file-features (into #{} xf-remove-ephimeral file-features)]
(let [not-supported (-> enabled-features
(set/union client-features)
(set/difference file-features)
;; NOTE: we don't want to raise a feature-mismatch
;; exception for features which don't require an
;; explicit file migration process or has no real
;; effect on file data structure
(set/difference no-migration-features))]
(when (seq not-supported)
(ex/raise :type :restriction
:code :file-feature-mismatch
:feature (first not-supported)
:hint (str/ffmt "enabled features '%' not present in file (missing migration)"
(str/join "," not-supported)))))
(check-supported-features! file-features)
(let [not-supported (-> file-features
(set/difference enabled-features)
(set/difference client-features)
(set/difference frontend-only-features))]
(when (seq not-supported)
(ex/raise :type :restriction
:code :feature-mismatch
:feature (first not-supported)
:hint (str/ffmt "file features '%' not enabled"
(str/join "," not-supported))))))
enabled-features))
(defn check-teams-compatibility!
[{source-features :features} {destination-features :features}]
(when (contains? source-features "ephimeral/migration")
(ex/raise :type :restriction
:code :migration-in-progress
:hint "the source team is in migration process"))
(when (contains? destination-features "ephimeral/migration")
(ex/raise :type :restriction
:code :migration-in-progress
:hint "the destination team is in migration process"))
(let [not-supported (-> (or source-features #{})
(set/difference destination-features)
(set/difference no-migration-features)
(seq))]
(when not-supported
(ex/raise :type :restriction
:code :team-feature-mismatch
:feature (first not-supported)
:hint (str/ffmt "the destination team does not have support '%' features"
(str/join "," not-supported)))))
(let [not-supported (-> (or destination-features #{})
(set/difference source-features)
(set/difference no-migration-features)
(seq))]
(when not-supported
(ex/raise :type :restriction
:code :team-feature-mismatch
:feature (first not-supported)
:hint (str/ffmt "the source team does not have support '%' features"
(str/join "," not-supported))))))

View file

@ -6,4 +6,4 @@
(ns app.common.files.defaults)
(def version 34)
(def version 35)

View file

@ -1,17 +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
(ns app.common.files.features)
;; A set of enabled by default file features. Will be used in feature
;; negotiation on obtaining files from backend.
(def enabled #{})
(def ^:dynamic *previous* #{})
(def ^:dynamic *current* #{})
(def ^:dynamic *wrap-with-objects-map-fn* identity)
(def ^:dynamic *wrap-with-pointer-map-fn* identity)

View file

@ -8,6 +8,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.files.defaults :refer [version]]
[app.common.geom.matrix :as gmt]
[app.common.geom.rect :as grc]
@ -441,6 +442,7 @@
(defmethod migrate 25
[data]
(some-> cfeat/*new* (swap! conj "fdata/shape-data-type"))
(letfn [(update-object [object]
(-> object
(d/update-when :selrect grc/make-rect)
@ -594,6 +596,7 @@
(defmethod migrate 32
[data]
(some-> cfeat/*new* (swap! conj "fdata/shape-data-type"))
(letfn [(update-object [object]
(as-> object object
(if (contains? object :svg-attrs)
@ -638,3 +641,13 @@
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
;; NOTE: We need to repeat this migration for correct feature handling
(defmethod migrate 35
[data]
(-> data
(assoc :version 25)
(migrate)
(assoc :version 35)))

View file

@ -8,7 +8,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.features :as cfeat]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
@ -75,7 +75,7 @@
(defn with-objects
[changes objects]
(let [fdata (binding [ffeat/*current* #{"components/v2"}]
(let [fdata (binding [cfeat/*current* #{"components/v2"}]
(ctf/make-file-data (uuid/next) uuid/zero))
fdata (assoc-in fdata [:pages-index uuid/zero :objects] objects)]
(vary-meta changes assoc

View file

@ -1045,7 +1045,9 @@
(let [redfn (fn [acc {:keys [tag attrs]}]
(cond-> acc
(= :image tag)
(conj (or (:href attrs) (:xlink:href attrs)))))]
(conj {:href (or (:href attrs) (:xlink:href attrs))
:width (d/parse-integer (:width attrs) 0)
:height (d/parse-integer (:height attrs) 0)})))]
(reduce-nodes redfn [] svg-data )))
#?(:cljs

View file

@ -8,7 +8,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.features :as cfeat]
[app.common.time :as dt]
[app.common.types.component :as ctk]))
@ -40,7 +40,7 @@
(cond-> (update-in fdata [:components id] assoc :main-instance-id main-instance-id :main-instance-page main-instance-page)
annotation (update-in [:components id] assoc :annotation annotation))
(let [wrap-object-fn ffeat/*wrap-with-objects-map-fn*]
(let [wrap-object-fn cfeat/*wrap-with-objects-map-fn*]
(assoc-in fdata [:components id :objects]
(->> shapes
(d/index-by :id)
@ -48,7 +48,7 @@
(defn mod-component
[file-data {:keys [id name path main-instance-id main-instance-page objects annotation]}]
(let [wrap-objects-fn ffeat/*wrap-with-objects-map-fn*]
(let [wrap-objects-fn cfeat/*wrap-with-objects-map-fn*]
(d/update-in-when file-data [:components id]
(fn [component]
(let [objects (some-> objects wrap-objects-fn)]

View file

@ -8,8 +8,8 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.files.defaults :refer [version]]
[app.common.files.features :as ffeat]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.logging :as l]
@ -57,7 +57,6 @@
[:map-of {:gen/max 2} ::sm/uuid ::cty/typography]]
[:media {:optional true}
[:map-of {:gen/max 5} ::sm/uuid ::media-object]]])
(def file-data?
(sm/pred-fn ::data))
@ -82,11 +81,11 @@
(let [page (when (some? page-id)
(ctp/make-empty-page page-id "Page 1"))]
(cond-> (assoc empty-file-data :id file-id)
(cond-> (assoc empty-file-data :id file-id :version version)
(some? page-id)
(ctpl/add-page page)
(contains? ffeat/*current* "components/v2")
(contains? cfeat/*current* "components/v2")
(assoc-in [:options :components-v2] true)))))
;; Helpers
@ -610,7 +609,7 @@
(-> file-data
(update :pages-index update-vals fix-container)
(update :components update-vals fix-container))))
fix-copies-of-detached
(fn [file-data]
; Find any copy that is referencing a detached shape inside a component, and
@ -1012,7 +1011,7 @@
(-> libs-to-show
(add-component library-id component-id))))))
;; (find-used-components-cumulative page root)
libs-to-show
components))
libs-to-show

View file

@ -7,7 +7,7 @@
(ns app.common.types.page
(:require
[app.common.data :as d]
[app.common.files.features :as ffeat]
[app.common.features :as cfeat]
[app.common.schema :as sm]
[app.common.types.color :as-alias ctc]
[app.common.types.grid :as ctg]
@ -74,8 +74,8 @@
(defn make-empty-page
[id name]
(let [wrap-objects-fn ffeat/*wrap-with-objects-map-fn*
wrap-pointer-fn ffeat/*wrap-with-pointer-map-fn*]
(let [wrap-objects-fn cfeat/*wrap-with-objects-map-fn*
wrap-pointer-fn cfeat/*wrap-with-pointer-map-fn*]
(-> empty-page-data
(assoc :id id)
(assoc :name name)

View file

@ -428,24 +428,22 @@
(defn generate-shape-grid
"Generate a sequence of positions that lays out the list of
shapes in a grid of equal-sized rows and columns."
[shapes start-pos gap]
(let [shapes-bounds (map gsh/bounding-box shapes)
[shapes start-position gap]
(when (seq shapes)
(let [bounds (map gsh/bounding-box shapes)
grid-size (mth/ceil (mth/sqrt (count shapes)))
row-size (+ (apply max (map :height shapes-bounds))
gap)
column-size (+ (apply max (map :width shapes-bounds))
gap)
grid-size (-> shapes count mth/sqrt mth/ceil)
row-size (+ (reduce d/max ##-Inf (map :height bounds)) gap)
column-size (+ (reduce d/max ##-Inf (map :width bounds)) gap)
next-pos (fn [position]
(let [counter (inc (:counter (meta position)))
row (quot counter grid-size)
column (mod counter grid-size)
new-pos (gpt/add start-pos
(gpt/point (* column column-size)
(* row row-size)))]
(with-meta new-pos
{:counter counter})))]
(iterate next-pos
(with-meta start-pos
{:counter 0}))))
get-next (fn get-next
[counter]
(let [row (quot counter grid-size)
column (mod counter grid-size)
position (->> (gpt/point (* column column-size)
(* row row-size))
(gpt/add start-position))]
(lazy-seq
(cons position (get-next (inc counter))))))]
(get-next 0))))

View file

@ -7,7 +7,6 @@
(ns app.main.data.common
"A general purpose events."
(:require
[app.common.files.features :as ffeat]
[app.common.types.components-list :as ctkl]
[app.config :as cf]
[app.main.data.messages :as msg]
@ -90,9 +89,7 @@
(ptk/reify ::show-shared-dialog
ptk/WatchEvent
(watch [_ state _]
(let [features (cond-> ffeat/enabled
(features/active-feature? state :components-v2)
(conj "components/v2"))
(let [features (features/get-team-enabled-features state)
data (:workspace-data state)
file (:workspace-file state)]
(->> (if (and data file)

View file

@ -8,6 +8,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.files.helpers :as cfh]
[app.common.schema :as sm]
[app.common.uri :as u]
@ -28,6 +29,7 @@
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[clojure.set :as set]
[potok.core :as ptk]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -43,11 +45,10 @@
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(du/set-current-team! id)
(let [prev-team-id (:current-team-id state)]
(cond-> state
(not= prev-team-id id)
(-> (assoc :current-team-id id)
(-> (dissoc :current-team-id)
(dissoc :dashboard-files)
(dissoc :dashboard-projects)
(dissoc :dashboard-shared-files)
@ -58,27 +59,36 @@
ptk/WatchEvent
(watch [_ state stream]
(rx/concat
(rx/of (features/initialize))
(rx/merge
;; fetch teams must be first in case the team doesn't exist
(ptk/watch (du/fetch-teams) state stream)
(ptk/watch (df/load-team-fonts id) state stream)
(ptk/watch (fetch-projects) state stream)
(ptk/watch (fetch-team-members) state stream)
(ptk/watch (du/fetch-users {:team-id id}) state stream)
(let [stoper-s (rx/filter (ptk/type? ::finalize) stream)
profile-id (:profile-id state)]
(let [stoper (rx/filter (ptk/type? ::finalize) stream)
profile-id (:profile-id state)]
(->> stream
(rx/filter (ptk/type? ::dws/message))
(rx/map deref)
(rx/filter (fn [{:keys [subs-id type] :as msg}]
(and (or (= subs-id uuid/zero)
(= subs-id profile-id))
(= :notification type))))
(rx/map handle-notification)
(rx/take-until stoper))))))))
(->> (rx/merge
;; fetch teams must be first in case the team doesn't exist
(ptk/watch (du/fetch-teams) state stream)
(ptk/watch (df/load-team-fonts id) state stream)
(ptk/watch (fetch-projects id) state stream)
(ptk/watch (fetch-team-members id) state stream)
(ptk/watch (du/fetch-users {:team-id id}) state stream)
(->> stream
(rx/filter (ptk/type? ::dws/message))
(rx/map deref)
(rx/filter (fn [{:keys [subs-id type] :as msg}]
(and (or (= subs-id uuid/zero)
(= subs-id profile-id))
(= :notification type))))
(rx/map handle-notification))
;; Once the teams are fecthed, initialize features related
;; to currently active team
(->> stream
(rx/filter (ptk/type? ::du/teams-fetched))
(rx/observe-on :async)
(rx/mapcat deref)
(rx/filter #(= id (:id %)))
(rx/map du/set-current-team)))
(rx/take-until stoper-s))))))
(defn finalize
[params]
@ -98,13 +108,12 @@
(assoc state :dashboard-team-members (d/index-by :id members)))))
(defn fetch-team-members
[]
[team-id]
(ptk/reify ::fetch-team-members
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-team-members {:team-id team-id})
(rx/map team-members-fetched))))))
(watch [_ _ _]
(->> (rp/cmd! :get-team-members {:team-id team-id})
(rx/map team-members-fetched)))))
;; --- EVENT: fetch-team-stats
@ -116,13 +125,12 @@
(assoc state :dashboard-team-stats stats))))
(defn fetch-team-stats
[]
[team-id]
(ptk/reify ::fetch-team-stats
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-team-stats {:team-id team-id})
(rx/map team-stats-fetched))))))
(watch [_ _ _]
(->> (rp/cmd! :get-team-stats {:team-id team-id})
(rx/map team-stats-fetched)))))
;; --- EVENT: fetch-team-invitations
@ -171,13 +179,12 @@
(assoc state :dashboard-projects projects)))))
(defn fetch-projects
[]
[team-id]
(ptk/reify ::fetch-projects
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-projects {:team-id team-id})
(rx/map projects-fetched))))))
(watch [_ _ _]
(->> (rp/cmd! :get-projects {:team-id team-id})
(rx/map projects-fetched)))))
;; --- EVENT: search
@ -344,11 +351,12 @@
(dm/assert! (string? name))
(ptk/reify ::create-team
ptk/WatchEvent
(watch [_ _ _]
(watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :create-team {:name name})
on-error rx/throw}} (meta params)
features (features/get-enabled-features state)]
(->> (rp/cmd! :create-team {:name name :features features})
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
@ -359,13 +367,15 @@
[{:keys [name emails role] :as params}]
(ptk/reify ::create-team-with-invitations
ptk/WatchEvent
(watch [_ _ _]
(watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
params {:name name
:emails #{emails}
:role role}]
features (features/get-enabled-features state)]
params {:name name
:emails #{emails}
:role role
:features features}
(->> (rp/cmd! :create-team-with-invitations params)
(rx/tap on-success)
(rx/map team-created)
@ -419,7 +429,7 @@
params (assoc params :team-id team-id)]
(->> (rp/cmd! :update-team-member-role params)
(rx/mapcat (fn [_]
(rx/of (fetch-team-members)
(rx/of (fetch-team-members team-id)
(du/fetch-teams)))))))))
(defn delete-team-member
@ -432,7 +442,7 @@
params (assoc params :team-id team-id)]
(->> (rp/cmd! :delete-team-member params)
(rx/mapcat (fn [_]
(rx/of (fetch-team-members)
(rx/of (fetch-team-members team-id)
(du/fetch-teams)))))))))
(defn leave-team
@ -846,9 +856,8 @@
files (get state :dashboard-files)
unames (cfh/get-used-names files)
name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))
features (cond-> #{}
(features/active-feature? state :components-v2)
(conj "components/v2"))
features (-> (features/get-team-enabled-features state)
(set/difference cfeat/frontend-only-features))
params (-> params
(assoc :name name)
(assoc :features features))]

View file

@ -16,6 +16,7 @@
[app.main.data.events :as ev]
[app.main.data.media :as di]
[app.main.data.websocket :as ws]
[app.main.features :as features]
[app.main.repo :as rp]
[app.util.i18n :as i18n]
[app.util.router :as rt]
@ -56,21 +57,20 @@
(defn teams-fetched
[teams]
(let [teams (d/index-by :id teams)
ids (into #{} (keys teams))]
(ptk/reify ::teams-fetched
IDeref
(-deref [_] teams)
(ptk/reify ::teams-fetched
IDeref
(-deref [_] teams)
ptk/UpdateEvent
(update [_ state]
(assoc state :teams (d/index-by :id teams)))
ptk/UpdateEvent
(update [_ state]
(assoc state :teams teams))
ptk/EffectEvent
(effect [_ _ _]
;; Check if current team-id is part of available teams
;; if not, dissoc it from storage.
ptk/EffectEvent
(effect [_ _ _]
;; Check if current team-id is part of available teams
;; if not, dissoc it from storage.
(let [ids (into #{} (map :id) teams)]
(when-let [ctid (::current-team-id @storage)]
(when-not (contains? ids ctid)
(swap! storage dissoc ::current-team-id)))))))
@ -83,6 +83,23 @@
(->> (rp/cmd! :get-teams)
(rx/map teams-fetched)))))
(defn set-current-team
[team]
(ptk/reify ::set-current-team
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :team team)
(assoc :current-team-id (:id team))))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (features/initialize (:features team #{}))))
ptk/EffectEvent
(effect [_ _ _]
(set-current-team! (:id team)))))
;; --- EVENT: fetch-profile
(declare logout)

View file

@ -8,7 +8,6 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.geom.point :as gpt]
[app.common.pages.helpers :as cph]
[app.common.schema :as sm]
@ -108,12 +107,8 @@
(ptk/reify ::fetch-bundle
ptk/WatchEvent
(watch [_ state _]
(let [features (cond-> ffeat/enabled
(features/active-feature? state :components-v2)
(conj "components/v2")
(let [features (features/get-team-enabled-features state)
:always
(conj "storage/pointer-map"))
params' (cond-> {:file-id file-id :features features}
(uuid? share-id)
(assoc :share-id share-id))

View file

@ -9,17 +9,13 @@
[app.common.attrs :as attrs]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.files.helpers :as cfh]
[app.common.files.libraries-helpers :as cflh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.align :as gal]
[app.common.geom.point :as gpt]
[app.common.geom.proportions :as gpp]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.grid-layout :as gslg]
[app.common.logging :as log]
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.text :as txt]
@ -27,8 +23,6 @@
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
@ -39,7 +33,6 @@
[app.main.data.events :as ev]
[app.main.data.fonts :as df]
[app.main.data.messages :as msg]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.changes :as dch]
@ -94,18 +87,12 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private workspace-initialized)
(declare ^:private remove-graphics)
(declare ^:private libraries-fetched)
;; --- Initialize Workspace
(defn initialize-layout
[lname]
;; (dm/assert!
;; "expected valid layout"
;; (and (keyword? lname)
;; (contains? layout/presets lname)))
(ptk/reify ::initialize-layout
ptk/UpdateEvent
(update [_ state]
@ -129,18 +116,10 @@
(assoc :workspace-ready? true)))
ptk/WatchEvent
(watch [_ state _]
(let [file (:workspace-data state)
has-graphics? (-> file :media seq)
components-v2 (features/active-feature? state :components-v2)]
(rx/merge
(rx/of (fbc/fix-bool-contents)
(fdf/fix-deleted-fonts)
(fbs/fix-broken-shapes))
(if (and has-graphics? components-v2)
(rx/of (remove-graphics (:id file) (:name file)))
(rx/empty)))))))
(watch [_ _ _]
(rx/of (fbc/fix-bool-contents)
(fdf/fix-deleted-fonts)
(fbs/fix-broken-shapes)))))
(defn- workspace-data-loaded
[data]
@ -171,39 +150,43 @@
(assoc data :pages-index pages-index))))))
(defn- bundle-fetched
[features [{:keys [id data] :as file} thumbnails project users comments-users]]
[{:keys [features file thumbnails project team team-users comments-users]}]
(ptk/reify ::bundle-fetched
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :users (d/index-by :id team-users))
(assoc :workspace-thumbnails thumbnails)
(assoc :workspace-file (dissoc file :data))
(assoc :workspace-project project)
(assoc :current-team-id (:team-id project))
(assoc :users (d/index-by :id users))
(assoc :current-file-comments-users (d/index-by :id comments-users))))
ptk/WatchEvent
(watch [_ _ stream]
(let [team-id (:team-id project)
stoper (rx/filter (ptk/type? ::bundle-fetched) stream)]
(let [team-id (:id team)
file-id (:id file)
file-data (:data file)
stoper-s (rx/filter (ptk/type? ::bundle-fetched) stream)]
(->> (rx/concat
;; Initialize notifications
(rx/of (dwn/initialize team-id id)
(rx/of (dwn/initialize team-id file-id)
(dwsl/initialize))
;; Load team fonts. We must ensure custom fonts are
;; fully loadad before mark workspace as initialized
(rx/merge
(->> stream
(rx/filter (ptk/type? :app.main.data.fonts/team-fonts-loaded))
(rx/filter (ptk/type? ::df/team-fonts-loaded))
(rx/take 1)
(rx/ignore))
(rx/of (df/load-team-fonts team-id))
;; FIXME: move to bundle fetch stages
;; Load main file
(->> (resolve-file-data id data)
(->> (resolve-file-data file-id file-data)
(rx/mapcat (fn [{:keys [pages-index] :as data}]
(->> (rx/from (seq pages-index))
(rx/mapcat
@ -217,7 +200,7 @@
(rx/map workspace-data-loaded))
;; Load libraries
(->> (rp/cmd! :get-file-libraries {:file-id id})
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat identity)
(rx/merge-map
(fn [{:keys [id synced-at]}]
@ -233,8 +216,10 @@
(rx/map #(assoc file :thumbnails %)))))
(rx/reduce conj [])
(rx/map libraries-fetched)))
(rx/of (with-meta (workspace-initialized) {:file-id id})))
(rx/take-until stoper))))))
(rx/of (with-meta (workspace-initialized)
{:file-id file-id})))
(rx/take-until stoper-s))))))
(defn- libraries-fetched
[libraries]
@ -255,7 +240,7 @@
(rx/concat (rx/timer 1000)
(rx/of (dwl/notify-sync-file file-id))))))))
(defn- fetch-thumbnail-blob-uri
(defn- datauri->blob-uri
[uri]
(->> (http/send! {:uri uri
:response-type :blob
@ -263,47 +248,86 @@
(rx/map :body)
(rx/map (fn [blob] (wapi/create-uri blob)))))
(defn- fetch-thumbnail-blobs
(defn- fetch-file-object-thumbnails
[file-id]
(->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
(rx/mapcat (fn [thumbnails]
(->> (rx/from thumbnails)
(rx/mapcat (fn [[k v]]
;; we only need to fetch the thumbnail if
;; it is a data:uri, otherwise we can just
;; use the value as is.
(if (.startsWith v "data:")
(->> (fetch-thumbnail-blob-uri v)
(rx/map (fn [uri] [k uri])))
(rx/of [k v])))))))
(->> (rx/from thumbnails)
(rx/mapcat (fn [[k v]]
;; we only need to fetch the thumbnail if
;; it is a data:uri, otherwise we can just
;; use the value as is.
(if (str/starts-with? v "data:")
(->> (datauri->blob-uri v)
(rx/map (fn [uri] [k uri])))
(rx/of [k v])))))))
(rx/reduce conj {})))
(defn- fetch-bundle
(defn- fetch-bundle-stage-1
[project-id file-id]
(ptk/reify ::fetch-bundle
(ptk/reify ::fetch-bundle-stage-1
ptk/WatchEvent
(watch [_ _ stream]
(->> (rp/cmd! :get-project {:id project-id})
(rx/mapcat (fn [project]
(->> (rp/cmd! :get-team {:id (:team-id project)})
(rx/mapcat (fn [team]
(let [bundle {:team team
:project project
:file-id file-id
:project-id project-id}]
(rx/of (du/set-current-team team)
(ptk/data-event ::bundle-stage-1 bundle))))))))
(rx/take-until
(rx/filter (ptk/type? ::fetch-bundle) stream))))))
(defn- fetch-bundle-stage-2
[{:keys [file-id project-id] :as bundle}]
(ptk/reify ::fetch-bundle-stage-2
ptk/WatchEvent
(watch [_ state stream]
(let [features (cond-> ffeat/enabled
(features/active-feature? state :components-v2)
(conj "components/v2")
;; We still put the feature here and not in the
;; ffeat/enabled var because the pointers map is only
;; supported on workspace bundle fetching mechanism.
:always
(conj "storage/pointer-map"))
(let [features (features/get-team-enabled-features state)
;; WTF is this?
share-id (-> state :viewer-local :share-id)
stoper (rx/filter (ptk/type? ::fetch-bundle) stream)]
share-id (-> state :viewer-local :share-id)]
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features :project-id project-id})
(fetch-thumbnail-blobs file-id)
(rp/cmd! :get-project {:id project-id})
(fetch-file-object-thumbnails file-id)
(rp/cmd! :get-team-users {:file-id file-id})
(rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id}))
(rx/take 1)
(rx/map (partial bundle-fetched features))
(rx/take-until stoper))))))
(rx/map (fn [[file thumbnails team-users comments-users]]
(let [bundle (-> bundle
(assoc :file file)
(assoc :thumbnails thumbnails)
(assoc :team-users team-users)
(assoc :comments-users comments-users))]
(ptk/data-event ::bundle-stage-2 bundle))))
(rx/take-until
(rx/filter (ptk/type? ::fetch-bundle) stream)))))))
(defn- fetch-bundle
"Multi-stage file bundle fetch coordinator"
[project-id file-id]
(ptk/reify ::fetch-bundle
ptk/WatchEvent
(watch [_ _ stream]
(->> (rx/merge
(rx/of (fetch-bundle-stage-1 project-id file-id))
(->> stream
(rx/filter (ptk/type? ::bundle-stage-1))
(rx/observe-on :async)
(rx/map deref)
(rx/map fetch-bundle-stage-2))
(->> stream
(rx/filter (ptk/type? ::bundle-stage-2))
(rx/observe-on :async)
(rx/map deref)
(rx/map bundle-fetched)))
(rx/take-until
(rx/filter (ptk/type? ::fetch-bundle) stream))))))
(defn initialize-file
[project-id file-id]
@ -322,7 +346,6 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of msg/hide
(features/initialize)
(dcm/retrieve-comment-threads file-id)
(dwp/initialize-file-persistence file-id)
(fetch-bundle project-id file-id)))
@ -548,7 +571,7 @@
(ptk/reify ::delete-page
ptk/WatchEvent
(watch [it state _]
(let [components-v2 (features/active-feature? state :components-v2)
(let [components-v2 (features/active-feature? state "components/v2")
file-id (:current-file-id state)
file (wsh/get-file state file-id)
pages (get-in state [:workspace-data :pages])
@ -1326,7 +1349,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [components-v2 (features/active-feature? state :components-v2)]
(let [components-v2 (features/active-feature? state "components/v2")]
(if components-v2
(rx/of (go-to-main-instance nil component-id))
(let [project-id (get-in state [:workspace-project :id])
@ -1341,7 +1364,7 @@
ptk/EffectEvent
(effect [_ state _]
(let [components-v2 (features/active-feature? state :components-v2)
(let [components-v2 (features/active-feature? state "components/v2")
wrapper-id (str "component-shape-id-" component-id)]
(when-not components-v2
(tm/schedule-on-idle #(dom/scroll-into-view-if-needed! (dom/get-element wrapper-id))))))))
@ -2007,143 +2030,6 @@
(rx/of (dch/commit-changes changes))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Remove graphics
;; TODO: this should be deprecated and removed together with components-v2
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- initialize-remove-graphics
[total]
(ptk/reify ::initialize-remove-graphics
ptk/UpdateEvent
(update [_ state]
(assoc state :remove-graphics {:total total
:current nil
:error false
:completed false}))))
(defn- update-remove-graphics
[current]
(ptk/reify ::update-remove-graphics
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:remove-graphics :current] current))))
(defn- error-in-remove-graphics
[]
(ptk/reify ::error-in-remove-graphics
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:remove-graphics :error] true))))
(defn clear-remove-graphics
[]
(ptk/reify ::clear-remove-graphics
ptk/UpdateEvent
(update [_ state]
(dissoc state :remove-graphics))))
(defn- complete-remove-graphics
[]
(ptk/reify ::complete-remove-graphics
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:remove-graphics :completed] true))
ptk/WatchEvent
(watch [_ state _]
(when-not (get-in state [:remove-graphics :error])
(rx/of (modal/hide))))))
(defn- remove-graphic
[it file-data page [index [media-obj pos]]]
(let [process-shapes
(fn [[shape children]]
(let [changes1 (-> (pcb/empty-changes it)
(pcb/set-save-undo? false)
(pcb/with-page page)
(pcb/with-objects (:objects page))
(pcb/with-library-data file-data)
(pcb/delete-media (:id media-obj))
(pcb/add-objects (cons shape children)))
page' (reduce (fn [page shape]
(ctst/add-shape (:id shape)
shape
page
uuid/zero
uuid/zero
nil
true))
page
(cons shape children))
[_ _ changes2] (cflh/generate-add-component it
[shape]
(:objects page')
(:id page)
(:id file-data)
true
nil
cfsh/prepare-create-artboard-from-selection)
changes (pcb/concat-changes changes1 changes2)]
(dch/commit-changes changes)))
shapes (if (= (:mtype media-obj) "image/svg+xml")
(->> (dwm/load-and-parse-svg media-obj)
(rx/mapcat (partial dwm/create-shapes-svg (:id file-data) (:objects page) pos)))
(dwm/create-shapes-img pos media-obj :wrapper-type :frame))]
(->> (rx/concat
(rx/of (update-remove-graphics index))
(rx/map process-shapes shapes))
(rx/catch #(do
(log/error :msg (str "Error removing " (:name media-obj))
:hint (ex-message %)
:error %)
(js/console.log (.-stack %))
(rx/of (error-in-remove-graphics)))))))
(defn- remove-graphics
[file-id file-name]
(ptk/reify ::remove-graphics
ptk/WatchEvent
(watch [it state stream]
(let [file-data (wsh/get-file state file-id)
grid-gap 50
[file-data' page-id start-pos]
(ctf/get-or-add-library-page file-data grid-gap)
new-page? (nil? (ctpl/get-page file-data page-id))
page (ctpl/get-page file-data' page-id)
media (vals (:media file-data'))
media-points
(map #(assoc % :points (-> (grc/make-rect 0 0 (:width %) (:height %))
(grc/rect->points)))
media)
shape-grid
(ctst/generate-shape-grid media-points start-pos grid-gap)
stoper (rx/filter (ptk/type? ::finalize-file) stream)]
(rx/concat
(rx/of (modal/show {:type :remove-graphics-dialog :file-name file-name})
(initialize-remove-graphics (count media)))
(when new-page?
(rx/of (dch/commit-changes (-> (pcb/empty-changes it)
(pcb/set-save-undo? false)
(pcb/add-page (:id page) page)))))
(->> (rx/mapcat (partial remove-graphic it file-data' page)
(rx/from (d/enumerate (d/zip media shape-grid))))
(rx/take-until stoper))
(rx/of (complete-remove-graphics)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Read only
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -8,7 +8,6 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.files.libraries-helpers :as cflh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.point :as gpt]
@ -328,7 +327,7 @@
selected (->> (wsh/lookup-selected state)
(cph/clean-loops objects)
(remove #(ctn/has-any-copy-parent? objects (get objects %)))) ;; We don't want to change the structure of component copies
components-v2 (features/active-feature? state :components-v2)]
components-v2 (features/active-feature? state "components/v2")]
(rx/of (add-component2 selected components-v2))))))
(defn add-multiple-components
@ -337,7 +336,7 @@
(ptk/reify ::add-multiple-components
ptk/WatchEvent
(watch [_ state _]
(let [components-v2 (features/active-feature? state :components-v2)
(let [components-v2 (features/active-feature? state "components/v2")
objects (wsh/lookup-page-objects state)
selected (->> (wsh/lookup-selected state)
(cph/clean-loops objects)
@ -364,7 +363,7 @@
(rx/empty)
(let [data (get state :workspace-data)
[path name] (cph/parse-path-name new-name)
components-v2 (features/active-feature? state :components-v2)
components-v2 (features/active-feature? state "components/v2")
update-fn
(fn [component]
@ -411,7 +410,7 @@
component (ctkl/get-component (:data library) component-id)
new-name (:name component)
components-v2 (features/active-feature? state :components-v2)
components-v2 (features/active-feature? state "components/v2")
main-instance-page (when components-v2
(ctf/get-component-page (:data library) component))
@ -447,7 +446,7 @@
ptk/WatchEvent
(watch [it state _]
(let [data (get state :workspace-data)]
(if (features/active-feature? state :components-v2)
(if (features/active-feature? state "components/v2")
(let [component (ctkl/get-component data id)
page-id (:main-instance-page component)
root-id (:main-instance-id component)]
@ -639,7 +638,7 @@
container (cph/get-container file :page page-id)
components-v2
(features/active-feature? state :components-v2)
(features/active-feature? state "components/v2")
changes
(-> (pcb/empty-changes it)
@ -686,7 +685,7 @@
local-file (wsh/get-local-file state)
container (cph/get-container local-file :page page-id)
shape (ctn/get-shape container id)
components-v2 (features/active-feature? state :components-v2)]
components-v2 (features/active-feature? state "components/v2")]
(when (ctk/instance-head? shape)
(let [libraries (wsh/get-libraries state)
@ -1016,7 +1015,7 @@
(ptk/reify ::watch-component-changes
ptk/WatchEvent
(watch [_ state stream]
(let [components-v2? (features/active-feature? state :components-v2)
(let [components-v2? (features/active-feature? state "components/v2")
stopper-s
(->> stream
@ -1138,9 +1137,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [features (cond-> ffeat/enabled
(features/active-feature? state :components-v2)
(conj "components/v2"))]
(let [features (features/get-team-enabled-features state)]
(rx/merge
(->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id})
(rx/ignore))

View file

@ -139,19 +139,15 @@
(ptk/reify ::persist-changes
ptk/WatchEvent
(watch [_ state _]
(let [;; this features set does not includes the ffeat/enabled
;; because they are already available on the backend and
;; this request provides a set of features to enable in
;; this request.
features (cond-> #{}
(features/active-feature? state :components-v2)
(conj "components/v2"))
sid (:session-id state)
(let [sid (:session-id state)
features (features/get-team-enabled-features state)
params {:id file-id
:revn file-revn
:session-id sid
:changes-with-metadata (into [] changes)
:features features}]
:features features
}]
(->> (rp/cmd! :update-file params)
(rx/mapcat (fn [lagged]
@ -209,7 +205,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [features (cond-> #{}
(features/active-feature? state :components-v2)
(features/active-feature? state "components/v2")
(conj "components/v2"))
sid (:session-id state)
file (dm/get-in state [:workspace-libraries file-id])

View file

@ -104,7 +104,7 @@
page (wsh/lookup-page state page-id)
objects (wsh/lookup-page-objects state page-id)
components-v2 (features/active-feature? state :components-v2)
components-v2 (features/active-feature? state "components/v2")
ids (cph/clean-loops objects ids)

View file

@ -23,37 +23,41 @@
[cuerdas.core :as str]
[potok.core :as ptk]))
(defn extract-name [url]
(let [query-idx (str/last-index-of url "?")
url (if (> query-idx 0) (subs url 0 query-idx) url)
filename (->> (str/split url "/") (last))
(defn extract-name [href]
(let [query-idx (str/last-index-of href "?")
href (if (> query-idx 0) (subs href 0 query-idx) href)
filename (->> (str/split href "/") (last))
ext-idx (str/last-index-of filename ".")]
(if (> ext-idx 0) (subs filename 0 ext-idx) filename)))
(defn upload-images
"Extract all bitmap images inside the svg data, and upload them, associated to the file.
Return a map {<url> <image-data>}."
Return a map {<href> <image-data>}."
[svg-data file-id]
(->> (rx/from (csvg/collect-images svg-data))
(rx/map (fn [uri]
(merge
{:file-id file-id
:is-local true
:url uri}
(if (str/starts-with? uri "data:")
{:name "image"
:content (wapi/data-uri->blob uri)}
{:name (extract-name uri)}))))
(rx/mapcat (fn [uri-data]
(->> (rp/cmd! (if (contains? uri-data :content)
(rx/map (fn [{:keys [href] :as item}]
(let [item (-> item
(assoc :file-id file-id)
(assoc :is-local true)
(assoc :name "image"))]
(if (str/starts-with? href "data:")
(assoc item :content (wapi/data-uri->blob href))
(-> item
(assoc :name (extract-name href))
(assoc :url href))))))
(rx/mapcat (fn [item]
;; TODO: :create-file-media-object-from-url is
;; deprecated and this should be resolved in
;; frontend
(->> (rp/cmd! (if (contains? item :content)
:upload-file-media-object
:create-file-media-object-from-url)
uri-data)
(dissoc item :href))
;; When the image uploaded fail we skip the shape
;; returning `nil` will afterward not create the shape.
(rx/catch #(rx/of nil))
(rx/map #(vector (:url uri-data) %)))))
(rx/reduce (fn [acc [url image]] (assoc acc url image)) {})))
(rx/map #(vector (:href item) %)))))
(rx/reduce conj {})))
(defn add-svg-shapes
[svg-data position]

View file

@ -165,6 +165,25 @@
(defmethod ptk/handle-error :restriction
[{:keys [code] :as error}]
(cond
(= :migration-in-progress code)
(let [message (tr "errors.migration-in-progress" (:feature error))
on-accept (constantly nil)]
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept})))
(= :team-feature-mismatch code)
(let [message (tr "errors.team-feature-mismatch" (:feature error))
on-accept (constantly nil)]
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept})))
(= :file-feature-mismatch code)
(let [message (tr "errors.file-feature-mismatch" (:feature error))
team-id (:current-team-id @st/state)
project-id (:current-project-id @st/state)
on-accept #(if (and project-id team-id)
(st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id}))
(set! (.-href glob/location) ""))]
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept})))
(= :feature-mismatch code)
(let [message (tr "errors.feature-mismatch" (:feature error))
team-id (:current-team-id @st/state)
@ -174,7 +193,7 @@
(set! (.-href glob/location) ""))]
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept})))
(= :features-not-supported code)
(= :feature-not-supported code)
(let [message (tr "errors.feature-not-supported" (:feature error))
team-id (:current-team-id @st/state)
project-id (:current-project-id @st/state)

View file

@ -5,103 +5,116 @@
;; Copyright (c) KALEIDOS INC
(ns app.main.features
"A thin, frontend centric abstraction layer and collection of
helpers for `app.common.features` namespace."
(:require
[app.common.data :as d]
[app.common.features :as cfeat]
[app.common.logging :as log]
[app.config :as cf]
[app.main.store :as st]
[app.util.timers :as tm]
[beicon.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str]
[okulary.core :as l]
[potok.core :as ptk]
[rumext.v2 :as mf]))
(log/set-level! :warn)
(log/set-level! :trace)
(def available-features
#{:components-v2 :new-css-system :grid-layout})
(def global-enabled-features
(cfeat/get-enabled-features cf/flags))
(defn- toggle-feature
(defn get-enabled-features
[state]
(-> (get state :features/runtime #{})
(set/union global-enabled-features)))
(defn get-team-enabled-features
[state]
(-> global-enabled-features
(set/union (get state :features/runtime #{}))
(set/intersection cfeat/no-migration-features)
(set/union (get state :features/team #{}))))
(def features-ref
(l/derived get-team-enabled-features st/state =))
(defn active-feature?
"Given a state and feature, check if feature is enabled"
[state feature]
(assert (contains? cfeat/supported-features feature) "not supported feature")
(or (contains? (get state :features/runtime) feature)
(if (contains? cfeat/no-migration-features feature)
(or (contains? global-enabled-features feature)
(contains? (get state :features/team) feature))
(contains? (get state :features/team state) feature))))
(defn use-feature
"A react hook that checks if feature is currently enabled"
[feature]
(assert (contains? cfeat/supported-features feature) "Not supported feature")
(let [enabled-features (mf/deref features-ref)]
(contains? enabled-features feature)))
(defn toggle-feature
"An event constructor for runtime feature toggle.
Warning: if a feature is active globally or by team, it can't be
disabled."
[feature]
(ptk/reify ::toggle-feature
ptk/UpdateEvent
(update [_ state]
(let [features (or (:features state) #{})]
(if (contains? features feature)
(do
(log/debug :hint "feature disabled" :feature (d/name feature))
(assoc state :features (disj features feature)))
(do
(log/debug :hint "feature enabled" :feature (d/name feature))
(assoc state :features (conj features feature))))))))
(assert (contains? cfeat/supported-features feature) "not supported feature")
(update state :features/runtime (fn [features]
(if (contains? features feature)
(do
(log/trc :hint "feature disabled" :feature feature)
(disj features feature))
(do
(log/trc :hint "feature enabled" :feature feature)
(conj features feature))))))))
(defn- enable-feature
(defn enable-feature
[feature]
(ptk/reify ::enable-feature
ptk/UpdateEvent
(update [_ state]
(let [features (or (:features state) #{})]
(if (contains? features feature)
state
(do
(log/debug :hint "feature enabled" :feature (d/name feature))
(assoc state :features (conj features feature))))))))
(defn toggle-feature!
[feature]
(assert (contains? available-features feature) "Not supported feature")
(tm/schedule-on-idle #(st/emit! (toggle-feature feature))))
(defn enable-feature!
[feature]
(assert (contains? available-features feature) "Not supported feature")
(tm/schedule-on-idle #(st/emit! (enable-feature feature))))
(defn active-feature?
([feature]
(active-feature? @st/state feature))
([state feature]
(assert (contains? available-features feature) "Not supported feature")
(contains? (get state :features) feature)))
(def features
(l/derived :features st/state))
(defn active-feature
[feature]
(l/derived #(contains? % feature) features))
(defn use-feature
[feature]
(assert (contains? available-features feature) "Not supported feature")
(let [active-feature-ref (mf/use-memo (mf/deps feature) #(active-feature feature))
active-feature? (mf/deref active-feature-ref)]
active-feature?))
(assert (contains? cfeat/supported-features feature) "not supported feature")
(if (active-feature? state feature)
state
(do
(log/trc :hint "feature enabled" :feature feature)
(update state :features/runtime (fnil conj #{}) feature))))))
(defn initialize
[]
(ptk/reify ::initialize
ptk/WatchEvent
(watch [_ _ _]
(log/trace :hint "event:initialize" :fn "features")
(rx/concat
;; Enable all features set on the configuration
(->> (rx/from cf/flags)
(rx/map name)
(rx/map (fn [flag]
(when (str/starts-with? flag "frontend-feature-")
(subs flag 17))))
(rx/filter some?)
(rx/map keyword)
(rx/map enable-feature))
([] (initialize #{}))
([team-features]
(assert (set? team-features) "expected a set of features")
(assert (every? string? team-features) "expected a set of strings")
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(let [runtime-features (get state :features/runtime #{})
team-features (into cfeat/default-enabled-features
cfeat/xf-supported-features
team-features)]
(-> state
(assoc :features/runtime runtime-features)
(assoc :features/team team-features))))
ptk/WatchEvent
(watch [_ _ _]
(when *assert*
(->> (rx/from cfeat/no-migration-features)
(rx/filter #(not (contains? cfeat/backend-only-features %)))
(rx/observe-on :async)
(rx/map enable-feature))))
ptk/EffectEvent
(effect [_ state _]
(log/trc :hint "initialized features"
:team (str/join "," (:features/team state))
:runtime (str/join "," (:features/runtime state)))))))
;; Enable the rest of available configuration if we are on development
;; environemnt (aka devenv).
(when *assert*
;; By default, all features disabled, except in development
;; environment, that are enabled except components-v2 and new css
(->> (rx/from available-features)
(rx/filter #(not= % :components-v2))
(rx/filter #(not= % :new-css-system))
(rx/map enable-feature)))))))

View file

@ -425,10 +425,6 @@
ids)))
st/state =))
;; Remove this when deprecating components-v2
(def remove-graphics
(l/derived :remove-graphics st/state))
;; ---- Viewer refs
(defn lookup-viewer-objects-by-id

View file

@ -40,7 +40,7 @@
{::mf/wrap [#(mf/catch % {:fallback on-main-error})]}
[{:keys [route profile]}]
(let [{:keys [data params]} route
new-css-system (features/use-feature :new-css-system)]
new-css-system (features/use-feature "styles/v2")]
[:& (mf/provider ctx/current-route) {:value route}
[:& (mf/provider ctx/new-css-system) {:value new-css-system}
(case (:name data)

View file

@ -157,7 +157,7 @@
(mf/with-effect [profile team-id]
(st/emit! (dd/initialize {:id team-id}))
(fn []
(dd/finalize {:id team-id})))
(st/emit! (dd/finalize {:id team-id}))))
(mf/with-effect []
(let [key (events/listen goog/global "keydown"

View file

@ -60,7 +60,7 @@
::mf/register-as :export
::mf/wrap-props false}
[{:keys [team-id files has-libraries? binary?]}]
(let [components-v2 (features/use-feature :components-v2)
(let [components-v2 (features/use-feature "components/v2")
state* (mf/use-state
#(let [files (mapv (fn [file] (assoc file :loading? true)) files)]
{:status :prepare

View file

@ -7,7 +7,6 @@
(ns app.main.ui.dashboard.grid
(:require
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.geom.point :as gpt]
[app.common.logging :as log]
[app.main.data.dashboard :as dd]
@ -51,22 +50,18 @@
(defn- ask-for-thumbnail
"Creates some hooks to handle the files thumbnails cache"
[file-id revn]
(let [features (cond-> ffeat/enabled
(features/active-feature? :components-v2)
(conj "components/v2"))]
(->> (wrk/ask! {:cmd :thumbnails/generate-for-file
:revn revn
:file-id file-id
:features features})
(rx/mapcat (fn [{:keys [fonts] :as result}]
(->> (fonts/render-font-styles fonts)
(rx/map (fn [styles]
(assoc result
:styles styles
:width 250))))))
(rx/mapcat thr/render)
(rx/mapcat (partial persist-thumbnail file-id revn)))))
(->> (wrk/ask! {:cmd :thumbnails/generate-for-file
:revn revn
:file-id file-id
:features (features/get-team-enabled-features @st/state)})
(rx/mapcat (fn [{:keys [fonts] :as result}]
(->> (fonts/render-font-styles fonts)
(rx/map (fn [styles]
(assoc result
:styles styles
:width 250))))))
(rx/mapcat thr/render)
(rx/mapcat (partial persist-thumbnail file-id revn))))
(mf/defc grid-item-thumbnail
{::mf/wrap-props false}

View file

@ -33,7 +33,7 @@
(sort-by :modified-at)
(reverse))))
components-v2 (features/use-feature :components-v2)
components-v2 (features/use-feature "components/v2")
width (mf/use-state nil)
rowref (mf/use-ref)

View file

@ -255,7 +255,7 @@
(fn []
(st/emit! (dd/fetch-files {:project-id project-id})
(dd/fetch-recent-files (:id team))
(dd/fetch-projects)
(dd/fetch-projects (:id team))
(dd/clear-selected-files))))]
(mf/with-effect

View file

@ -336,7 +336,7 @@
on-leave-as-owner-clicked
(fn []
(st/emit! (dd/fetch-team-members)
(st/emit! (dd/fetch-team-members (:id team))
(modal/show
{:type :leave-and-reassign
:profile profile

View file

@ -355,7 +355,7 @@
(mf/use-fn
(mf/deps profile team on-leave-accepted)
(fn []
(st/emit! (dd/fetch-team-members)
(st/emit! (dd/fetch-team-members (:id team))
(modal/show
{:type :leave-and-reassign
:profile profile
@ -452,8 +452,8 @@
(tr "dashboard.your-penpot")
(:name team)))))
(mf/with-effect []
(st/emit! (dd/fetch-team-members)))
(mf/with-effect [team]
(st/emit! (dd/fetch-team-members (:id team))))
[:*
[:& header {:section :dashboard-team-members :team team}]
@ -992,9 +992,10 @@
(:name team)))))
(mf/with-effect []
(st/emit! (dd/fetch-team-members)
(dd/fetch-team-stats)))
(mf/with-effect [team]
(let [team-id (:id team)]
(st/emit! (dd/fetch-team-members team-id)
(dd/fetch-team-stats team-id))))
[:*
[:& header {:section :dashboard-team-settings :team team}]

View file

@ -89,7 +89,7 @@
{::mf/wrap-props false}
[]
(let [modal (mf/deref modal-ref)
new-css-system (features/use-feature :new-css-system)]
new-css-system (features/use-feature "styles/v2")]
(when modal
[:& (mf/provider ctx/new-css-system) {:value new-css-system}
[:& modal-wrapper {:data modal

View file

@ -42,7 +42,7 @@
(update profile :lang #(or % "")))
form (fm/use-form :spec ::options-form
:initial initial)
new-css-system (features/use-feature :new-css-system)]
new-css-system (features/use-feature "styles/v2")]
[:& fm/form {:class "options-form"
:on-submit on-submit

View file

@ -38,7 +38,6 @@
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[goog.events :as events]
[okulary.core :as l]
[rumext.v2 :as mf]))
@ -177,8 +176,8 @@
(make-file-ready-ref file-id))
file-ready? (mf/deref file-ready*)
components-v2? (features/use-feature :components-v2)
new-css-system (features/use-feature :new-css-system)
components-v2? (features/use-feature "components/v2")
new-css-system (features/use-feature "styles/v2")
background-color (:background-color wglobal)]
@ -236,49 +235,3 @@
:wglobal wglobal
:layout layout}]
[:& workspace-loader])])]]]]]]]))
(mf/defc remove-graphics-dialog
{::mf/register modal/components
::mf/register-as :remove-graphics-dialog}
[{:keys [] :as ctx}]
(let [remove-state (mf/deref refs/remove-graphics)
project (mf/deref refs/workspace-project)
close #(modal/hide!)
reload-file #(dom/reload-current-window)
nav-out #(st/emit! (rt/navigate :dashboard-files
{:team-id (:team-id project)
:project-id (:id project)}))]
(mf/use-effect
(fn []
#(st/emit! (dw/clear-remove-graphics))))
[:div.modal-overlay
[:div.modal-container.remove-graphics-dialog
[:div.modal-header
[:div.modal-header-title
[:h2 (tr "workspace.remove-graphics.title" (:file-name ctx))]]
(if (and (:completed remove-state) (:error remove-state))
[:div.modal-close-button
{:on-click close} i/close]
[:div.modal-close-button
{:on-click nav-out}
i/close])]
(if-not (and (:completed remove-state) (:error remove-state))
[:div.modal-content
[:p (tr "workspace.remove-graphics.text1")]
[:p (tr "workspace.remove-graphics.text2")]
[:p.progress-message (tr "workspace.remove-graphics.progress"
(:current remove-state)
(:total remove-state))]]
[:*
[:div.modal-content
[:p.error-message [:span i/close] (tr "workspace.remove-graphics.error-msg")]
[:p (tr "workspace.remove-graphics.error-hint")]]
[:div.modal-footer
[:div.action-buttons
[:input.button-secondary {:type "button"
:value (tr "labels.close")
:on-click close}]
[:input.button-primary {:type "button"
:value (tr "labels.reload-file")
:on-click reload-file}]]]])]]))

View file

@ -443,7 +443,7 @@
(mf/defc context-menu-component
[{:keys [shapes]}]
(let [components-v2 (features/use-feature :components-v2)
(let [components-v2 (features/use-feature "components/v2")
single? (= (count shapes) 1)
objects (deref refs/workspace-page-objects)
any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes))

View file

@ -665,7 +665,7 @@
{::mf/register modal/components
::mf/register-as :libraries-dialog}
[{:keys [starting-tab] :as props :or {starting-tab :libraries}}]
(let [new-css-system (features/use-feature :new-css-system)
(let [new-css-system (features/use-feature "styles/v2")
project (mf/deref refs/workspace-project)
file-data (mf/deref refs/workspace-data)
file (mf/deref ref:workspace-file)

View file

@ -1207,6 +1207,8 @@
grid-justify-content-row (:layout-justify-content values)
grid-justify-content-column (:layout-align-content values)
grid-enabled? (features/use-feature "layout/grid")
set-justify-grid
(mf/use-fn
(mf/deps ids)
@ -1225,7 +1227,7 @@
:class (stl/css-case :title-spacing-layout (not has-layout?))}
(if (and (not multiple) (:layout values))
[:div {:class (stl/css :title-actions)}
(when (features/active-feature? :grid-layout)
(when ^boolean grid-enabled?
[:div {:class (stl/css :layout-options)}
[:& radio-buttons {:selected (d/name layout-type)
:on-change toggle-layout-style
@ -1317,7 +1319,7 @@
[:*
[:span "Layout"]
(if (features/active-feature? :grid-layout)
(if ^boolean grid-enabled?
[:div.title-actions
[:div.layout-btns
[:button {:on-click set-flex

View file

@ -100,7 +100,7 @@
(mf/defc object-svg
[{:keys [page-id file-id share-id object-id render-embed?]}]
(let [components-v2 (feat/use-feature :components-v2)
(let [components-v2 (feat/use-feature "components/v2")
fetch-state (mf/use-fn
(mf/deps file-id page-id share-id object-id components-v2)
(fn []
@ -141,7 +141,7 @@
(mf/defc objects-svg
[{:keys [page-id file-id share-id object-ids render-embed?]}]
(let [components-v2 (feat/use-feature :components-v2)
(let [components-v2 (feat/use-feature "components/v2")
fetch-state (mf/use-fn
(mf/deps file-id page-id share-id components-v2)
(fn []

View file

@ -99,21 +99,21 @@
(rf result input)))))
(defn prettify
"Prepare x fror cleaner output when logged."
"Prepare x for cleaner output when logged."
[x]
(cond
(map? x) (d/mapm #(prettify %2) x)
(vector? x) (mapv prettify x)
(seq? x) (map prettify x)
(set? x) (into #{} (map prettify x))
(set? x) (into #{} (map prettify) x)
(number? x) (mth/precision x 4)
(uuid? x) (str "#uuid " x)
(uuid? x) (str/concat "#uuid " x)
:else x))
(defn ^:export logjs
([str] (tap (partial logjs str)))
([str val]
(js/console.log str (clj->js (prettify val)))
(js/console.log str (clj->js (prettify val) :keyword-fn (fn [v] (str/concat v))))
val))
(when (exists? js/window)
@ -403,7 +403,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [features (cond-> #{}
(features/active-feature? state :components-v2)
(features/active-feature? state "components/v2")
(conj "components/v2"))
sid (:session-id state)
file (get state :workspace-file)

View file

@ -7,18 +7,23 @@
;; This namespace is only to export the functions for toggle features
(ns features
(:require
[app.main.features :as features]))
(defn ^:export components-v2 []
(features/toggle-feature! :components-v2)
nil)
[app.main.features :as features]
[app.main.store :as st]
[app.util.timers :as tm]))
(defn ^:export is-components-v2 []
(let [active? (features/active-feature :components-v2)]
@active?))
(features/active-feature? @st/state "components/v2"))
(defn ^:export new-css-system []
(features/toggle-feature! :new-css-system))
(tm/schedule-on-idle #(st/emit! (features/toggle-feature "styles/v2")))
nil)
(defn ^:export grid []
(features/toggle-feature! :grid-layout))
(tm/schedule-on-idle #(st/emit! (features/toggle-feature "layout/grid")))
nil)
(defn ^:export get-enabled []
(clj->js (features/get-enabled-features @st/state)))
(defn ^:export get-team-enabled []
(clj->js (features/get-team-enabled-features @st/state)))

View file

@ -38,7 +38,7 @@
:pages []
:pages-index {}}
:workspace-libraries {}
:features {:components-v2 true}})
:features/team #{"components/v2"}})
(def ^:private idmap (atom {}))

View file

@ -4174,35 +4174,6 @@ msgstr "Oddělit uzly (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Přichytit uzly (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Chcete-li to zkusit znovu, můžete tento soubor znovu načíst. Pokud problém "
"přetrvává, doporučujeme vám podívat se na seznam a zvážit odstranění "
"poškozené grafiky."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Některé grafiky nebylo možné aktualizovat."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Převádí se %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Grafiky knihovny jsou od nynějška komponenty, díky čemuž budou mnohem "
"výkonnější."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Tato aktualizace je jednorázová."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Aktualizace %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Přidat flexibilní rozložení"

View file

@ -4453,35 +4453,6 @@ msgstr "Ankerpunkte trennen (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "An Ankerpunkten ausrichten (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Um es erneut zu versuchen, können Sie diese Datei neu laden. Wenn das "
"Problem weiterhin besteht, empfehlen wir Ihnen, einen Blick auf die Liste "
"zu werfen und zu überlegen, ob Sie defekte Grafiken löschen wollen."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Einige Grafiken konnten nicht aktualisiert werden."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Konvertieren von %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Von nun an sind Grafiken in der Bibliothek auch Komponenten. Das macht sie "
"viel leistungsfähiger."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Diese Aktualisierung ist eine einmalige Aktion."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Aktualisierung von %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Flex-Layout hinzufügen"

View file

@ -879,10 +879,21 @@ msgstr ""
"Looks like you are opening a file that has the feature '%s' enabled but "
"your penpot frontend does not supports it or has it disabled."
#: src/app/main/errors.cljs
msgid "errors.file-feature-mismatch"
msgstr ""
"It seems that there is a mismatch between the enabled features and the "
"features of the file you are trying to open. Migrations for '%s' need "
"to be applied before the file can be opened."
#: src/app/main/errors.cljs
msgid "errors.feature-not-supported"
msgstr "Feature '%s' is not supported."
#: src/app/main/errors.cljs
msgid "errors.team-feature-mismatch"
msgstr "Detected incompatible feature '%s'"
#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs
msgid "errors.generic"
msgstr "Something wrong has happened."
@ -4561,35 +4572,6 @@ msgstr "Separate nodes (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Snap nodes (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"To try it again, you can reload this file. If the problem persists, we "
"suggest you to take a look at the list and consider to delete broken "
"graphics."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Some graphics could not be updated."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Converting %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Library Graphics are Components from now on, which will make them much more "
"powerful."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "This update is a one time action."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Updating %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Add flex layout"

View file

@ -903,6 +903,13 @@ msgstr ""
"pero la aplicacion web de penpot que esta usando no tiene soporte para ella "
"o esta deshabilitada."
#: src/app/main/errors.cljs
msgid "errors.file-feature-mismatch"
msgstr ""
"Parece que hay discordancia entre las features habilitadas y las features "
"del fichero que se esta intentando abrir. Falta aplicar migraciones para "
"'%s' antes de poder abrir el fichero."
#: src/app/main/errors.cljs
msgid "errors.feature-not-supported"
msgstr "Caracteristica no soportada: '%s'."
@ -4650,35 +4657,6 @@ msgstr "Separar nodos (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Alinear nodos (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Para intentarlo de nuevo, puedes recargar este archivo. Si el problema "
"persiste, te sugerimos que compruebes la lista y consideres borrar los "
"gráficos que estén mal."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Algunos gráficos no han podido ser actualizados."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Convirtiendo %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Desde ahora los gráficos de la librería serán componentes, lo cual los hará "
"mucho más potentes."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Esta actualización sólo ocurrirá una vez."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Actualizando %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Añadir flex layout"

View file

@ -4173,35 +4173,6 @@ msgstr "Banatu nodoak (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Atxikitu nodoak (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Berriz saiatzeko, fitxategi hau berriz kargatu dezakezu. Hala ere arazoa "
"izaten jarraitzen baduzu, begiratu zerrenda eta ezabatu apurtutako "
"grafikoak."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Grafiko batzuk ezin izan dira eguneratu."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Bihurtzen %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Liburutegiko grafikoak osagaiak izango dira orain, horrek ahaltsuago egingo "
"ditu."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Eguneraketa hau behin bakarrik gertatuko da."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Eguneratzen %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Gehitu flex diseinua"

View file

@ -4457,32 +4457,6 @@ msgstr "הפרדת מפרקים (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "הצמדת מפרקים (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"כדי לנסות שוב, אפשר לרענן את הקובץ הזה. אם הבעיה נמשכת, אנו ממליצים לך "
"להביט ברשימה ולשקול למחוק גרפיקה פגומה."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "לא ניתן לעדכן חלק מהגרפיקה."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "מתבצעת המרה %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr "גרפיקות ספרייה הן רכיבים מעתה ואילך, מה שהופך אותן להרבה יותר עוצמתיות."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "העדכון הזה הוא חד־פעמי."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "%s מתעדכן…"
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "הוספת פריסת flex"

View file

@ -4585,35 +4585,6 @@ msgstr "Simpul terpisah (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Tancap simpul (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Untuk mencoba lagi, Anda dapat memuat ulang berkas ini. Jika masalah tetap "
"ada, kami menyarankan Anda untuk melihat daftar dan mempertimbangkan untuk "
"menghapus grafis yang rusak."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Beberapa grafis tidak dapat diperbarui."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Mengubah %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Grafis Pustaka itu Komponen dari sekarang, yang akan membuatnya lebih "
"berdaya."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Pembaruan ini adalah tindakan satu kali."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Memperbarui %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Tambahkan tata letak flex"

View file

@ -4568,34 +4568,6 @@ msgstr "Atdalīt mezglus (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Pieķert mezglus (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Lai to mēģinātu vēlreiz, varat atkārtoti ielādēt šo failu. Ja problēma "
"joprojām pastāv, ieteicams apskatīt sarakstu un dzēst bojātās grafikas."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Dažas grafikas nevar atjaunināt."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "%s/%s pārvēršana"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Bibliotēkas grafikas turpmāk sauksies Komponentes, kas padarīs tās daudz "
"jaudīgākas."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Šis atjauninājums ir vienreizēja darbība."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Notiek %s atjaunināšana..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Pievienot elastīgo izkārtojumu"

View file

@ -4537,35 +4537,6 @@ msgstr "Verschillende knooppunten (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Snap knooppunten (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Om het opnieuw te proberen, kun je dit bestand opnieuw laden. Als het "
"probleem zich blijft voordoen, raden we aan de lijst te bekijken en te "
"overwegen om kapotte afbeeldingen te verwijderen."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Sommige afbeeldingen kunnen niet worden bijgewerkt."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "%s/%s converteren"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Bibliotheekafbeeldingen zijn vanaf nu componenten, waardoor ze veel "
"krachtiger worden."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Deze update is een eenmalige actie."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "%s bijwerken..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Flex-indeling toevoegen"

View file

@ -4011,35 +4011,6 @@ msgstr "Rozłącz węzły (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Przyciągnij węzły (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Aby spróbować ponownie, możesz ponownie załadować ten plik. Jeśli problem "
"będzie się powtarzał, sugerujemy przejrzenie listy i rozważenie usunięcia "
"uszkodzonej grafiki."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Niektórych grafik nie udało się zaktualizować."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Konwersja %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Grafika biblioteczna jest od teraz komponentami, co sprawi, że będą "
"znacznie potężniejsze."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Ta aktualizacja jest działaniem jednorazowym."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Aktualizowanie %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Dodaj układ flex"

View file

@ -3994,35 +3994,6 @@ msgstr "Separar pontos (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Aderir aos pontos (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Para tentar novamente, recarregue este arquivo. Se o problema persistir, "
"sugerimos olhar a lista e considerar excluir gráficos que não estejam "
"funcionando."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Alguns gráficos não puderam ser atualizados."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Convertendo %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"A partir de agora os gráficos da biblioteca são Componentes, o que os "
"tornarão bem mais poderosos."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Essa atualização acontecerá apenas uma vez."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Atualizando %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Adicionar Flex Layout"

View file

@ -4588,35 +4588,6 @@ msgstr "Separar nós (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Ajustar nós (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Para tentar de novo, podes recarregar este ficheiro. Se o problema "
"persistir, sugerimos que observes a lista e consideres em apagar os "
"gráficos problemáticos."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Não foi possível atualizar alguns gráficos."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "A converter %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"A partir de agora, os Gráficos da biblioteca passarão a ser Componentes, o "
"que os tornará mais poderosos."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Esta atualização só ocorrerá uma única vez."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "A atualizar %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Adicionar layout flex"

View file

@ -4626,35 +4626,6 @@ msgstr "Separă noduri (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Trage noduri (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Pentru a încerca din nou, puteți reîncărca acest fișier. Dacă problema "
"persistă, vă sugerăm să aruncați o privire pe listă și să luați în "
"considerare ștergerea graficii rupte."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Unele elemente grafice nu au putut fi actualizate."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Se convertește %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Bibliotecile Grafice sunt Componente de acum înainte, ceea ce le va face "
"mult mai puternice."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Această actualizare este o acțiune unică."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Actualizare %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Adăugați aspect flexibil"

View file

@ -4087,35 +4087,6 @@ msgstr "Düğümleri ayır (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Düğümleri tuttur (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Tekrar denemek için bu dosyayı yeniden yükleyebilirsiniz. Sorun devam "
"ederse, listeye bir göz atmanızı ve bozuk grafikleri silmeyi düşünmenizi "
"öneririz."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Bazı grafikler güncellenemedi."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "%s/%s dönüştürülüyor"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Kütüphane Grafikleri bundan böyle Bileşenlerdir ve bu da onları çok daha "
"güçlü kılacaktır."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Bu güncelleme tek seferlik bir işlemdir."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "%s güncelleniyor..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Düzen esnekliği ekle"

View file

@ -4069,30 +4069,6 @@ msgstr "拆分节点(%s"
msgid "workspace.path.actions.snap-nodes"
msgstr "对接节点 (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr "要重试,您可以重新加载此文件。如果问题仍然存在,我们建议您查看列表并考虑删除损坏的图形。"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "某些图形无法更新。"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "转换%s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr "从现在开始,库图形是组件,这将使它们更加强大。"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "此更新是一次性操作。"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "正在更新 %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "添加弹性布局"