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

View file

@ -21,7 +21,7 @@
[app.common.transit :as t] [app.common.transit :as t]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cfg] [app.config :as cf]
[app.main :as main] [app.main :as main]
[app.srepl.helpers :as srepl.helpers] [app.srepl.helpers :as srepl.helpers]
[app.srepl.main :as srepl] [app.srepl.main :as srepl]
@ -96,7 +96,9 @@
(try (try
(alter-var-root #'system (fn [sys] (alter-var-root #'system (fn [sys]
(when sys (ig/halt! 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/prep)
(ig/init)))) (ig/init))))
:started :started

View file

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

View file

@ -231,60 +231,76 @@
`(jdbc/with-transaction ~@args))) `(jdbc/with-transaction ~@args)))
(defn open (defn open
[pool] [system-or-pool]
(jdbc/get-connection 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] [o]
(if (connection? o) (cond
o (connection? o) o
(if (pool? o) (pool? o) o
o (map? o) (get-connectable (or (:conn o) (::pool o)))
(or (::conn o) (::pool o))))) :else (ex/raise :type :internal
:code :unable-resolve-connectable
:hint "expected conn, pool or system")))
(def ^:private default-opts (def ^:private default-opts
{:builder-fn sql/as-kebab-maps}) {:builder-fn sql/as-kebab-maps})
(defn exec! (defn exec!
([ds sv] ([ds sv]
(-> (resolve-connectable ds) (-> (get-connectable ds)
(jdbc/execute! sv default-opts))) (jdbc/execute! sv default-opts)))
([ds sv opts] ([ds sv opts]
(-> (resolve-connectable ds) (-> (get-connectable ds)
(jdbc/execute! sv (merge default-opts opts))))) (jdbc/execute! sv (merge default-opts opts)))))
(defn exec-one! (defn exec-one!
([ds sv] ([ds sv]
(-> (resolve-connectable ds) (-> (get-connectable ds)
(jdbc/execute-one! sv default-opts))) (jdbc/execute-one! sv default-opts)))
([ds sv opts] ([ds sv opts]
(-> (resolve-connectable ds) (-> (get-connectable ds)
(jdbc/execute-one! sv (jdbc/execute-one! sv
(-> (merge default-opts opts) (-> (merge default-opts opts)
(assoc :return-keys (::return-keys? opts false))))))) (assoc :return-keys (::return-keys? opts false)))))))
(defn insert! (defn insert!
[ds table params & {:as opts}] [ds table params & {:as opts}]
(-> (resolve-connectable ds) (-> (get-connectable ds)
(exec-one! (sql/insert table params opts) (exec-one! (sql/insert table params opts)
(merge {::return-keys? true} opts)))) (merge {::return-keys? true} opts))))
(defn insert-multi! (defn insert-multi!
[ds table cols rows & {:as opts}] [ds table cols rows & {:as opts}]
(-> (resolve-connectable ds) (-> (get-connectable ds)
(exec! (sql/insert-multi table cols rows opts) (exec! (sql/insert-multi table cols rows opts)
(merge {::return-keys? true} opts)))) (merge {::return-keys? true} opts))))
(defn update! (defn update!
[ds table params where & {:as opts}] [ds table params where & {:as opts}]
(-> (resolve-connectable ds) (-> (get-connectable ds)
(exec-one! (sql/update table params where opts) (exec-one! (sql/update table params where opts)
(merge {::return-keys? true} opts)))) (merge {::return-keys? true} opts))))
(defn delete! (defn delete!
[ds table params & {:as opts}] [ds table params & {:as opts}]
(-> (resolve-connectable ds) (-> (get-connectable ds)
(exec-one! (sql/delete table params opts) (exec-one! (sql/delete table params opts)
(merge {::return-keys? true} opts)))) (merge {::return-keys? true} opts))))
@ -318,7 +334,7 @@
(defn plan (defn plan
[ds sql] [ds sql]
(-> (resolve-connectable ds) (-> (get-connectable ds)
(jdbc/plan sql sql/default-opts))) (jdbc/plan sql sql/default-opts)))
(defn get-by-id (defn get-by-id
@ -422,12 +438,16 @@
(release! conn sp) (release! conn sp)
result) result)
(catch Throwable cause (catch Throwable cause
(rollback! sp) (rollback! conn sp)
(throw cause)))) (throw cause))))
(::pool cfg) (::pool cfg)
(with-atomic [conn (::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 :else
(throw (IllegalArgumentException. "invalid arguments")))) (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" {:name "0106-mod-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0106-mod-file-object-thumbnail-table.sql")} :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! (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 :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -291,9 +292,12 @@
(defn create-profile-rels! (defn create-profile-rels!
[conn {:keys [id] :as profile}] [conn {:keys [id] :as profile}]
(let [team (teams/create-team conn {:profile-id id (let [features (cfeat/get-enabled-features cf/flags)
:name "Default" team (teams/create-team conn
:is-default true})] {:profile-id id
:name "Default"
:features features
:is-default true})]
(-> (db/update! conn :profile (-> (db/update! conn :profile
{:default-team-id (:id team) {:default-team-id (:id team)
:default-project-id (:default-project-id team)} :default-project-id (:default-project-id team)}

View file

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

View file

@ -9,11 +9,11 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.files.migrations :as pmg] [app.common.files.migrations :as pmg]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.schema.desc-js-like :as-alias smdj] [app.common.schema.desc-js-like :as-alias smdj]
[app.common.schema.generators :as sg]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
@ -43,23 +43,6 @@
(when media-id (when media-id
(str (cf/get :public-uri) "/assets/by-id/" 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 ;; --- SPECS
(s/def ::features ::us/set-of-strings) (s/def ::features ::us/set-of-strings)
@ -181,28 +164,10 @@
:code :object-not-found :code :object-not-found
:hint "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 ;; 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 (defn load-pointer
[conn file-id id] [conn file-id id]
(let [row (db/get conn :file-data-fragment (let [row (db/get conn :file-data-fragment
@ -253,73 +218,16 @@
(into #{} (comp (filter pmap/pointer-map?) (into #{} (comp (filter pmap/pointer-map?)
(map pmap/get-id))))) (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 ;; QUERY COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- COMMAND QUERY: get-file (by id) ;; --- 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 (def schema:file
[:map {:title "File"} [:map {:title "File"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:features schema:features] [:features ::cfeat/features]
[:has-media-trimmed :boolean] [:has-media-trimmed :boolean]
[:comment-thread-seqn {:min 0} :int] [:comment-thread-seqn {:min 0} :int]
[:name :string] [:name :string]
@ -341,18 +249,21 @@
(def schema:get-file (def schema:get-file
[:map {:title "get-file"} [:map {:title "get-file"}
[:features {:optional true} schema:features] [:features {:optional true} ::cfeat/features]
[:id ::sm/uuid] [:id ::sm/uuid]
[:project-id {:optional true} ::sm/uuid]]) [:project-id {:optional true} ::sm/uuid]])
(defn get-file (defn get-file
([conn id client-features] ([conn id] (get-file conn id nil))
(get-file conn id client-features nil)) ([conn id project-id]
([conn id client-features project-id]
;; here we check if client requested features are supported (dm/assert!
(check-features-compatibility! client-features) "expected raw connection"
(db/connection? conn))
(binding [pmap/*load-fn* (partial load-pointer conn id) (binding [pmap/*load-fn* (partial load-pointer conn id)
pmap/*tracked* (atom {})] pmap/*tracked* (atom {})
cfeat/*new* (atom #{})]
(let [params (merge {:id id} (let [params (merge {:id id}
(when (some? project-id) (when (some? project-id)
@ -360,22 +271,21 @@
file (-> (db/get conn :file params) file (-> (db/get conn :file params)
(decode-row) (decode-row)
(pmg/migrate-file)) (pmg/migrate-file))]
file (handle-file-features! conn file client-features)]
;; NOTE: when file is migrated, we break the rule of no perform ;; NOTE: when file is migrated, we break the rule of no perform
;; mutations on get operations and update the file with all ;; mutations on get operations and update the file with all
;; migrations applied ;; migrations applied
(when (pmg/migrated? file) (if (pmg/migrated? file)
(let [features (db/create-array conn "text" (:features file))] (let [features (set/union (deref cfeat/*new*) (:features file))]
(db/update! conn :file (db/update! conn :file
{:data (blob/encode (:data file)) {:data (blob/encode (:data file))
:features features} :features (db/create-array conn "text" features)}
{:id id}) {:id id})
(persist-pointers! conn id))) (persist-pointers! conn id)
(assoc file :features features))
file)))) file)))))
(defn get-minimal-file (defn get-minimal-file
[{:keys [::db/pool] :as cfg} id] [{:keys [::db/pool] :as cfg} id]
@ -392,14 +302,32 @@
::cond/key-fn get-file-etag ::cond/key-fn get-file-etag
::sm/params schema:get-file ::sm/params schema:get-file
::sm/result schema:file-with-permissions} ::sm/result schema:file-with-permissions}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features project-id] :as params}] [cfg {:keys [::rpc/profile-id id project-id] :as params}]
(db/with-atomic [conn pool] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(let [perms (get-permissions conn profile-id id)] (let [perms (get-permissions conn profile-id id)]
(check-read-permissions! perms) (check-read-permissions! perms)
(let [file (-> (get-file conn id features project-id) (let [team (teams/get-team cfg
(assoc :permissions perms))] :profile-id profile-id
(vary-meta file assoc ::cond/key (get-file-etag params file)))))) :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) ;; --- COMMAND QUERY: get-file-fragment (by id)
@ -422,7 +350,7 @@
(update :content blob/decode))) (update :content blob/decode)))
(sv/defmethod ::get-file-fragment (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" {::doc/added "1.17"
::sm/params schema:get-file-fragment ::sm/params schema:get-file-fragment
::sm/result schema:file-fragment} ::sm/result schema:file-fragment}
@ -477,7 +405,6 @@
(projects/check-read-permissions! conn profile-id project-id) (projects/check-read-permissions! conn profile-id project-id)
(get-project-files conn project-id))) (get-project-files conn project-id)))
;; --- COMMAND QUERY: has-file-libraries ;; --- COMMAND QUERY: has-file-libraries
(declare get-has-file-libraries) (declare get-has-file-libraries)
@ -528,30 +455,41 @@
(update page :objects update-vals #(dissoc % :thumbnail))) (update page :objects update-vals #(dissoc % :thumbnail)))
(defn get-page (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) (when (and (uuid? object-id)
(not (uuid? page-id))) (not (uuid? page-id)))
(ex/raise :type :validation (ex/raise :type :validation
:code :params-validation :code :params-validation
:hint "page-id is required when object-id is provided")) :hint "page-id is required when object-id is provided"))
(let [file (get-file conn file-id features) (let [team (teams/get-team cfg
page-id (or page-id (-> file :data :pages first)) :profile-id profile-id
page (dm/get-in file [:data :pages-index page-id]) :file-id file-id)
page (if (pmap/pointer-map? page)
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) (deref page)
page)] page)))]
(cond-> (prune-thumbnails page) (cond-> (prune-thumbnails page)
(uuid? object-id) (uuid? object-id)
(prune-objects object-id)))) (prune-objects object-id))))
(def schema:get-page (def schema:get-page
[:map {:title "GetPage"} [:map {:title "get-page"}
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid] [:page-id {:optional true} ::sm/uuid]
[:share-id {:optional true} ::sm/uuid] [:share-id {:optional true} ::sm/uuid]
[:object-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 (sv/defmethod ::get-page
"Retrieves the page data from file and returns it. If no page-id is "Retrieves the page data from file and returns it. If no page-id is
@ -565,12 +503,11 @@
Mainly used for rendering purposes." Mainly used for rendering purposes."
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:get-page} ::sm/params schema:get-page}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] [cfg {:keys [::rpc/profile-id file-id share-id] :as params}]
(dm/with-open [conn (db/open pool)] (db/tx-run! cfg
(let [perms (get-permissions conn profile-id file-id share-id)] (fn [{:keys [::db/conn] :as cfg}]
(check-read-permissions! perms) (check-read-permissions! conn profile-id file-id share-id)
(binding [pmap/*load-fn* (partial load-pointer conn file-id)] (get-page cfg (assoc params :profile-id profile-id)))))
(get-page conn params)))))
;; --- COMMAND QUERY: get-team-shared-files ;; --- COMMAND QUERY: get-team-shared-files
@ -593,6 +530,7 @@
and p.team_id = ? and p.team_id = ?
order by f.modified_at desc") order by f.modified_at desc")
;; FIXME: i'm not sure about feature handling here... ???
(defn get-team-shared-files (defn get-team-shared-files
[conn team-id] [conn team-id]
(letfn [(assets-sample [assets limit] (letfn [(assets-sample [assets limit]
@ -626,19 +564,19 @@
(map #(assoc % :library-summary (library-summary %))) (map #(assoc % :library-summary (library-summary %)))
(map #(dissoc % :data))))))) (map #(dissoc % :data)))))))
(s/def ::get-team-shared-files (def ^:private schema:get-team-shared-files
(s/keys :req [::rpc/profile-id] [:map {:title "get-team-shared-files"}
:req-un [::team-id])) [:team-id ::sm/uuid]])
(sv/defmethod ::get-team-shared-files (sv/defmethod ::get-team-shared-files
"Get all file (libraries) for the specified team." "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]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(get-team-shared-files conn team-id))) (get-team-shared-files conn team-id)))
;; --- COMMAND QUERY: get-file-libraries ;; --- COMMAND QUERY: get-file-libraries
(def ^:private sql:get-file-libraries (def ^:private sql:get-file-libraries
@ -669,17 +607,20 @@
[conn file-id] [conn file-id]
(into [] (into []
(comp (comp
;; FIXME: :is-indirect set to false to all rows looks
;; completly useless
(map #(assoc % :is-indirect false)) (map #(assoc % :is-indirect false))
(map decode-row)) (map decode-row))
(db/exec! conn [sql:get-file-libraries file-id]))) (db/exec! conn [sql:get-file-libraries file-id])))
(s/def ::get-file-libraries (def ^:private schema:get-file-libraries
(s/keys :req [::rpc/profile-id] [:map {:title "get-file-libraries"}
:req-un [::file-id])) [:file-id ::sm/uuid]])
(sv/defmethod ::get-file-libraries (sv/defmethod ::get-file-libraries
"Get libraries used by the specified file." "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]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
@ -700,12 +641,14 @@
[conn file-id] [conn file-id]
(db/exec! conn [sql:library-using-files file-id])) (db/exec! conn [sql:library-using-files file-id]))
(s/def ::get-library-file-references (def ^:private schema:get-library-file-references
(s/keys :req [::rpc/profile-id] :req-un [::file-id])) [:map {:title "get-library-file-references"}
[:file-id ::sm/uuid]])
(sv/defmethod ::get-library-file-references (sv/defmethod ::get-library-file-references
"Returns all the file references that use specified file (library) id." "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}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
@ -745,12 +688,13 @@
(assoc :thumbnail-uri (resolve-public-uri media-id))) (assoc :thumbnail-uri (resolve-public-uri media-id)))
(dissoc row :media-id)))))) (dissoc row :media-id))))))
(s/def ::get-team-recent-files (def ^:private schema:get-team-recent-files
(s/keys :req [::rpc/profile-id] [:map {:title "get-team-recent-files"}
:req-un [::team-id])) [:team-id ::sm/uuid]])
(sv/defmethod ::get-team-recent-files (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]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
@ -763,15 +707,26 @@
"Retrieve a file summary by its ID. Only authenticated users." "Retrieve a file summary by its ID. Only authenticated users."
{::doc/added "1.20" {::doc/added "1.20"
::sm/params schema:get-file} ::sm/params schema:get-file}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features project-id] :as params}] [cfg {:keys [::rpc/profile-id id project-id] :as params}]
(db/with-atomic [conn pool] (db/tx-run! cfg
(check-read-permissions! conn profile-id id) (fn [{:keys [::db/conn] :as cfg}]
(let [file (get-file conn id features project-id)] (check-read-permissions! conn profile-id id)
{:name (:name file) (let [team (teams/get-team cfg
:components-count (count (ctkl/components-seq (:data file))) :profile-id profile-id
:graphics-count (count (get-in file [:data :media] [])) :project-id project-id
:colors-count (count (get-in file [:data :colors] [])) :file-id id)
:typography-count (count (get-in file [:data :typographies] []))})))
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 ;; MUTATION COMMANDS
@ -927,13 +882,15 @@
[conn {:keys [file-id library-id] :as params}] [conn {:keys [file-id library-id] :as params}]
(db/exec-one! conn [sql:link-file-to-library file-id library-id])) (db/exec-one! conn [sql:link-file-to-library file-id library-id]))
(s/def ::link-file-to-library (def ^:private schema:link-file-to-library
(s/keys :req [::rpc/profile-id] [:map {:title "link-file-to-library"}
:req-un [::file-id ::library-id])) [:file-id ::sm/uuid]
[:library-id ::sm/uuid]])
(sv/defmethod ::link-file-to-library (sv/defmethod ::link-file-to-library
{::doc/added "1.17" {::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}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
(when (= file-id library-id) (when (= file-id library-id)
(ex/raise :type :validation (ex/raise :type :validation
@ -952,13 +909,15 @@
{:file-id file-id {:file-id file-id
:library-file-id library-id})) :library-file-id library-id}))
(s/def ::unlink-file-from-library (def ^:private schema:unlink-file-to-library
(s/keys :req [::rpc/profile-id] [:map {:title "unlink-file-to-library"}
:req-un [::file-id ::library-id])) [:file-id ::sm/uuid]
[:library-id ::sm/uuid]])
(sv/defmethod ::unlink-file-from-library (sv/defmethod ::unlink-file-from-library
{::doc/added "1.17" {::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}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id file-id)
@ -974,15 +933,15 @@
{:file-id file-id {:file-id file-id
:library-file-id library-id})) :library-file-id library-id}))
(s/def ::update-file-library-sync-status (def ^:private schema:update-file-library-sync-status
(s/keys :req [::rpc/profile-id] [:map {:title "update-file-library-sync-status"}
:req-un [::file-id ::library-id])) [:file-id ::sm/uuid]
[:library-id ::sm/uuid]])
;; TODO: improve naming
(sv/defmethod ::update-file-library-sync-status (sv/defmethod ::update-file-library-sync-status
"Update the synchronization status of a file->library link" "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}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id file-id)

View file

@ -7,15 +7,18 @@
(ns app.rpc.commands.files-create (ns app.rpc.commands.files-create
(:require (:require
[app.common.data :as d] [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.types.file :as ctf]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db] [app.db :as db]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects] [app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.permissions :as perms] [app.rpc.permissions :as perms]
[app.rpc.quotes :as quotes] [app.rpc.quotes :as quotes]
@ -24,7 +27,7 @@
[app.util.pointer-map :as pmap] [app.util.pointer-map :as pmap]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.spec.alpha :as s])) [clojure.set :as set]))
(defn create-file-role! (defn create-file-role!
[conn {:keys [file-id profile-id role]}] [conn {:keys [file-id profile-id role]}]
@ -34,27 +37,27 @@
(db/insert! conn :file-profile-rel)))) (db/insert! conn :file-profile-rel))))
(defn create-file (defn create-file
[conn {:keys [id name project-id is-shared revn [{:keys [::db/conn] :as cfg}
modified-at deleted-at create-page {:keys [id name project-id is-shared revn
ignore-sync-until features] modified-at deleted-at create-page
:or {is-shared false revn 0 create-page true} ignore-sync-until features]
:as params}] :or {is-shared false revn 0 create-page true}
:as params}]
(let [id (or id (uuid/next)) (let [id (or id (uuid/next))
features (->> features
(into (files/get-default-features))
(files/check-features-compatibility!))
pointers (atom {}) pointers (atom {})
data (binding [pmap/*tracked* pointers data (binding [pmap/*tracked* pointers
ffeat/*current* features cfeat/*current* features
ffeat/*wrap-with-objects-map-fn* (if (features "storate/objects-map") omap/wrap identity) cfeat/*wrap-with-objects-map-fn* (if (features "fdata/objects-map") omap/wrap identity)
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)] cfeat/*wrap-with-pointer-map-fn* (if (features "fdata/pointer-map") pmap/wrap identity)]
(if create-page (if create-page
(ctf/make-file-data id) (ctf/make-file-data id)
(ctf/make-file-data id nil))) (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 file (db/insert! conn :file
(d/without-nils (d/without-nils
{:id id {:id id
@ -80,29 +83,58 @@
(files/decode-row file))) (files/decode-row file)))
(s/def ::create-file (def ^:private schema:create-file
(s/keys :req [::rpc/profile-id] [:map {:title "create-file"}
:req-un [::files/name [:name :string]
::files/project-id] [:project-id ::sm/uuid]
:opt-un [::files/id [:id {:optional true} ::sm/uuid]
::files/is-shared [:is-shared {:optional true} :boolean]
::files/features])) [:features {:optional true} ::cfeat/features]])
(sv/defmethod ::create-file (sv/defmethod ::create-file
{::doc/added "1.17" {::doc/added "1.17"
::doc/module :files ::doc/module :files
::webhooks/event? true} ::webhooks/event? true
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] ::sm/params schema:create-file}
(db/with-atomic [conn pool] [cfg {:keys [::rpc/profile-id project-id] :as params}]
(projects/check-edition-permissions! conn profile-id project-id) (db/tx-run! cfg
(let [team-id (files/get-team-id conn project-id) (fn [{:keys [::db/conn] :as cfg}]
params (assoc params :profile-id profile-id)] (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) ;; When we create files, we only need to respect the team
(list {::quotes/id ::quotes/files-per-project ;; features, because some features can be enabled
::quotes/team-id team-id ;; globally, but the team is still not migrated properly.
::quotes/profile-id profile-id features (-> (cfeat/get-team-enabled-features cf/flags team)
::quotes/project-id project-id})) (cfeat/check-client-features! (:features params)))
(-> (create-file conn params) ;; We also include all no migration features declared by
(vary-meta assoc ::audit/props {:team-id team-id}))))) ;; 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 :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.thumbnails :as thc] [app.common.thumbnails :as thc]
[app.common.types.shape-tree :as ctt] [app.common.types.shape-tree :as ctt]
[app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
@ -22,6 +24,7 @@
[app.media :as media] [app.media :as media]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond] [app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
@ -237,7 +240,7 @@
(def ^:private schema:get-file-data-for-thumbnail (def ^:private schema:get-file-data-for-thumbnail
[:map {:title "get-file-data-for-thumbnail"} [:map {:title "get-file-data-for-thumbnail"}
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:features {:optional true} files/schema:features]]) [:features {:optional true} ::cfeat/features]])
(def ^:private schema:partial-file (def ^:private schema:partial-file
[:map {:title "PartialFile"} [:map {:title "PartialFile"}
@ -252,17 +255,23 @@
::doc/module :files ::doc/module :files
::sm/params schema:get-file-data-for-thumbnail ::sm/params schema:get-file-data-for-thumbnail
::sm/result schema:partial-file} ::sm/result schema:partial-file}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}] [cfg {:keys [::rpc/profile-id file-id] :as params}]
(dm/with-open [conn (db/open pool)] (db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-read-permissions! conn profile-id file-id) (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 (let [team (teams/get-team cfg
;; or not. :profile-id profile-id
(let [feat (into #{"storage/pointer-map"} features) :file-id file-id)
file (files/get-file conn file-id feat)]
{:file-id file-id file (files/get-file conn file-id)]
:revn (:revn file)
:page (get-file-data-for-thumbnail conn file)}))) (-> (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 ;; MUTATION COMMANDS

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,16 +10,18 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.pprint :as p] [app.common.pprint :as p]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.features.fdata :as features.fdata]
[app.msgbus :as mbus] [app.msgbus :as mbus]
[app.rpc.commands.auth :as auth] [app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
[app.rpc.commands.files-snapshot :as fsnap] [app.rpc.commands.files-snapshot :as fsnap]
[app.rpc.commands.profile :as profile]
[app.srepl.fixes :as f] [app.srepl.fixes :as f]
[app.srepl.helpers :as h] [app.srepl.helpers :as h]
[app.storage :as sto] [app.storage :as sto]
@ -110,41 +112,57 @@
(defn enable-objects-map-feature-on-file! (defn enable-objects-map-feature-on-file!
[system & {:keys [save? id]}] [system & {:keys [save? id]}]
(letfn [(update-file [{:keys [features] :as file}] (h/update-file! system
(if (contains? features "storage/objects-map") :id id
file :update-fn features.fdata/enable-objects-map
(-> file :save? save?))
(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?)))
(defn enable-pointer-map-feature-on-file! (defn enable-pointer-map-feature-on-file!
[system & {:keys [save? id]}] [system & {:keys [save? id]}]
(letfn [(update-file [{:keys [features] :as file}] (h/update-file! system
(if (contains? features "storage/pointer-map") :id id
file :update-fn features.fdata/enable-pointer-map
(-> file :save? save?))
(update :data migrate)
(update :features conj "storage/pointer-map"))))
(migrate [data] (defn enable-team-feature!
(-> data [system team-id feature]
(update :pages-index update-vals pmap/wrap) (dm/verify!
(update :components pmap/wrap)))] "feature should be supported"
(contains? cfeat/supported-features feature))
(h/update-file! system (let [team-id (if (string? team-id)
:id id (parse-uuid team-id)
:update-fn update-file team-id)]
:save? save?))) (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! (defn enable-storage-features-on-file!
[system & {:as params}] [system & {:as params}]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -622,9 +622,9 @@
(t/is (uuid? (first result))) (t/is (uuid? (first result)))
(t/is (= 1 (count 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}) (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)} ::rpc/profile-id (:id prof)}
out (th/command! data)] out (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)

View file

@ -28,7 +28,9 @@
funcool/tubax {:mvn/version "2021.05.20-0"} funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2022.06.16-403"} 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" funcool/datoteka {:mvn/version "3.0.66"
:exclusions [funcool/promesa]} :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) (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 (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.files.defaults :refer [version]] [app.common.files.defaults :refer [version]]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.rect :as grc] [app.common.geom.rect :as grc]
@ -441,6 +442,7 @@
(defmethod migrate 25 (defmethod migrate 25
[data] [data]
(some-> cfeat/*new* (swap! conj "fdata/shape-data-type"))
(letfn [(update-object [object] (letfn [(update-object [object]
(-> object (-> object
(d/update-when :selrect grc/make-rect) (d/update-when :selrect grc/make-rect)
@ -594,6 +596,7 @@
(defmethod migrate 32 (defmethod migrate 32
[data] [data]
(some-> cfeat/*new* (swap! conj "fdata/shape-data-type"))
(letfn [(update-object [object] (letfn [(update-object [object]
(as-> object object (as-> object object
(if (contains? object :svg-attrs) (if (contains? object :svg-attrs)
@ -638,3 +641,13 @@
(update :pages-index update-vals update-container) (update :pages-index update-vals update-container)
(update :components 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 (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [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.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.rect :as grc] [app.common.geom.rect :as grc]
@ -75,7 +75,7 @@
(defn with-objects (defn with-objects
[changes 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)) (ctf/make-file-data (uuid/next) uuid/zero))
fdata (assoc-in fdata [:pages-index uuid/zero :objects] objects)] fdata (assoc-in fdata [:pages-index uuid/zero :objects] objects)]
(vary-meta changes assoc (vary-meta changes assoc

View file

@ -1045,7 +1045,9 @@
(let [redfn (fn [acc {:keys [tag attrs]}] (let [redfn (fn [acc {:keys [tag attrs]}]
(cond-> acc (cond-> acc
(= :image tag) (= :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 ))) (reduce-nodes redfn [] svg-data )))
#?(:cljs #?(:cljs

View file

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

View file

@ -8,8 +8,8 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.files.defaults :refer [version]] [app.common.files.defaults :refer [version]]
[app.common.files.features :as ffeat]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.logging :as l] [app.common.logging :as l]
@ -58,7 +58,6 @@
[:media {:optional true} [:media {:optional true}
[:map-of {:gen/max 5} ::sm/uuid ::media-object]]]) [:map-of {:gen/max 5} ::sm/uuid ::media-object]]])
(def file-data? (def file-data?
(sm/pred-fn ::data)) (sm/pred-fn ::data))
@ -82,11 +81,11 @@
(let [page (when (some? page-id) (let [page (when (some? page-id)
(ctp/make-empty-page page-id "Page 1"))] (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) (some? page-id)
(ctpl/add-page page) (ctpl/add-page page)
(contains? ffeat/*current* "components/v2") (contains? cfeat/*current* "components/v2")
(assoc-in [:options :components-v2] true))))) (assoc-in [:options :components-v2] true)))))
;; Helpers ;; Helpers

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,17 +9,13 @@
[app.common.attrs :as attrs] [app.common.attrs :as attrs]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.files.helpers :as cfh] [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.align :as gal]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.proportions :as gpp] [app.common.geom.proportions :as gpp]
[app.common.geom.rect :as grc] [app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.geom.shapes.grid-layout :as gslg] [app.common.geom.shapes.grid-layout :as gslg]
[app.common.logging :as log]
[app.common.pages.changes-builder :as pcb] [app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.text :as txt] [app.common.text :as txt]
@ -27,8 +23,6 @@
[app.common.types.component :as ctk] [app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn] [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 :as cts]
[app.common.types.shape-tree :as ctst] [app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
@ -39,7 +33,6 @@
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.fonts :as df] [app.main.data.fonts :as df]
[app.main.data.messages :as msg] [app.main.data.messages :as msg]
[app.main.data.modal :as modal]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.data.workspace.bool :as dwb] [app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.changes :as dch] [app.main.data.workspace.changes :as dch]
@ -94,18 +87,12 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private workspace-initialized) (declare ^:private workspace-initialized)
(declare ^:private remove-graphics)
(declare ^:private libraries-fetched) (declare ^:private libraries-fetched)
;; --- Initialize Workspace ;; --- Initialize Workspace
(defn initialize-layout (defn initialize-layout
[lname] [lname]
;; (dm/assert!
;; "expected valid layout"
;; (and (keyword? lname)
;; (contains? layout/presets lname)))
(ptk/reify ::initialize-layout (ptk/reify ::initialize-layout
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
@ -129,18 +116,10 @@
(assoc :workspace-ready? true))) (assoc :workspace-ready? true)))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ _ _]
(let [file (:workspace-data state) (rx/of (fbc/fix-bool-contents)
has-graphics? (-> file :media seq) (fdf/fix-deleted-fonts)
components-v2 (features/active-feature? state :components-v2)] (fbs/fix-broken-shapes)))))
(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)))))))
(defn- workspace-data-loaded (defn- workspace-data-loaded
[data] [data]
@ -171,39 +150,43 @@
(assoc data :pages-index pages-index)))))) (assoc data :pages-index pages-index))))))
(defn- bundle-fetched (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/reify ::bundle-fetched
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(assoc :users (d/index-by :id team-users))
(assoc :workspace-thumbnails thumbnails) (assoc :workspace-thumbnails thumbnails)
(assoc :workspace-file (dissoc file :data)) (assoc :workspace-file (dissoc file :data))
(assoc :workspace-project project) (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)))) (assoc :current-file-comments-users (d/index-by :id comments-users))))
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ stream] (watch [_ _ stream]
(let [team-id (:team-id project) (let [team-id (:id team)
stoper (rx/filter (ptk/type? ::bundle-fetched) stream)] file-id (:id file)
file-data (:data file)
stoper-s (rx/filter (ptk/type? ::bundle-fetched) stream)]
(->> (rx/concat (->> (rx/concat
;; Initialize notifications ;; Initialize notifications
(rx/of (dwn/initialize team-id id) (rx/of (dwn/initialize team-id file-id)
(dwsl/initialize)) (dwsl/initialize))
;; Load team fonts. We must ensure custom fonts are ;; Load team fonts. We must ensure custom fonts are
;; fully loadad before mark workspace as initialized ;; fully loadad before mark workspace as initialized
(rx/merge (rx/merge
(->> stream (->> stream
(rx/filter (ptk/type? :app.main.data.fonts/team-fonts-loaded)) (rx/filter (ptk/type? ::df/team-fonts-loaded))
(rx/take 1) (rx/take 1)
(rx/ignore)) (rx/ignore))
(rx/of (df/load-team-fonts team-id)) (rx/of (df/load-team-fonts team-id))
;; FIXME: move to bundle fetch stages
;; Load main file ;; Load main file
(->> (resolve-file-data id data) (->> (resolve-file-data file-id file-data)
(rx/mapcat (fn [{:keys [pages-index] :as data}] (rx/mapcat (fn [{:keys [pages-index] :as data}]
(->> (rx/from (seq pages-index)) (->> (rx/from (seq pages-index))
(rx/mapcat (rx/mapcat
@ -217,7 +200,7 @@
(rx/map workspace-data-loaded)) (rx/map workspace-data-loaded))
;; Load libraries ;; Load libraries
(->> (rp/cmd! :get-file-libraries {:file-id id}) (->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat identity) (rx/mapcat identity)
(rx/merge-map (rx/merge-map
(fn [{:keys [id synced-at]}] (fn [{:keys [id synced-at]}]
@ -233,8 +216,10 @@
(rx/map #(assoc file :thumbnails %))))) (rx/map #(assoc file :thumbnails %)))))
(rx/reduce conj []) (rx/reduce conj [])
(rx/map libraries-fetched))) (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 (defn- libraries-fetched
[libraries] [libraries]
@ -255,7 +240,7 @@
(rx/concat (rx/timer 1000) (rx/concat (rx/timer 1000)
(rx/of (dwl/notify-sync-file file-id)))))))) (rx/of (dwl/notify-sync-file file-id))))))))
(defn- fetch-thumbnail-blob-uri (defn- datauri->blob-uri
[uri] [uri]
(->> (http/send! {:uri uri (->> (http/send! {:uri uri
:response-type :blob :response-type :blob
@ -263,47 +248,86 @@
(rx/map :body) (rx/map :body)
(rx/map (fn [blob] (wapi/create-uri blob))))) (rx/map (fn [blob] (wapi/create-uri blob)))))
(defn- fetch-thumbnail-blobs (defn- fetch-file-object-thumbnails
[file-id] [file-id]
(->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id}) (->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
(rx/mapcat (fn [thumbnails] (rx/mapcat (fn [thumbnails]
(->> (rx/from thumbnails) (->> (rx/from thumbnails)
(rx/mapcat (fn [[k v]] (rx/mapcat (fn [[k v]]
;; we only need to fetch the thumbnail if ;; we only need to fetch the thumbnail if
;; it is a data:uri, otherwise we can just ;; it is a data:uri, otherwise we can just
;; use the value as is. ;; use the value as is.
(if (.startsWith v "data:") (if (str/starts-with? v "data:")
(->> (fetch-thumbnail-blob-uri v) (->> (datauri->blob-uri v)
(rx/map (fn [uri] [k uri]))) (rx/map (fn [uri] [k uri])))
(rx/of [k v]))))))) (rx/of [k v])))))))
(rx/reduce conj {}))) (rx/reduce conj {})))
(defn- fetch-bundle (defn- fetch-bundle-stage-1
[project-id file-id] [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 ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [features (cond-> ffeat/enabled (let [features (features/get-team-enabled-features state)
(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"))
;; WTF is this? ;; WTF is this?
share-id (-> state :viewer-local :share-id) share-id (-> state :viewer-local :share-id)]
stoper (rx/filter (ptk/type? ::fetch-bundle) stream)]
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features :project-id project-id}) (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features :project-id project-id})
(fetch-thumbnail-blobs file-id) (fetch-file-object-thumbnails file-id)
(rp/cmd! :get-project {:id project-id})
(rp/cmd! :get-team-users {:file-id 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})) (rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id}))
(rx/take 1) (rx/take 1)
(rx/map (partial bundle-fetched features)) (rx/map (fn [[file thumbnails team-users comments-users]]
(rx/take-until stoper)))))) (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 (defn initialize-file
[project-id file-id] [project-id file-id]
@ -322,7 +346,6 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of msg/hide (rx/of msg/hide
(features/initialize)
(dcm/retrieve-comment-threads file-id) (dcm/retrieve-comment-threads file-id)
(dwp/initialize-file-persistence file-id) (dwp/initialize-file-persistence file-id)
(fetch-bundle project-id file-id))) (fetch-bundle project-id file-id)))
@ -548,7 +571,7 @@
(ptk/reify ::delete-page (ptk/reify ::delete-page
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (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-id (:current-file-id state)
file (wsh/get-file state file-id) file (wsh/get-file state file-id)
pages (get-in state [:workspace-data :pages]) pages (get-in state [:workspace-data :pages])
@ -1326,7 +1349,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [components-v2 (features/active-feature? state :components-v2)] (let [components-v2 (features/active-feature? state "components/v2")]
(if components-v2 (if components-v2
(rx/of (go-to-main-instance nil component-id)) (rx/of (go-to-main-instance nil component-id))
(let [project-id (get-in state [:workspace-project :id]) (let [project-id (get-in state [:workspace-project :id])
@ -1341,7 +1364,7 @@
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (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)] wrapper-id (str "component-shape-id-" component-id)]
(when-not components-v2 (when-not components-v2
(tm/schedule-on-idle #(dom/scroll-into-view-if-needed! (dom/get-element wrapper-id)))))))) (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)))))) (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 ;; Read only
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

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

View file

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

View file

@ -104,7 +104,7 @@
page (wsh/lookup-page state page-id) page (wsh/lookup-page state page-id)
objects (wsh/lookup-page-objects 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) ids (cph/clean-loops objects ids)

View file

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

View file

@ -165,6 +165,25 @@
(defmethod ptk/handle-error :restriction (defmethod ptk/handle-error :restriction
[{:keys [code] :as error}] [{:keys [code] :as error}]
(cond (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) (= :feature-mismatch code)
(let [message (tr "errors.feature-mismatch" (:feature error)) (let [message (tr "errors.feature-mismatch" (:feature error))
team-id (:current-team-id @st/state) team-id (:current-team-id @st/state)
@ -174,7 +193,7 @@
(set! (.-href glob/location) ""))] (set! (.-href glob/location) ""))]
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept}))) (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)) (let [message (tr "errors.feature-not-supported" (:feature error))
team-id (:current-team-id @st/state) team-id (:current-team-id @st/state)
project-id (:current-project-id @st/state) project-id (:current-project-id @st/state)

View file

@ -5,103 +5,116 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.main.features (ns app.main.features
"A thin, frontend centric abstraction layer and collection of
helpers for `app.common.features` namespace."
(:require (:require
[app.common.data :as d] [app.common.features :as cfeat]
[app.common.logging :as log] [app.common.logging :as log]
[app.config :as cf] [app.config :as cf]
[app.main.store :as st] [app.main.store :as st]
[app.util.timers :as tm]
[beicon.core :as rx] [beicon.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(log/set-level! :warn) (log/set-level! :trace)
(def available-features (def global-enabled-features
#{:components-v2 :new-css-system :grid-layout}) (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] [feature]
(ptk/reify ::toggle-feature (ptk/reify ::toggle-feature
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [features (or (:features state) #{})] (assert (contains? cfeat/supported-features feature) "not supported feature")
(if (contains? features feature) (update state :features/runtime (fn [features]
(do (if (contains? features feature)
(log/debug :hint "feature disabled" :feature (d/name feature)) (do
(assoc state :features (disj features feature))) (log/trc :hint "feature disabled" :feature feature)
(do (disj features feature))
(log/debug :hint "feature enabled" :feature (d/name feature)) (do
(assoc state :features (conj features feature)))))))) (log/trc :hint "feature enabled" :feature feature)
(conj features feature))))))))
(defn- enable-feature (defn enable-feature
[feature] [feature]
(ptk/reify ::enable-feature (ptk/reify ::enable-feature
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [features (or (:features state) #{})] (assert (contains? cfeat/supported-features feature) "not supported feature")
(if (contains? features feature) (if (active-feature? state feature)
state state
(do (do
(log/debug :hint "feature enabled" :feature (d/name feature)) (log/trc :hint "feature enabled" :feature feature)
(assoc state :features (conj features feature)))))))) (update state :features/runtime (fnil conj #{}) 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?))
(defn initialize (defn initialize
[] ([] (initialize #{}))
(ptk/reify ::initialize ([team-features]
ptk/WatchEvent (assert (set? team-features) "expected a set of features")
(watch [_ _ _] (assert (every? string? team-features) "expected a set of strings")
(log/trace :hint "event:initialize" :fn "features")
(rx/concat (ptk/reify ::initialize
;; Enable all features set on the configuration ptk/UpdateEvent
(->> (rx/from cf/flags) (update [_ state]
(rx/map name) (let [runtime-features (get state :features/runtime #{})
(rx/map (fn [flag] team-features (into cfeat/default-enabled-features
(when (str/starts-with? flag "frontend-feature-") cfeat/xf-supported-features
(subs flag 17)))) team-features)]
(rx/filter some?) (-> state
(rx/map keyword) (assoc :features/runtime runtime-features)
(rx/map enable-feature)) (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))) ids)))
st/state =)) st/state =))
;; Remove this when deprecating components-v2
(def remove-graphics
(l/derived :remove-graphics st/state))
;; ---- Viewer refs ;; ---- Viewer refs
(defn lookup-viewer-objects-by-id (defn lookup-viewer-objects-by-id

View file

@ -40,7 +40,7 @@
{::mf/wrap [#(mf/catch % {:fallback on-main-error})]} {::mf/wrap [#(mf/catch % {:fallback on-main-error})]}
[{:keys [route profile]}] [{:keys [route profile]}]
(let [{:keys [data params]} route (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/current-route) {:value route}
[:& (mf/provider ctx/new-css-system) {:value new-css-system} [:& (mf/provider ctx/new-css-system) {:value new-css-system}
(case (:name data) (case (:name data)

View file

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

View file

@ -60,7 +60,7 @@
::mf/register-as :export ::mf/register-as :export
::mf/wrap-props false} ::mf/wrap-props false}
[{:keys [team-id files has-libraries? binary?]}] [{: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 state* (mf/use-state
#(let [files (mapv (fn [file] (assoc file :loading? true)) files)] #(let [files (mapv (fn [file] (assoc file :loading? true)) files)]
{:status :prepare {:status :prepare

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -89,7 +89,7 @@
{::mf/wrap-props false} {::mf/wrap-props false}
[] []
(let [modal (mf/deref modal-ref) (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 (when modal
[:& (mf/provider ctx/new-css-system) {:value new-css-system} [:& (mf/provider ctx/new-css-system) {:value new-css-system}
[:& modal-wrapper {:data modal [:& modal-wrapper {:data modal

View file

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

View file

@ -38,7 +38,6 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.globals :as globals] [app.util.globals :as globals]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[goog.events :as events] [goog.events :as events]
[okulary.core :as l] [okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -177,8 +176,8 @@
(make-file-ready-ref file-id)) (make-file-ready-ref file-id))
file-ready? (mf/deref file-ready*) file-ready? (mf/deref file-ready*)
components-v2? (features/use-feature :components-v2) components-v2? (features/use-feature "components/v2")
new-css-system (features/use-feature :new-css-system) new-css-system (features/use-feature "styles/v2")
background-color (:background-color wglobal)] background-color (:background-color wglobal)]
@ -236,49 +235,3 @@
:wglobal wglobal :wglobal wglobal
:layout layout}] :layout layout}]
[:& workspace-loader])])]]]]]]])) [:& 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 (mf/defc context-menu-component
[{:keys [shapes]}] [{:keys [shapes]}]
(let [components-v2 (features/use-feature :components-v2) (let [components-v2 (features/use-feature "components/v2")
single? (= (count shapes) 1) single? (= (count shapes) 1)
objects (deref refs/workspace-page-objects) objects (deref refs/workspace-page-objects)
any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes)) 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 modal/components
::mf/register-as :libraries-dialog} ::mf/register-as :libraries-dialog}
[{:keys [starting-tab] :as props :or {starting-tab :libraries}}] [{: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) project (mf/deref refs/workspace-project)
file-data (mf/deref refs/workspace-data) file-data (mf/deref refs/workspace-data)
file (mf/deref ref:workspace-file) file (mf/deref ref:workspace-file)

View file

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

View file

@ -100,7 +100,7 @@
(mf/defc object-svg (mf/defc object-svg
[{:keys [page-id file-id share-id object-id render-embed?]}] [{: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 fetch-state (mf/use-fn
(mf/deps file-id page-id share-id object-id components-v2) (mf/deps file-id page-id share-id object-id components-v2)
(fn [] (fn []
@ -141,7 +141,7 @@
(mf/defc objects-svg (mf/defc objects-svg
[{:keys [page-id file-id share-id object-ids render-embed?]}] [{: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 fetch-state (mf/use-fn
(mf/deps file-id page-id share-id components-v2) (mf/deps file-id page-id share-id components-v2)
(fn [] (fn []

View file

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

View file

@ -7,18 +7,23 @@
;; This namespace is only to export the functions for toggle features ;; This namespace is only to export the functions for toggle features
(ns features (ns features
(:require (:require
[app.main.features :as features])) [app.main.features :as features]
[app.main.store :as st]
(defn ^:export components-v2 [] [app.util.timers :as tm]))
(features/toggle-feature! :components-v2)
nil)
(defn ^:export is-components-v2 [] (defn ^:export is-components-v2 []
(let [active? (features/active-feature :components-v2)] (features/active-feature? @st/state "components/v2"))
@active?))
(defn ^:export new-css-system [] (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 [] (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 []
:pages-index {}} :pages-index {}}
:workspace-libraries {} :workspace-libraries {}
:features {:components-v2 true}}) :features/team #{"components/v2"}})
(def ^:private idmap (atom {})) (def ^:private idmap (atom {}))

View file

@ -4174,35 +4174,6 @@ msgstr "Oddělit uzly (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Přichytit uzly (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Přidat flexibilní rozložení" msgstr "Přidat flexibilní rozložení"

View file

@ -4453,35 +4453,6 @@ msgstr "Ankerpunkte trennen (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "An Ankerpunkten ausrichten (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Flex-Layout hinzufügen" 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 " "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." "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 #: src/app/main/errors.cljs
msgid "errors.feature-not-supported" msgid "errors.feature-not-supported"
msgstr "Feature '%s' is 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 #: 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" msgid "errors.generic"
msgstr "Something wrong has happened." msgstr "Something wrong has happened."
@ -4561,35 +4572,6 @@ msgstr "Separate nodes (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Snap nodes (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Add flex layout" 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 " "pero la aplicacion web de penpot que esta usando no tiene soporte para ella "
"o esta deshabilitada." "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 #: src/app/main/errors.cljs
msgid "errors.feature-not-supported" msgid "errors.feature-not-supported"
msgstr "Caracteristica no soportada: '%s'." msgstr "Caracteristica no soportada: '%s'."
@ -4650,35 +4657,6 @@ msgstr "Separar nodos (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Alinear nodos (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Añadir flex layout" msgstr "Añadir flex layout"

View file

@ -4173,35 +4173,6 @@ msgstr "Banatu nodoak (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Atxikitu nodoak (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Gehitu flex diseinua" msgstr "Gehitu flex diseinua"

View file

@ -4457,32 +4457,6 @@ msgstr "הפרדת מפרקים (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "הצמדת מפרקים (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "הוספת פריסת flex" msgstr "הוספת פריסת flex"

View file

@ -4585,35 +4585,6 @@ msgstr "Simpul terpisah (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Tancap simpul (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Tambahkan tata letak flex" msgstr "Tambahkan tata letak flex"

View file

@ -4568,34 +4568,6 @@ msgstr "Atdalīt mezglus (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Pieķert mezglus (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Pievienot elastīgo izkārtojumu" msgstr "Pievienot elastīgo izkārtojumu"

View file

@ -4537,35 +4537,6 @@ msgstr "Verschillende knooppunten (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Snap knooppunten (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Flex-indeling toevoegen" msgstr "Flex-indeling toevoegen"

View file

@ -4011,35 +4011,6 @@ msgstr "Rozłącz węzły (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Przyciągnij węzły (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Dodaj układ flex" msgstr "Dodaj układ flex"

View file

@ -3994,35 +3994,6 @@ msgstr "Separar pontos (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Aderir aos pontos (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Adicionar Flex Layout" msgstr "Adicionar Flex Layout"

View file

@ -4588,35 +4588,6 @@ msgstr "Separar nós (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Ajustar nós (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Adicionar layout flex" msgstr "Adicionar layout flex"

View file

@ -4626,35 +4626,6 @@ msgstr "Separă noduri (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Trage noduri (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Adăugați aspect flexibil" msgstr "Adăugați aspect flexibil"

View file

@ -4087,35 +4087,6 @@ msgstr "Düğümleri ayır (%s)"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "Düğümleri tuttur (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "Düzen esnekliği ekle" msgstr "Düzen esnekliği ekle"

View file

@ -4069,30 +4069,6 @@ msgstr "拆分节点(%s"
msgid "workspace.path.actions.snap-nodes" msgid "workspace.path.actions.snap-nodes"
msgstr "对接节点 (%s)" 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 #: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex" msgid "workspace.shape.menu.add-flex"
msgstr "添加弹性布局" msgstr "添加弹性布局"