mirror of
https://github.com/penpot/penpot.git
synced 2025-06-07 10:51:37 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
a25f069f8e
61 changed files with 1003 additions and 594 deletions
|
@ -41,15 +41,18 @@
|
||||||
(reduce-kv clojure.string/replace s replacements))
|
(reduce-kv clojure.string/replace s replacements))
|
||||||
|
|
||||||
(defn- search-user
|
(defn- search-user
|
||||||
[{:keys [conn attrs base-dn] :as cfg} email]
|
[{:keys [::conn base-dn] :as cfg} email]
|
||||||
(let [query (replace-several (:query cfg) ":username" email)
|
(let [query (replace-several (:query cfg) ":username" email)
|
||||||
|
attrs [(:attrs-username cfg)
|
||||||
|
(:attrs-email cfg)
|
||||||
|
(:attrs-fullname cfg)]
|
||||||
params {:filter query
|
params {:filter query
|
||||||
:sizelimit 1
|
:sizelimit 1
|
||||||
:attributes attrs}]
|
:attributes attrs}]
|
||||||
(first (ldap/search conn base-dn params))))
|
(first (ldap/search conn base-dn params))))
|
||||||
|
|
||||||
(defn- retrieve-user
|
(defn- retrieve-user
|
||||||
[{:keys [conn] :as cfg} {:keys [email password]}]
|
[{:keys [::conn] :as cfg} {:keys [email password]}]
|
||||||
(when-let [{:keys [dn] :as user} (search-user cfg email)]
|
(when-let [{:keys [dn] :as user} (search-user cfg email)]
|
||||||
(when (ldap/bind? conn dn password)
|
(when (ldap/bind? conn dn password)
|
||||||
{:fullname (get user (-> cfg :attrs-fullname keyword))
|
{:fullname (get user (-> cfg :attrs-fullname keyword))
|
||||||
|
@ -66,7 +69,7 @@
|
||||||
(defn authenticate
|
(defn authenticate
|
||||||
[cfg params]
|
[cfg params]
|
||||||
(with-open [conn (connect cfg)]
|
(with-open [conn (connect cfg)]
|
||||||
(when-let [user (-> (assoc cfg :conn conn)
|
(when-let [user (-> (assoc cfg ::conn conn)
|
||||||
(retrieve-user params))]
|
(retrieve-user params))]
|
||||||
(when-not (s/valid? ::info-data user)
|
(when-not (s/valid? ::info-data user)
|
||||||
(let [explain (s/explain-str ::info-data user)]
|
(let [explain (s/explain-str ::info-data user)]
|
||||||
|
@ -100,17 +103,6 @@
|
||||||
:host (:host cfg) :port (:port cfg) :cause cause)
|
:host (:host cfg) :port (:port cfg) :cause cause)
|
||||||
nil))))
|
nil))))
|
||||||
|
|
||||||
(defn- prepare-attributes
|
|
||||||
[cfg]
|
|
||||||
(assoc cfg :attrs [(:attrs-username cfg)
|
|
||||||
(:attrs-email cfg)
|
|
||||||
(:attrs-fullname cfg)]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::provider
|
|
||||||
[_ cfg]
|
|
||||||
(when (:enabled? cfg)
|
|
||||||
(some-> cfg try-connectivity prepare-attributes)))
|
|
||||||
|
|
||||||
(s/def ::enabled? ::us/boolean)
|
(s/def ::enabled? ::us/boolean)
|
||||||
(s/def ::host ::cf/ldap-host)
|
(s/def ::host ::cf/ldap-host)
|
||||||
(s/def ::port ::cf/ldap-port)
|
(s/def ::port ::cf/ldap-port)
|
||||||
|
@ -124,8 +116,7 @@
|
||||||
(s/def ::attrs-fullname ::cf/ldap-attrs-fullname)
|
(s/def ::attrs-fullname ::cf/ldap-attrs-fullname)
|
||||||
(s/def ::attrs-username ::cf/ldap-attrs-username)
|
(s/def ::attrs-username ::cf/ldap-attrs-username)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::provider
|
(s/def ::provider-params
|
||||||
[_]
|
|
||||||
(s/keys :opt-un [::host ::port
|
(s/keys :opt-un [::host ::port
|
||||||
::ssl ::tls
|
::ssl ::tls
|
||||||
::enabled?
|
::enabled?
|
||||||
|
@ -135,3 +126,14 @@
|
||||||
::attrs-email
|
::attrs-email
|
||||||
::attrs-username
|
::attrs-username
|
||||||
::attrs-fullname]))
|
::attrs-fullname]))
|
||||||
|
(s/def ::provider
|
||||||
|
(s/nilable ::provider-params))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::provider
|
||||||
|
[_]
|
||||||
|
(s/spec ::provider))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::provider
|
||||||
|
[_ cfg]
|
||||||
|
(when (:enabled? cfg)
|
||||||
|
(try-connectivity cfg)))
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
:public-uri "http://localhost:3449"
|
:public-uri "http://localhost:3449"
|
||||||
:host "localhost"
|
:host "localhost"
|
||||||
:tenant "main"
|
:tenant "default"
|
||||||
|
|
||||||
:redis-uri "redis://redis/0"
|
:redis-uri "redis://redis/0"
|
||||||
:srepl-host "127.0.0.1"
|
:srepl-host "127.0.0.1"
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"Services related to the user activity (audit log)."
|
"Services related to the user activity (audit log)."
|
||||||
(: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.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.retry :as rtry]
|
[app.util.retry :as rtry]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
@ -171,18 +173,20 @@
|
||||||
(::webhooks/event? event))
|
(::webhooks/event? event))
|
||||||
(let [batch-key (::webhooks/batch-key event)
|
(let [batch-key (::webhooks/batch-key event)
|
||||||
batch-timeout (::webhooks/batch-timeout event)
|
batch-timeout (::webhooks/batch-timeout event)
|
||||||
label-suffix (when (ifn? batch-key)
|
label (dm/str "rpc:" (:name params))
|
||||||
(str/ffmt ":%" (batch-key (:props params))))
|
label (cond
|
||||||
dedupe? (boolean
|
(ifn? batch-key) (dm/str label ":" (batch-key (::rpc/params event)))
|
||||||
(and batch-key batch-timeout))]
|
(string? batch-key) (dm/str label ":" batch-key)
|
||||||
|
:else label)
|
||||||
|
dedupe? (boolean (and batch-key batch-timeout))]
|
||||||
|
|
||||||
(wrk/submit! ::wrk/conn pool
|
(wrk/submit! ::wrk/conn pool
|
||||||
::wrk/task :process-webhook-event
|
::wrk/task :process-webhook-event
|
||||||
::wrk/queue :webhooks
|
::wrk/queue :webhooks
|
||||||
::wrk/max-retries 0
|
::wrk/max-retries 0
|
||||||
::wrk/delay (or batch-timeout 0)
|
::wrk/delay (or batch-timeout 0)
|
||||||
::wrk/dedupe dedupe?
|
::wrk/dedupe dedupe?
|
||||||
::wrk/label
|
::wrk/label label
|
||||||
(str/ffmt "rpc:%1%2" (:name params) label-suffix)
|
|
||||||
|
|
||||||
::webhooks/event
|
::webhooks/event
|
||||||
(-> params
|
(-> params
|
||||||
|
|
|
@ -11,12 +11,12 @@
|
||||||
[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.util.async :as aa]
|
[app.loggers.zmq :as lzmq]
|
||||||
[app.worker :as wrk]
|
|
||||||
[clojure.core.async :as a]
|
[clojure.core.async :as a]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]
|
||||||
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Error Listener
|
;; Error Listener
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
(defonce enabled (atom true))
|
(defonce enabled (atom true))
|
||||||
|
|
||||||
(defn- persist-on-database!
|
(defn- persist-on-database!
|
||||||
[{:keys [pool] :as cfg} {:keys [id] :as event}]
|
[{:keys [::db/pool] :as cfg} {:keys [id] :as event}]
|
||||||
(when-not (db/read-only? pool)
|
(when-not (db/read-only? pool)
|
||||||
(db/insert! pool :server-error-report {:id id :content (db/tjson event)})))
|
(db/insert! pool :server-error-report {:id id :content (db/tjson event)})))
|
||||||
|
|
||||||
|
@ -53,41 +53,49 @@
|
||||||
(assoc :version (:full cf/version))
|
(assoc :version (:full cf/version))
|
||||||
(update :id #(or % (uuid/next)))))
|
(update :id #(or % (uuid/next)))))
|
||||||
|
|
||||||
(defn handle-event
|
(defn- handle-event
|
||||||
[{:keys [executor] :as cfg} event]
|
[cfg event]
|
||||||
(aa/with-thread executor
|
(try
|
||||||
(try
|
(let [event (parse-event event)
|
||||||
(let [event (parse-event event)
|
uri (cf/get :public-uri)]
|
||||||
uri (cf/get :public-uri)]
|
|
||||||
|
|
||||||
(l/debug :hint "registering error on database" :id (:id event)
|
(l/debug :hint "registering error on database" :id (:id event)
|
||||||
:uri (str uri "/dbg/error/" (:id event)))
|
:uri (str uri "/dbg/error/" (:id event)))
|
||||||
|
|
||||||
(persist-on-database! cfg event))
|
(persist-on-database! cfg event))
|
||||||
(catch Exception cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "unexpected exception on database error logger" :cause cause)))))
|
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::reporter [_]
|
(defn- error-event?
|
||||||
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]))
|
|
||||||
|
|
||||||
(defn error-event?
|
|
||||||
[event]
|
[event]
|
||||||
(= "error" (:logger/level event)))
|
(= "error" (:logger/level event)))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::reporter [_]
|
||||||
|
(s/keys :req [::db/pool ::lzmq/receiver]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::reporter
|
(defmethod ig/init-key ::reporter
|
||||||
[_ {:keys [receiver] :as cfg}]
|
[_ {:keys [::lzmq/receiver] :as cfg}]
|
||||||
(l/info :msg "initializing database error persistence")
|
(px/thread
|
||||||
(let [output (a/chan (a/sliding-buffer 5) (filter error-event?))]
|
{:name "penpot/database-reporter"}
|
||||||
(receiver :sub output)
|
(l/info :hint "initializing database error persistence")
|
||||||
(a/go-loop []
|
|
||||||
(let [msg (a/<! output)]
|
(let [input (a/chan (a/sliding-buffer 5)
|
||||||
(if (nil? msg)
|
(filter error-event?))]
|
||||||
(l/info :msg "stopping error reporting loop")
|
(try
|
||||||
(do
|
(lzmq/sub! receiver input)
|
||||||
(a/<! (handle-event cfg msg))
|
(loop []
|
||||||
(recur)))))
|
(when-let [msg (a/<!! input)]
|
||||||
output))
|
(handle-event cfg msg))
|
||||||
|
(recur))
|
||||||
|
|
||||||
|
(catch InterruptedException _
|
||||||
|
(l/debug :hint "reporter interrupted"))
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/error :hint "unexpected error" :cause cause))
|
||||||
|
(finally
|
||||||
|
(a/close! input)
|
||||||
|
(l/info :hint "reporter terminated"))))))
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::reporter
|
(defmethod ig/halt-key! ::reporter
|
||||||
[_ output]
|
[_ thread]
|
||||||
(a/close! output))
|
(some-> thread px/interrupt!))
|
||||||
|
|
|
@ -38,13 +38,13 @@
|
||||||
|
|
||||||
(defn handle-event
|
(defn handle-event
|
||||||
[cfg event]
|
[cfg event]
|
||||||
(try
|
(when @enabled
|
||||||
(let [event (ldb/parse-event event)]
|
(try
|
||||||
(when @enabled
|
(let [event (ldb/parse-event event)]
|
||||||
(send-mattermost-notification! cfg event)))
|
(send-mattermost-notification! cfg event))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "unhandled error"
|
(l/warn :hint "unhandled error"
|
||||||
:cause cause))))
|
:cause cause)))))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::reporter [_]
|
(defmethod ig/pre-init-spec ::reporter [_]
|
||||||
(s/keys :req [::http/client
|
(s/keys :req [::http/client
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"A mattermost integration for error reporting."
|
"A mattermost integration for error reporting."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.transit :as t]
|
[app.common.transit :as t]
|
||||||
[app.common.uri :as uri]
|
[app.common.uri :as uri]
|
||||||
|
@ -21,6 +22,15 @@
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
|
;; --- HELPERS
|
||||||
|
|
||||||
|
(defn key-fn
|
||||||
|
[k & keys]
|
||||||
|
(fn [params]
|
||||||
|
(reduce #(dm/str %1 ":" (get params %2))
|
||||||
|
(dm/str (get params k))
|
||||||
|
keys)))
|
||||||
|
|
||||||
;; --- PROC
|
;; --- PROC
|
||||||
|
|
||||||
(defn- lookup-webhooks-by-team
|
(defn- lookup-webhooks-by-team
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
(ns app.main
|
(ns app.main
|
||||||
(:require
|
(:require
|
||||||
|
[app.auth.ldap :as-alias ldap]
|
||||||
[app.auth.oidc :as-alias oidc]
|
[app.auth.oidc :as-alias oidc]
|
||||||
[app.auth.oidc.providers :as-alias oidc.providers]
|
[app.auth.oidc.providers :as-alias oidc.providers]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
|
@ -231,7 +232,7 @@
|
||||||
:max-body-size (cf/get :http-server-max-body-size)
|
:max-body-size (cf/get :http-server-max-body-size)
|
||||||
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
||||||
|
|
||||||
:app.auth.ldap/provider
|
::ldap/provider
|
||||||
{:host (cf/get :ldap-host)
|
{:host (cf/get :ldap-host)
|
||||||
:port (cf/get :ldap-port)
|
:port (cf/get :ldap-port)
|
||||||
:ssl (cf/get :ldap-ssl)
|
:ssl (cf/get :ldap-ssl)
|
||||||
|
@ -327,6 +328,7 @@
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
::props (ig/ref :app.setup/props)
|
::props (ig/ref :app.setup/props)
|
||||||
|
::ldap/provider (ig/ref ::ldap/provider)
|
||||||
:pool (ig/ref ::db/pool)
|
:pool (ig/ref ::db/pool)
|
||||||
:session (ig/ref :app.http.session/manager)
|
:session (ig/ref :app.http.session/manager)
|
||||||
:sprops (ig/ref :app.setup/props)
|
:sprops (ig/ref :app.setup/props)
|
||||||
|
@ -335,7 +337,6 @@
|
||||||
:msgbus (ig/ref :app.msgbus/msgbus)
|
:msgbus (ig/ref :app.msgbus/msgbus)
|
||||||
:public-uri (cf/get :public-uri)
|
:public-uri (cf/get :public-uri)
|
||||||
:redis (ig/ref ::rds/redis)
|
:redis (ig/ref ::rds/redis)
|
||||||
:ldap (ig/ref :app.auth.ldap/provider)
|
|
||||||
:http-client (ig/ref ::http.client/client)
|
:http-client (ig/ref ::http.client/client)
|
||||||
:climit (ig/ref :app.rpc/climit)
|
:climit (ig/ref :app.rpc/climit)
|
||||||
:rlimit (ig/ref :app.rpc/rlimit)
|
:rlimit (ig/ref :app.rpc/rlimit)
|
||||||
|
@ -450,9 +451,8 @@
|
||||||
::http.client/client (ig/ref ::http.client/client)}
|
::http.client/client (ig/ref ::http.client/client)}
|
||||||
|
|
||||||
:app.loggers.database/reporter
|
:app.loggers.database/reporter
|
||||||
{:receiver (ig/ref :app.loggers.zmq/receiver)
|
{::lzmq/receiver (ig/ref :app.loggers.zmq/receiver)
|
||||||
:pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)}
|
||||||
:executor (ig/ref ::wrk/executor)}
|
|
||||||
|
|
||||||
::sto/storage
|
::sto/storage
|
||||||
{:pool (ig/ref ::db/pool)
|
{:pool (ig/ref ::db/pool)
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
(ns app.rpc
|
(ns app.rpc
|
||||||
(:require
|
(:require
|
||||||
|
[app.auth.ldap :as-alias ldap]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
|
@ -72,12 +73,14 @@
|
||||||
internal async flow into ring async flow."
|
internal async flow into ring async flow."
|
||||||
[methods {:keys [profile-id session-id params] :as request} respond raise]
|
[methods {:keys [profile-id session-id params] :as request} respond raise]
|
||||||
(let [type (keyword (:type params))
|
(let [type (keyword (:type params))
|
||||||
data (into {::http/request request} params)
|
data (-> params
|
||||||
|
(assoc ::request-at (dt/now))
|
||||||
|
(assoc ::http/request request))
|
||||||
data (if profile-id
|
data (if profile-id
|
||||||
(assoc data
|
(-> data
|
||||||
:profile-id profile-id
|
(assoc :profile-id profile-id)
|
||||||
::profile-id profile-id
|
(assoc ::profile-id profile-id)
|
||||||
::session-id session-id)
|
(assoc ::session-id session-id))
|
||||||
(dissoc data :profile-id ::profile-id))
|
(dissoc data :profile-id ::profile-id))
|
||||||
method (get methods type default-handler)]
|
method (get methods type default-handler)]
|
||||||
|
|
||||||
|
@ -93,14 +96,15 @@
|
||||||
internal async flow into ring async flow."
|
internal async flow into ring async flow."
|
||||||
[methods {:keys [profile-id session-id params] :as request} respond raise]
|
[methods {:keys [profile-id session-id params] :as request} respond raise]
|
||||||
(let [type (keyword (:type params))
|
(let [type (keyword (:type params))
|
||||||
data (into {::http/request request} params)
|
data (-> params
|
||||||
|
(assoc ::request-at (dt/now))
|
||||||
|
(assoc ::http/request request))
|
||||||
data (if profile-id
|
data (if profile-id
|
||||||
(assoc data
|
(-> data
|
||||||
:profile-id profile-id
|
(assoc :profile-id profile-id)
|
||||||
::profile-id profile-id
|
(assoc ::profile-id profile-id)
|
||||||
::session-id session-id)
|
(assoc ::session-id session-id))
|
||||||
(dissoc data :profile-id ::profile-id))
|
(dissoc data :profile-id ::profile-id))
|
||||||
|
|
||||||
method (get methods type default-handler)]
|
method (get methods type default-handler)]
|
||||||
(-> (method data)
|
(-> (method data)
|
||||||
(p/then (partial handle-response request))
|
(p/then (partial handle-response request))
|
||||||
|
@ -115,12 +119,15 @@
|
||||||
[methods {:keys [profile-id session-id params] :as request} respond raise]
|
[methods {:keys [profile-id session-id params] :as request} respond raise]
|
||||||
(let [cmd (keyword (:type params))
|
(let [cmd (keyword (:type params))
|
||||||
etag (yrq/get-header request "if-none-match")
|
etag (yrq/get-header request "if-none-match")
|
||||||
data (into {::request-at (dt/now)
|
|
||||||
::http/request request
|
data (-> params
|
||||||
::cond/key etag} params)
|
(assoc ::request-at (dt/now))
|
||||||
data (if profile-id
|
(assoc ::http/request request)
|
||||||
(assoc data ::profile-id profile-id ::session-id session-id)
|
(assoc ::cond/key etag)
|
||||||
(dissoc data ::profile-id))
|
(cond-> (uuid? profile-id)
|
||||||
|
(-> (assoc ::profile-id profile-id)
|
||||||
|
(assoc ::session-id session-id))))
|
||||||
|
|
||||||
method (get methods cmd default-handler)]
|
method (get methods cmd default-handler)]
|
||||||
(binding [cond/*enabled* true]
|
(binding [cond/*enabled* true]
|
||||||
(-> (method data)
|
(-> (method data)
|
||||||
|
@ -184,6 +191,12 @@
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:ip-addr (some-> request audit/parse-client-ip)
|
:ip-addr (some-> request audit/parse-client-ip)
|
||||||
:props props
|
:props props
|
||||||
|
|
||||||
|
;; NOTE: for batch-key lookup we need the params as-is
|
||||||
|
;; because the rpc api does not need to know the
|
||||||
|
;; audit/webhook specific object layout.
|
||||||
|
::params (dissoc params ::http/request)
|
||||||
|
|
||||||
::webhooks/batch-key
|
::webhooks/batch-key
|
||||||
(or (::webhooks/batch-key mdata)
|
(or (::webhooks/batch-key mdata)
|
||||||
(::webhooks/batch-key resultm))
|
(::webhooks/batch-key resultm))
|
||||||
|
@ -281,6 +294,7 @@
|
||||||
'app.rpc.commands.management
|
'app.rpc.commands.management
|
||||||
'app.rpc.commands.verify-token
|
'app.rpc.commands.verify-token
|
||||||
'app.rpc.commands.search
|
'app.rpc.commands.search
|
||||||
|
'app.rpc.commands.media
|
||||||
'app.rpc.commands.teams
|
'app.rpc.commands.teams
|
||||||
'app.rpc.commands.auth
|
'app.rpc.commands.auth
|
||||||
'app.rpc.commands.ldap
|
'app.rpc.commands.ldap
|
||||||
|
@ -306,6 +320,7 @@
|
||||||
(s/keys :req [::audit/collector
|
(s/keys :req [::audit/collector
|
||||||
::http.client/client
|
::http.client/client
|
||||||
::db/pool
|
::db/pool
|
||||||
|
::ldap/provider
|
||||||
::wrk/executor]
|
::wrk/executor]
|
||||||
:req-un [::sto/storage
|
:req-un [::sto/storage
|
||||||
::http.session/session
|
::http.session/session
|
||||||
|
@ -316,8 +331,7 @@
|
||||||
::climit
|
::climit
|
||||||
::wrk/executor
|
::wrk/executor
|
||||||
::mtx/metrics
|
::mtx/metrics
|
||||||
::db/pool
|
::db/pool]))
|
||||||
::ldap]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::methods
|
(defmethod ig/init-key ::methods
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
|
|
||||||
(sv/defmethod ::push-audit-events
|
(sv/defmethod ::push-audit-events
|
||||||
{::climit/queue :push-audit-events
|
{::climit/queue :push-audit-events
|
||||||
::climit/key-fn :profile-id
|
::climit/key-fn ::rpc/profile-id
|
||||||
::audit/skip true
|
::audit/skip true
|
||||||
::doc/added "1.17"}
|
::doc/added "1.17"}
|
||||||
[{:keys [::db/pool ::wrk/executor] :as cfg} params]
|
[{:keys [::db/pool ::wrk/executor] :as cfg} params]
|
||||||
|
|
|
@ -288,7 +288,9 @@
|
||||||
(sv/defmethod ::create-comment-thread
|
(sv/defmethod ::create-comment-thread
|
||||||
{::doc/added "1.15"
|
{::doc/added "1.15"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
|
[{:keys [::db/pool] :as cfg}
|
||||||
|
{:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [{:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)]
|
(let [{:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
|
|
|
@ -268,7 +268,7 @@
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::cond/get-object #(get-minimal-file %1 (:id %2))
|
::cond/get-object #(get-minimal-file %1 (:id %2))
|
||||||
::cond/key-fn get-file-etag}
|
::cond/key-fn get-file-etag}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features] :as params}]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(let [perms (get-permissions conn profile-id id)]
|
(let [perms (get-permissions conn profile-id id)]
|
||||||
(check-read-permissions! perms)
|
(check-read-permissions! perms)
|
||||||
|
@ -296,7 +296,7 @@
|
||||||
"Retrieve a file by its ID. Only authenticated users."
|
"Retrieve a file by its ID. Only authenticated users."
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::rpc/:auth false}
|
::rpc/:auth false}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(let [perms (get-permissions conn profile-id file-id share-id)]
|
(let [perms (get-permissions conn profile-id file-id share-id)]
|
||||||
(check-read-permissions! perms)
|
(check-read-permissions! perms)
|
||||||
|
@ -363,7 +363,7 @@
|
||||||
(sv/defmethod ::get-project-files
|
(sv/defmethod ::get-project-files
|
||||||
"Get all files for the specified project."
|
"Get all files for the specified project."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(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)))
|
||||||
|
@ -376,15 +376,16 @@
|
||||||
(s/def ::file-id ::us/uuid)
|
(s/def ::file-id ::us/uuid)
|
||||||
|
|
||||||
(s/def ::has-file-libraries
|
(s/def ::has-file-libraries
|
||||||
(s/keys :req [::rpc/profile-id] :req-un [::file-id]))
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::file-id]))
|
||||||
|
|
||||||
(sv/defmethod ::has-file-libraries
|
(sv/defmethod ::has-file-libraries
|
||||||
"Checks if the file has libraries. Returns a boolean"
|
"Checks if the file has libraries. Returns a boolean"
|
||||||
{::doc/added "1.15.1"}
|
{::doc/added "1.15.1"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! pool profile-id file-id)
|
(check-read-permissions! pool profile-id file-id)
|
||||||
(get-has-file-libraries conn params)))
|
(get-has-file-libraries conn file-id)))
|
||||||
|
|
||||||
(def ^:private sql:has-file-libraries
|
(def ^:private sql:has-file-libraries
|
||||||
"SELECT COUNT(*) > 0 AS has_libraries
|
"SELECT COUNT(*) > 0 AS has_libraries
|
||||||
|
@ -395,7 +396,7 @@
|
||||||
fl.deleted_at > now())")
|
fl.deleted_at > now())")
|
||||||
|
|
||||||
(defn- get-has-file-libraries
|
(defn- get-has-file-libraries
|
||||||
[conn {:keys [file-id]}]
|
[conn file-id]
|
||||||
(let [row (db/exec-one! conn [sql:has-file-libraries file-id])]
|
(let [row (db/exec-one! conn [sql:has-file-libraries file-id])]
|
||||||
(:has-libraries row)))
|
(:has-libraries row)))
|
||||||
|
|
||||||
|
@ -474,7 +475,7 @@
|
||||||
order by f.modified_at desc")
|
order by f.modified_at desc")
|
||||||
|
|
||||||
(defn get-team-shared-files
|
(defn get-team-shared-files
|
||||||
[conn {:keys [team-id] :as params}]
|
[conn team-id]
|
||||||
(letfn [(assets-sample [assets limit]
|
(letfn [(assets-sample [assets limit]
|
||||||
(let [sorted-assets (->> (vals assets)
|
(let [sorted-assets (->> (vals assets)
|
||||||
(sort-by #(str/lower (:name %))))]
|
(sort-by #(str/lower (:name %))))]
|
||||||
|
@ -494,14 +495,16 @@
|
||||||
(map #(dissoc % :data)))))))
|
(map #(dissoc % :data)))))))
|
||||||
|
|
||||||
(s/def ::get-team-shared-files
|
(s/def ::get-team-shared-files
|
||||||
(s/keys :req [::rpc/profile-id] :req-un [::team-id]))
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::team-id]))
|
||||||
|
|
||||||
(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"}
|
||||||
[{:keys [pool] :as cfg} params]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(get-team-shared-files conn params)))
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
|
(get-team-shared-files conn team-id)))
|
||||||
|
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-file-libraries
|
;; --- COMMAND QUERY: get-file-libraries
|
||||||
|
@ -552,7 +555,7 @@
|
||||||
(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"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as params}]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(get-file-libraries conn file-id features)))
|
(get-file-libraries conn file-id features)))
|
||||||
|
@ -583,7 +586,6 @@
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(get-library-file-references conn file-id)))
|
(get-library-file-references conn file-id)))
|
||||||
|
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-team-recent-files
|
;; --- COMMAND QUERY: get-team-recent-files
|
||||||
|
|
||||||
(def sql:team-recent-files
|
(def sql:team-recent-files
|
||||||
|
@ -765,7 +767,7 @@
|
||||||
;; --- MUTATION COMMAND: rename-file
|
;; --- MUTATION COMMAND: rename-file
|
||||||
|
|
||||||
(defn rename-file
|
(defn rename-file
|
||||||
[conn {:keys [id name] :as params}]
|
[conn {:keys [id name]}]
|
||||||
(db/update! conn :file
|
(db/update! conn :file
|
||||||
{:name name
|
{:name name
|
||||||
:modified-at (dt/now)}
|
:modified-at (dt/now)}
|
||||||
|
@ -899,7 +901,7 @@
|
||||||
;; --- MUTATION COMMAND: unlink-file-from-library
|
;; --- MUTATION COMMAND: unlink-file-from-library
|
||||||
|
|
||||||
(defn unlink-file-from-library
|
(defn unlink-file-from-library
|
||||||
[conn {:keys [file-id library-id] :as params}]
|
[conn {:keys [file-id library-id]}]
|
||||||
(db/delete! conn :file-library-rel
|
(db/delete! conn :file-library-rel
|
||||||
{:file-id file-id
|
{:file-id file-id
|
||||||
:library-file-id library-id}))
|
:library-file-id library-id}))
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as webhooks]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.msgbus :as mbus]
|
[app.msgbus :as mbus]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
|
@ -130,7 +130,7 @@
|
||||||
::climit/key-fn :id
|
::climit/key-fn :id
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::webhooks/batch-timeout (dt/duration "2m")
|
::webhooks/batch-timeout (dt/duration "2m")
|
||||||
::webhooks/batch-key :id
|
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||||
::doc/added "1.17"}
|
::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
|
|
|
@ -12,10 +12,13 @@
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as-alias audit]
|
[app.loggers.audit :as-alias audit]
|
||||||
|
[app.main :as-alias main]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.auth :as cmd.auth]
|
[app.rpc.commands.auth :as cmd.auth]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.queries.profile :as profile]
|
[app.rpc.queries.profile :as profile]
|
||||||
|
[app.tokens :as tokens]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
@ -34,15 +37,15 @@
|
||||||
(sv/defmethod ::login-with-ldap
|
(sv/defmethod ::login-with-ldap
|
||||||
"Performs the authentication using LDAP backend. Only works if LDAP
|
"Performs the authentication using LDAP backend. Only works if LDAP
|
||||||
is properly configured and enabled with `login-with-ldap` flag."
|
is properly configured and enabled with `login-with-ldap` flag."
|
||||||
{:auth false
|
{::rpc/auth false
|
||||||
::doc/added "1.15"}
|
::doc/added "1.15"}
|
||||||
[{:keys [session tokens ldap] :as cfg} params]
|
[{:keys [::main/props ::ldap/provider session] :as cfg} params]
|
||||||
(when-not ldap
|
(when-not provider
|
||||||
(ex/raise :type :restriction
|
(ex/raise :type :restriction
|
||||||
:code :ldap-not-initialized
|
:code :ldap-not-initialized
|
||||||
:hide "ldap auth provider is not initialized"))
|
:hide "ldap auth provider is not initialized"))
|
||||||
|
|
||||||
(let [info (ldap/authenticate ldap params)]
|
(let [info (ldap/authenticate provider params)]
|
||||||
(when-not info
|
(when-not info
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :wrong-credentials))
|
:code :wrong-credentials))
|
||||||
|
@ -58,12 +61,11 @@
|
||||||
;; user comes from team-invitation process; in this case,
|
;; user comes from team-invitation process; in this case,
|
||||||
;; regenerate token and send back to the user a new invitation
|
;; regenerate token and send back to the user a new invitation
|
||||||
;; token (and mark current session as logged).
|
;; token (and mark current session as logged).
|
||||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
(let [claims (tokens/verify props {:token token :iss :team-invitation})
|
||||||
claims (assoc claims
|
claims (assoc claims
|
||||||
:member-id (:id profile)
|
:member-id (:id profile)
|
||||||
:member-email (:email profile))
|
:member-email (:email profile))
|
||||||
token (tokens :generate claims)]
|
token (tokens/generate props claims)]
|
||||||
|
|
||||||
(-> {:invitation-token token}
|
(-> {:invitation-token token}
|
||||||
(rph/with-transform (session/create-fn session (:id profile)))
|
(rph/with-transform (session/create-fn session (:id profile)))
|
||||||
(rph/with-meta {::audit/props (:props profile)
|
(rph/with-meta {::audit/props (:props profile)
|
||||||
|
|
|
@ -46,9 +46,9 @@
|
||||||
"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}
|
||||||
[{:keys [pool] :as cfg} params]
|
[{:keys [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 (::rpc/profile-id params)))))
|
(duplicate-file conn (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
(defn- remap-id
|
(defn- remap-id
|
||||||
[item index key]
|
[item index key]
|
||||||
|
@ -136,7 +136,7 @@
|
||||||
and so.deleted_at is null")
|
and so.deleted_at is null")
|
||||||
|
|
||||||
(defn duplicate-file*
|
(defn duplicate-file*
|
||||||
[conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag] :as opts}]
|
[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:retrieve-used-libraries (:id file)]))
|
||||||
fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]))
|
fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]))
|
||||||
|
|
||||||
|
@ -329,10 +329,9 @@
|
||||||
"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 [pool] :as cfg} params]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(move-files conn (assoc params :profile-id (::rpc/profile-id params)))))
|
(move-files conn (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
|
|
||||||
;; --- COMMAND: Move project
|
;; --- COMMAND: Move project
|
||||||
|
|
||||||
|
@ -370,9 +369,9 @@
|
||||||
"Move projects between teams."
|
"Move projects between teams."
|
||||||
{::doc/added "1.16"
|
{::doc/added "1.16"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} params]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(move-project conn (assoc params :profile-id (::rpc/profile-id params)))))
|
(move-project conn (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
;; --- COMMAND: Clone Template
|
;; --- COMMAND: Clone Template
|
||||||
|
|
||||||
|
@ -387,10 +386,10 @@
|
||||||
"Clone into the specified project the template by its id."
|
"Clone into the specified project the template by its id."
|
||||||
{::doc/added "1.16"
|
{::doc/added "1.16"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} params]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(-> (assoc cfg :conn conn)
|
(-> (assoc cfg :conn conn)
|
||||||
(clone-template (assoc params :profile-id (::rpc/profile-id params))))))
|
(clone-template (assoc params :profile-id profile-id)))))
|
||||||
|
|
||||||
(defn- clone-template
|
(defn- clone-template
|
||||||
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]
|
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]
|
||||||
|
|
274
backend/src/app/rpc/commands/media.clj
Normal file
274
backend/src/app/rpc/commands/media.clj
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
;; 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.rpc.commands.media
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.media :as cm]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.http.client :as http]
|
||||||
|
[app.media :as media]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.climit :as climit]
|
||||||
|
[app.rpc.commands.files :as files]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.storage :as sto]
|
||||||
|
[app.storage.tmp :as tmp]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[datoteka.io :as io]
|
||||||
|
[promesa.core :as p]
|
||||||
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
|
(def default-max-file-size
|
||||||
|
(* 1024 1024 10)) ; 10 MiB
|
||||||
|
|
||||||
|
(def thumbnail-options
|
||||||
|
{:width 100
|
||||||
|
:height 100
|
||||||
|
:quality 85
|
||||||
|
:format :jpeg})
|
||||||
|
|
||||||
|
(s/def ::id ::us/uuid)
|
||||||
|
(s/def ::name ::us/string)
|
||||||
|
(s/def ::file-id ::us/uuid)
|
||||||
|
(s/def ::team-id ::us/uuid)
|
||||||
|
|
||||||
|
(defn validate-content-size!
|
||||||
|
[content]
|
||||||
|
(when (> (:size content) (cf/get :media-max-file-size default-max-file-size))
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :media-max-file-size-reached
|
||||||
|
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
|
||||||
|
(:size content)
|
||||||
|
default-max-file-size))))
|
||||||
|
|
||||||
|
;; --- Create File Media object (upload)
|
||||||
|
|
||||||
|
(declare create-file-media-object)
|
||||||
|
|
||||||
|
(s/def ::content ::media/upload)
|
||||||
|
(s/def ::is-local ::us/boolean)
|
||||||
|
|
||||||
|
(s/def ::upload-file-media-object
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::file-id ::is-local ::name ::content]
|
||||||
|
:opt-un [::id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::upload-file-media-object
|
||||||
|
{::doc/added "1.17"}
|
||||||
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}]
|
||||||
|
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||||
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
|
(media/validate-media-type! content)
|
||||||
|
(validate-content-size! content)
|
||||||
|
|
||||||
|
(create-file-media-object cfg params)))
|
||||||
|
|
||||||
|
(defn- big-enough-for-thumbnail?
|
||||||
|
"Checks if the provided image info is big enough for
|
||||||
|
create a separate thumbnail storage object."
|
||||||
|
[info]
|
||||||
|
(or (> (:width info) (:width thumbnail-options))
|
||||||
|
(> (:height info) (:height thumbnail-options))))
|
||||||
|
|
||||||
|
(defn- svg-image?
|
||||||
|
[info]
|
||||||
|
(= (:mtype info) "image/svg+xml"))
|
||||||
|
|
||||||
|
;; NOTE: we use the `on conflict do update` instead of `do nothing`
|
||||||
|
;; because postgresql does not returns anything if no update is
|
||||||
|
;; performed, the `do update` does the trick.
|
||||||
|
|
||||||
|
(def sql:create-file-media-object
|
||||||
|
"insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype)
|
||||||
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
on conflict (id) do update set created_at=file_media_object.created_at
|
||||||
|
returning *")
|
||||||
|
|
||||||
|
;; NOTE: the following function executes without a transaction, this
|
||||||
|
;; means that if something fails in the middle of this function, it
|
||||||
|
;; will probably leave leaked/unreferenced objects in the database and
|
||||||
|
;; probably in the storage layer. For handle possible object leakage,
|
||||||
|
;; we create all media objects marked as touched, this ensures that if
|
||||||
|
;; something fails, all leaked (already created storage objects) will
|
||||||
|
;; be eventually marked as deleted by the touched-gc task.
|
||||||
|
;;
|
||||||
|
;; The touched-gc task, performs periodic analysis of all touched
|
||||||
|
;; storage objects and check references of it. This is the reason why
|
||||||
|
;; `reference` metadata exists: it indicates the name of the table
|
||||||
|
;; witch holds the reference to storage object (it some kind of
|
||||||
|
;; inverse, soft referential integrity).
|
||||||
|
|
||||||
|
(defn create-file-media-object
|
||||||
|
[{:keys [storage pool climit executor]}
|
||||||
|
{:keys [id file-id is-local name content]}]
|
||||||
|
(letfn [;; Function responsible to retrieve the file information, as
|
||||||
|
;; it is synchronous operation it should be wrapped into
|
||||||
|
;; with-dispatch macro.
|
||||||
|
(get-info [content]
|
||||||
|
(climit/with-dispatch (:process-image climit)
|
||||||
|
(media/run {:cmd :info :input content})))
|
||||||
|
|
||||||
|
;; Function responsible of calculating cryptographyc hash of
|
||||||
|
;; the provided data.
|
||||||
|
(calculate-hash [data]
|
||||||
|
(px/with-dispatch executor
|
||||||
|
(sto/calculate-hash data)))
|
||||||
|
|
||||||
|
;; Function responsible of generating thumnail. As it is synchronous
|
||||||
|
;; opetation, it should be wrapped into with-dispatch macro
|
||||||
|
(generate-thumbnail [info]
|
||||||
|
(climit/with-dispatch (:process-image climit)
|
||||||
|
(media/run (assoc thumbnail-options
|
||||||
|
:cmd :generic-thumbnail
|
||||||
|
:input info))))
|
||||||
|
|
||||||
|
(create-thumbnail [info]
|
||||||
|
(when (and (not (svg-image? info))
|
||||||
|
(big-enough-for-thumbnail? info))
|
||||||
|
(p/let [thumb (generate-thumbnail info)
|
||||||
|
hash (calculate-hash (:data thumb))
|
||||||
|
content (-> (sto/content (:data thumb) (:size thumb))
|
||||||
|
(sto/wrap-with-hash hash))]
|
||||||
|
(sto/put-object! storage
|
||||||
|
{::sto/content content
|
||||||
|
::sto/deduplicate? true
|
||||||
|
::sto/touched-at (dt/now)
|
||||||
|
:content-type (:mtype thumb)
|
||||||
|
:bucket "file-media-object"}))))
|
||||||
|
|
||||||
|
(create-image [info]
|
||||||
|
(p/let [data (:path info)
|
||||||
|
hash (calculate-hash data)
|
||||||
|
content (-> (sto/content data)
|
||||||
|
(sto/wrap-with-hash hash))]
|
||||||
|
(sto/put-object! storage
|
||||||
|
{::sto/content content
|
||||||
|
::sto/deduplicate? true
|
||||||
|
::sto/touched-at (dt/now)
|
||||||
|
:content-type (:mtype info)
|
||||||
|
:bucket "file-media-object"})))
|
||||||
|
|
||||||
|
(insert-into-database [info image thumb]
|
||||||
|
(px/with-dispatch executor
|
||||||
|
(db/exec-one! pool [sql:create-file-media-object
|
||||||
|
(or id (uuid/next))
|
||||||
|
file-id is-local name
|
||||||
|
(:id image)
|
||||||
|
(:id thumb)
|
||||||
|
(:width info)
|
||||||
|
(:height info)
|
||||||
|
(:mtype info)])))]
|
||||||
|
|
||||||
|
(p/let [info (get-info content)
|
||||||
|
thumb (create-thumbnail info)
|
||||||
|
image (create-image info)]
|
||||||
|
(insert-into-database info image thumb))))
|
||||||
|
|
||||||
|
;; --- Create File Media Object (from URL)
|
||||||
|
|
||||||
|
(declare ^:private create-file-media-object-from-url)
|
||||||
|
|
||||||
|
(s/def ::create-file-media-object-from-url
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::file-id ::is-local ::url]
|
||||||
|
:opt-un [::id ::name]))
|
||||||
|
|
||||||
|
(sv/defmethod ::create-file-media-object-from-url
|
||||||
|
{::doc/added "1.17"}
|
||||||
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
|
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||||
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
|
(create-file-media-object-from-url cfg params)))
|
||||||
|
|
||||||
|
(defn- create-file-media-object-from-url
|
||||||
|
[cfg {:keys [url name] :as params}]
|
||||||
|
(letfn [(parse-and-validate-size [headers]
|
||||||
|
(let [size (some-> (get headers "content-length") d/parse-integer)
|
||||||
|
mtype (get headers "content-type")
|
||||||
|
format (cm/mtype->format mtype)
|
||||||
|
max-size (cf/get :media-max-file-size default-max-file-size)]
|
||||||
|
|
||||||
|
(when-not size
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :unknown-size
|
||||||
|
:hint "seems like the url points to resource with unknown size"))
|
||||||
|
|
||||||
|
(when (> size max-size)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :file-too-large
|
||||||
|
:hint (str/ffmt "the file size % is greater than the maximum %"
|
||||||
|
size
|
||||||
|
default-max-file-size)))
|
||||||
|
|
||||||
|
(when (nil? format)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :media-type-not-allowed
|
||||||
|
:hint "seems like the url points to an invalid media object"))
|
||||||
|
|
||||||
|
{:size size
|
||||||
|
:mtype mtype
|
||||||
|
:format format}))
|
||||||
|
|
||||||
|
(download-media [uri]
|
||||||
|
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
|
||||||
|
(p/then process-response)))
|
||||||
|
|
||||||
|
(process-response [{:keys [body headers] :as response}]
|
||||||
|
(let [{:keys [size mtype]} (parse-and-validate-size headers)
|
||||||
|
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||||
|
written (io/write-to-file! body path :size size)]
|
||||||
|
|
||||||
|
(when (not= written size)
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :mismatch-write-size
|
||||||
|
:hint "unexpected state: unable to write to file"))
|
||||||
|
|
||||||
|
{:filename "tempfile"
|
||||||
|
:size size
|
||||||
|
:path path
|
||||||
|
:mtype mtype}))]
|
||||||
|
|
||||||
|
(p/let [content (download-media url)]
|
||||||
|
(->> (merge params {:content content :name (or name (:filename content))})
|
||||||
|
(create-file-media-object cfg)))))
|
||||||
|
|
||||||
|
;; --- Clone File Media object (Upload and create from url)
|
||||||
|
|
||||||
|
(declare clone-file-media-object)
|
||||||
|
|
||||||
|
(s/def ::clone-file-media-object
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::file-id ::is-local ::id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::clone-file-media-object
|
||||||
|
{::doc/added "1.17"}
|
||||||
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
|
(-> (assoc cfg :conn conn)
|
||||||
|
(clone-file-media-object params))))
|
||||||
|
|
||||||
|
(defn clone-file-media-object
|
||||||
|
[{:keys [conn]} {:keys [id file-id is-local]}]
|
||||||
|
(let [mobj (db/get-by-id conn :file-media-object id)]
|
||||||
|
(db/insert! conn :file-media-object
|
||||||
|
{:id (uuid/next)
|
||||||
|
:file-id file-id
|
||||||
|
:is-local is-local
|
||||||
|
:name (:name mobj)
|
||||||
|
:media-id (:media-id mobj)
|
||||||
|
:thumbnail-id (:thumbnail-id mobj)
|
||||||
|
:width (:width mobj)
|
||||||
|
:height (:height mobj)
|
||||||
|
:mtype (:mtype mobj)})))
|
|
@ -34,10 +34,10 @@
|
||||||
{::climit/queue :auth
|
{::climit/queue :auth
|
||||||
::climit/key-fn ::rpc/profile-id
|
::climit/key-fn ::rpc/profile-id
|
||||||
::doc/added "1.18"}
|
::doc/added "1.18"}
|
||||||
[{:keys [::db/pool]} {:keys [password] :as params}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id password]}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [admins (cf/get :admins)
|
(let [admins (cf/get :admins)
|
||||||
profile (db/get-by-id conn :profile (::rpc/profile-id params))]
|
profile (db/get-by-id conn :profile profile-id)]
|
||||||
|
|
||||||
(if (or (:is-admin profile)
|
(if (or (:is-admin profile)
|
||||||
(contains? admins (:email profile)))
|
(contains? admins (:email profile)))
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
order by f.created_at asc")
|
order by f.created_at asc")
|
||||||
|
|
||||||
(defn search-files
|
(defn search-files
|
||||||
[conn {:keys [::rpc/profile-id team-id search-term] :as params}]
|
[conn profile-id team-id search-term]
|
||||||
(db/exec! conn [sql:search-files
|
(db/exec! conn [sql:search-files
|
||||||
profile-id team-id
|
profile-id team-id
|
||||||
profile-id team-id
|
profile-id team-id
|
||||||
|
@ -64,6 +64,5 @@
|
||||||
|
|
||||||
(sv/defmethod ::search-files
|
(sv/defmethod ::search-files
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool]} {:keys [search-term] :as params}]
|
[{:keys [pool]} {:keys [::rpc/profile-id team-id search-term]}]
|
||||||
(when search-term
|
(some->> search-term (search-files pool profile-id team-id)))
|
||||||
(search-files pool params)))
|
|
||||||
|
|
|
@ -385,14 +385,8 @@
|
||||||
|
|
||||||
(declare role->params)
|
(declare role->params)
|
||||||
|
|
||||||
(s/def ::reassign-to ::us/uuid)
|
|
||||||
(s/def ::leave-team
|
|
||||||
(s/keys :req [::rpc/profile-id]
|
|
||||||
:req-un [::id]
|
|
||||||
:opt-un [::reassign-to]))
|
|
||||||
|
|
||||||
(defn leave-team
|
(defn leave-team
|
||||||
[conn {:keys [::rpc/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 (retrieve-team-members conn id)]
|
||||||
|
|
||||||
|
@ -437,12 +431,17 @@
|
||||||
|
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
|
(s/def ::reassign-to ::us/uuid)
|
||||||
|
(s/def ::leave-team
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::id]
|
||||||
|
:opt-un [::reassign-to]))
|
||||||
|
|
||||||
(sv/defmethod ::leave-team
|
(sv/defmethod ::leave-team
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} params]
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(leave-team conn params)))
|
(leave-team conn (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
;; --- Mutation: Delete Team
|
;; --- Mutation: Delete Team
|
||||||
|
|
||||||
|
@ -539,9 +538,9 @@
|
||||||
|
|
||||||
(sv/defmethod ::update-team-member-role
|
(sv/defmethod ::update-team-member-role
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} params]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(update-team-member-role conn (assoc params :profile-id (::rpc/profile-id params)))))
|
(update-team-member-role conn (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Delete Team Member
|
;; --- Mutation: Delete Team Member
|
||||||
|
|
|
@ -84,6 +84,6 @@
|
||||||
::cond/key-fn files/get-file-etag
|
::cond/key-fn files/get-file-etag
|
||||||
::cond/reuse-key? true
|
::cond/reuse-key? true
|
||||||
::doc/added "1.17"}
|
::doc/added "1.17"}
|
||||||
[{:keys [pool]} params]
|
[{:keys [pool]} {:keys [::rpc/profile-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(get-view-only-bundle conn (assoc params :profile-id (::rpc/profile-id params)))))
|
(get-view-only-bundle conn (assoc params :profile-id profile-id))))
|
||||||
|
|
|
@ -6,280 +6,49 @@
|
||||||
|
|
||||||
(ns app.rpc.mutations.media
|
(ns app.rpc.mutations.media
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.media :as cm]
|
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.config :as cf]
|
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.client :as http]
|
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc.climit :as climit]
|
[app.rpc.commands.files :as files]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.media :as cmd.media]
|
||||||
[app.storage :as sto]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.storage.tmp :as tmp]
|
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[clojure.spec.alpha :as s]))
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[datoteka.io :as io]
|
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
(def default-max-file-size (* 1024 1024 10)) ; 10 MiB
|
|
||||||
|
|
||||||
(def thumbnail-options
|
|
||||||
{:width 100
|
|
||||||
:height 100
|
|
||||||
:quality 85
|
|
||||||
:format :jpeg})
|
|
||||||
|
|
||||||
(s/def ::id ::us/uuid)
|
|
||||||
(s/def ::name ::us/string)
|
|
||||||
(s/def ::profile-id ::us/uuid)
|
|
||||||
(s/def ::file-id ::us/uuid)
|
|
||||||
(s/def ::team-id ::us/uuid)
|
|
||||||
|
|
||||||
;; --- Create File Media object (upload)
|
;; --- Create File Media object (upload)
|
||||||
|
|
||||||
(declare create-file-media-object)
|
(s/def ::upload-file-media-object ::cmd.media/upload-file-media-object)
|
||||||
(declare select-file)
|
|
||||||
|
|
||||||
(s/def ::content ::media/upload)
|
|
||||||
(s/def ::is-local ::us/boolean)
|
|
||||||
|
|
||||||
(s/def ::upload-file-media-object
|
|
||||||
(s/keys :req-un [::profile-id ::file-id ::is-local ::name ::content]
|
|
||||||
:opt-un [::id]))
|
|
||||||
|
|
||||||
(sv/defmethod ::upload-file-media-object
|
(sv/defmethod ::upload-file-media-object
|
||||||
|
{::doc/added "1.2"
|
||||||
|
::doc/deprecated "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}]
|
||||||
(let [file (select-file pool file-id)
|
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||||
cfg (update cfg :storage media/configure-assets-storage)]
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
|
|
||||||
(teams/check-edition-permissions! pool profile-id (:team-id file))
|
|
||||||
(media/validate-media-type! content)
|
(media/validate-media-type! content)
|
||||||
|
(cmd.media/validate-content-size! content)
|
||||||
(when (> (:size content) (cf/get :media-max-file-size default-max-file-size))
|
(cmd.media/create-file-media-object cfg params)))
|
||||||
(ex/raise :type :restriction
|
|
||||||
:code :media-max-file-size-reached
|
|
||||||
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
|
|
||||||
(:size content)
|
|
||||||
default-max-file-size)))
|
|
||||||
|
|
||||||
(create-file-media-object cfg params)))
|
|
||||||
|
|
||||||
(defn- big-enough-for-thumbnail?
|
|
||||||
"Checks if the provided image info is big enough for
|
|
||||||
create a separate thumbnail storage object."
|
|
||||||
[info]
|
|
||||||
(or (> (:width info) (:width thumbnail-options))
|
|
||||||
(> (:height info) (:height thumbnail-options))))
|
|
||||||
|
|
||||||
(defn- svg-image?
|
|
||||||
[info]
|
|
||||||
(= (:mtype info) "image/svg+xml"))
|
|
||||||
|
|
||||||
;; NOTE: we use the `on conflict do update` instead of `do nothing`
|
|
||||||
;; because postgresql does not returns anything if no update is
|
|
||||||
;; performed, the `do update` does the trick.
|
|
||||||
|
|
||||||
(def sql:create-file-media-object
|
|
||||||
"insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype)
|
|
||||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
on conflict (id) do update set created_at=file_media_object.created_at
|
|
||||||
returning *")
|
|
||||||
|
|
||||||
;; NOTE: the following function executes without a transaction, this
|
|
||||||
;; means that if something fails in the middle of this function, it
|
|
||||||
;; will probably leave leaked/unreferenced objects in the database and
|
|
||||||
;; probably in the storage layer. For handle possible object leakage,
|
|
||||||
;; we create all media objects marked as touched, this ensures that if
|
|
||||||
;; something fails, all leaked (already created storage objects) will
|
|
||||||
;; be eventually marked as deleted by the touched-gc task.
|
|
||||||
;;
|
|
||||||
;; The touched-gc task, performs periodic analysis of all touched
|
|
||||||
;; storage objects and check references of it. This is the reason why
|
|
||||||
;; `reference` metadata exists: it indicates the name of the table
|
|
||||||
;; witch holds the reference to storage object (it some kind of
|
|
||||||
;; inverse, soft referential integrity).
|
|
||||||
|
|
||||||
(defn create-file-media-object
|
|
||||||
[{:keys [storage pool climit executor] :as cfg}
|
|
||||||
{:keys [id file-id is-local name content] :as params}]
|
|
||||||
(letfn [;; Function responsible to retrieve the file information, as
|
|
||||||
;; it is synchronous operation it should be wrapped into
|
|
||||||
;; with-dispatch macro.
|
|
||||||
(get-info [content]
|
|
||||||
(climit/with-dispatch (:process-image climit)
|
|
||||||
(media/run {:cmd :info :input content})))
|
|
||||||
|
|
||||||
;; Function responsible of calculating cryptographyc hash of
|
|
||||||
;; the provided data.
|
|
||||||
(calculate-hash [data]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(sto/calculate-hash data)))
|
|
||||||
|
|
||||||
;; Function responsible of generating thumnail. As it is synchronous
|
|
||||||
;; opetation, it should be wrapped into with-dispatch macro
|
|
||||||
(generate-thumbnail [info]
|
|
||||||
(climit/with-dispatch (:process-image climit)
|
|
||||||
(media/run (assoc thumbnail-options
|
|
||||||
:cmd :generic-thumbnail
|
|
||||||
:input info))))
|
|
||||||
|
|
||||||
(create-thumbnail [info]
|
|
||||||
(when (and (not (svg-image? info))
|
|
||||||
(big-enough-for-thumbnail? info))
|
|
||||||
(p/let [thumb (generate-thumbnail info)
|
|
||||||
hash (calculate-hash (:data thumb))
|
|
||||||
content (-> (sto/content (:data thumb) (:size thumb))
|
|
||||||
(sto/wrap-with-hash hash))]
|
|
||||||
(sto/put-object! storage
|
|
||||||
{::sto/content content
|
|
||||||
::sto/deduplicate? true
|
|
||||||
::sto/touched-at (dt/now)
|
|
||||||
:content-type (:mtype thumb)
|
|
||||||
:bucket "file-media-object"}))))
|
|
||||||
|
|
||||||
(create-image [info]
|
|
||||||
(p/let [data (:path info)
|
|
||||||
hash (calculate-hash data)
|
|
||||||
content (-> (sto/content data)
|
|
||||||
(sto/wrap-with-hash hash))]
|
|
||||||
(sto/put-object! storage
|
|
||||||
{::sto/content content
|
|
||||||
::sto/deduplicate? true
|
|
||||||
::sto/touched-at (dt/now)
|
|
||||||
:content-type (:mtype info)
|
|
||||||
:bucket "file-media-object"})))
|
|
||||||
|
|
||||||
(insert-into-database [info image thumb]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(db/exec-one! pool [sql:create-file-media-object
|
|
||||||
(or id (uuid/next))
|
|
||||||
file-id is-local name
|
|
||||||
(:id image)
|
|
||||||
(:id thumb)
|
|
||||||
(:width info)
|
|
||||||
(:height info)
|
|
||||||
(:mtype info)])))]
|
|
||||||
|
|
||||||
(p/let [info (get-info content)
|
|
||||||
thumb (create-thumbnail info)
|
|
||||||
image (create-image info)]
|
|
||||||
(insert-into-database info image thumb))))
|
|
||||||
|
|
||||||
;; --- Create File Media Object (from URL)
|
;; --- Create File Media Object (from URL)
|
||||||
|
|
||||||
(declare ^:private create-file-media-object-from-url)
|
(s/def ::create-file-media-object-from-url ::cmd.media/create-file-media-object-from-url)
|
||||||
|
|
||||||
(s/def ::create-file-media-object-from-url
|
|
||||||
(s/keys :req-un [::profile-id ::file-id ::is-local ::url]
|
|
||||||
:opt-un [::id ::name]))
|
|
||||||
|
|
||||||
(sv/defmethod ::create-file-media-object-from-url
|
(sv/defmethod ::create-file-media-object-from-url
|
||||||
|
{::doc/added "1.3"
|
||||||
|
::doc/deprecated "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(let [file (select-file pool file-id)
|
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||||
cfg (update cfg :storage media/configure-assets-storage)]
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
(teams/check-edition-permissions! pool profile-id (:team-id file))
|
(#'cmd.media/create-file-media-object-from-url cfg params)))
|
||||||
(create-file-media-object-from-url cfg params)))
|
|
||||||
|
|
||||||
(defn- create-file-media-object-from-url
|
|
||||||
[cfg {:keys [url name] :as params}]
|
|
||||||
(letfn [(parse-and-validate-size [headers]
|
|
||||||
(let [size (some-> (get headers "content-length") d/parse-integer)
|
|
||||||
mtype (get headers "content-type")
|
|
||||||
format (cm/mtype->format mtype)
|
|
||||||
max-size (cf/get :media-max-file-size default-max-file-size)]
|
|
||||||
|
|
||||||
(when-not size
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :unknown-size
|
|
||||||
:hint "seems like the url points to resource with unknown size"))
|
|
||||||
|
|
||||||
(when (> size max-size)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :file-too-large
|
|
||||||
:hint (str/ffmt "the file size % is greater than the maximum %"
|
|
||||||
size
|
|
||||||
default-max-file-size)))
|
|
||||||
|
|
||||||
(when (nil? format)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :media-type-not-allowed
|
|
||||||
:hint "seems like the url points to an invalid media object"))
|
|
||||||
|
|
||||||
{:size size
|
|
||||||
:mtype mtype
|
|
||||||
:format format}))
|
|
||||||
|
|
||||||
(download-media [uri]
|
|
||||||
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
|
|
||||||
(p/then process-response)))
|
|
||||||
|
|
||||||
(process-response [{:keys [body headers] :as response}]
|
|
||||||
(let [{:keys [size mtype]} (parse-and-validate-size headers)
|
|
||||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
|
||||||
written (io/write-to-file! body path :size size)]
|
|
||||||
|
|
||||||
(when (not= written size)
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :mismatch-write-size
|
|
||||||
:hint "unexpected state: unable to write to file"))
|
|
||||||
|
|
||||||
{:filename "tempfile"
|
|
||||||
:size size
|
|
||||||
:path path
|
|
||||||
:mtype mtype}))]
|
|
||||||
|
|
||||||
(p/let [content (download-media url)]
|
|
||||||
(->> (merge params {:content content :name (or name (:filename content))})
|
|
||||||
(create-file-media-object cfg)))))
|
|
||||||
|
|
||||||
;; --- Clone File Media object (Upload and create from url)
|
;; --- Clone File Media object (Upload and create from url)
|
||||||
|
|
||||||
(declare clone-file-media-object)
|
(s/def ::clone-file-media-object ::cmd.media/clone-file-media-object)
|
||||||
|
|
||||||
(s/def ::clone-file-media-object
|
|
||||||
(s/keys :req-un [::profile-id ::file-id ::is-local ::id]))
|
|
||||||
|
|
||||||
(sv/defmethod ::clone-file-media-object
|
(sv/defmethod ::clone-file-media-object
|
||||||
|
{::doc/added "1.2"
|
||||||
|
::doc/deprecated "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [file (select-file conn file-id)]
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
(teams/check-edition-permissions! conn profile-id (:team-id file))
|
(-> (assoc cfg :conn conn)
|
||||||
(-> (assoc cfg :conn conn)
|
(cmd.media/clone-file-media-object params))))
|
||||||
(clone-file-media-object params)))))
|
|
||||||
|
|
||||||
(defn clone-file-media-object
|
|
||||||
[{:keys [conn] :as cfg} {:keys [id file-id is-local]}]
|
|
||||||
(let [mobj (db/get-by-id conn :file-media-object id)]
|
|
||||||
(db/insert! conn :file-media-object
|
|
||||||
{:id (uuid/next)
|
|
||||||
:file-id file-id
|
|
||||||
:is-local is-local
|
|
||||||
:name (:name mobj)
|
|
||||||
:media-id (:media-id mobj)
|
|
||||||
:thumbnail-id (:thumbnail-id mobj)
|
|
||||||
:width (:width mobj)
|
|
||||||
:height (:height mobj)
|
|
||||||
:mtype (:mtype mobj)})))
|
|
||||||
|
|
||||||
;; --- HELPERS
|
|
||||||
|
|
||||||
(def ^:private
|
|
||||||
sql:select-file
|
|
||||||
"select file.*,
|
|
||||||
project.team_id as team_id
|
|
||||||
from file
|
|
||||||
inner join project on (project.id = file.project_id)
|
|
||||||
where file.id = ?")
|
|
||||||
|
|
||||||
(defn- select-file
|
|
||||||
[conn id]
|
|
||||||
(let [row (db/exec-one! conn [sql:select-file id])]
|
|
||||||
(when-not row
|
|
||||||
(ex/raise :type :not-found))
|
|
||||||
row))
|
|
||||||
|
|
|
@ -179,6 +179,5 @@
|
||||||
(sv/defmethod ::search-files
|
(sv/defmethod ::search-files
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
::doc/deprecated "1.17"}
|
::doc/deprecated "1.17"}
|
||||||
[{:keys [pool]} {:keys [search-term] :as params}]
|
[{:keys [pool]} {:keys [profile-id team-id search-term]}]
|
||||||
(when search-term
|
(some->> search-term (search/search-files pool profile-id team-id)))
|
||||||
(search/search-files pool params)))
|
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
|
|
||||||
(ns backend-tests.rpc-file-test
|
(ns backend-tests.rpc-file-test
|
||||||
(:require
|
(:require
|
||||||
[backend-tests.helpers :as th]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.db.sql :as sql]
|
[app.db.sql :as sql]
|
||||||
[app.http :as http]
|
[app.http :as http]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[backend-tests.helpers :as th]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[datoteka.core :as fs]))
|
[datoteka.core :as fs]))
|
||||||
|
|
||||||
|
@ -28,13 +29,13 @@
|
||||||
|
|
||||||
(t/testing "create file"
|
(t/testing "create file"
|
||||||
(let [data {::th/type :create-file
|
(let [data {::th/type :create-file
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:project-id proj-id
|
:project-id proj-id
|
||||||
:id file-id
|
:id file-id
|
||||||
:name "foobar"
|
:name "foobar"
|
||||||
:is-shared false
|
:is-shared false
|
||||||
:components-v2 true}
|
:components-v2 true}
|
||||||
out (th/mutation! data)]
|
out (th/command! data)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
|
@ -47,8 +48,8 @@
|
||||||
(let [data {::th/type :rename-file
|
(let [data {::th/type :rename-file
|
||||||
:id file-id
|
:id file-id
|
||||||
:name "new name"
|
:name "new name"
|
||||||
:profile-id (:id prof)}
|
::rpc/profile-id (:id prof)}
|
||||||
out (th/mutation! data)]
|
out (th/command! data)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
|
@ -56,10 +57,10 @@
|
||||||
(t/is (= (:name data) (:name result))))))
|
(t/is (= (:name data) (:name result))))))
|
||||||
|
|
||||||
(t/testing "query files"
|
(t/testing "query files"
|
||||||
(let [data {::th/type :project-files
|
(let [data {::th/type :get-project-files
|
||||||
:project-id proj-id
|
::rpc/profile-id (:id prof)
|
||||||
:profile-id (:id prof)}
|
:project-id proj-id}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
|
@ -70,11 +71,11 @@
|
||||||
(t/is (= "new name" (get-in result [0 :name]))))))
|
(t/is (= "new name" (get-in result [0 :name]))))))
|
||||||
|
|
||||||
(t/testing "query single file without users"
|
(t/testing "query single file without users"
|
||||||
(let [data {::th/type :file
|
(let [data {::th/type :get-file
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:id file-id
|
:id file-id
|
||||||
:components-v2 true}
|
:components-v2 true}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
|
@ -88,18 +89,18 @@
|
||||||
(t/testing "delete file"
|
(t/testing "delete file"
|
||||||
(let [data {::th/type :delete-file
|
(let [data {::th/type :delete-file
|
||||||
:id file-id
|
:id file-id
|
||||||
:profile-id (:id prof)}
|
::rpc/profile-id (:id prof)}
|
||||||
out (th/mutation! data)]
|
out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(t/is (nil? (:result out)))))
|
(t/is (nil? (:result out)))))
|
||||||
|
|
||||||
(t/testing "query single file after delete"
|
(t/testing "query single file after delete"
|
||||||
(let [data {::th/type :file
|
(let [data {::th/type :get-file
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:id file-id
|
:id file-id
|
||||||
:components-v2 true}
|
:components-v2 true}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
|
||||||
|
@ -109,10 +110,10 @@
|
||||||
(t/is (= (:type error-data) :not-found)))))
|
(t/is (= (:type error-data) :not-found)))))
|
||||||
|
|
||||||
(t/testing "query list files after delete"
|
(t/testing "query list files after delete"
|
||||||
(let [data {::th/type :project-files
|
(let [data {::th/type :get-project-files
|
||||||
:project-id proj-id
|
::rpc/profile-id (:id prof)
|
||||||
:profile-id (:id prof)}
|
:project-id proj-id}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
|
@ -136,19 +137,18 @@
|
||||||
out (th/mutation! params)]
|
out (th/mutation! params)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(:result out)))
|
(:result out)))
|
||||||
|
|
||||||
(update-file [{:keys [profile-id file-id changes revn] :or {revn 0}}]
|
(update-file [{:keys [profile-id file-id changes revn] :or {revn 0}}]
|
||||||
(let [params {::th/type :update-file
|
(let [params {::th/type :update-file
|
||||||
|
::rpc/profile-id profile-id
|
||||||
:id file-id
|
:id file-id
|
||||||
:session-id (uuid/random)
|
:session-id (uuid/random)
|
||||||
:profile-id profile-id
|
|
||||||
:revn revn
|
:revn revn
|
||||||
:components-v2 true
|
:components-v2 true
|
||||||
:changes changes}
|
:changes changes}
|
||||||
out (th/mutation! params)]
|
out (th/command! params)]
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(:result out)))]
|
(:result out)))]
|
||||||
|
|
||||||
|
@ -257,12 +257,12 @@
|
||||||
profile2 (th/create-profile* 2)
|
profile2 (th/create-profile* 2)
|
||||||
|
|
||||||
data {::th/type :create-file
|
data {::th/type :create-file
|
||||||
:profile-id (:id profile2)
|
::rpc/profile-id (:id profile2)
|
||||||
:project-id (:default-project-id profile1)
|
:project-id (:default-project-id profile1)
|
||||||
:name "foobar"
|
:name "foobar"
|
||||||
:is-shared false
|
:is-shared false
|
||||||
:components-v2 true}
|
:components-v2 true}
|
||||||
out (th/mutation! data)
|
out (th/command! data)
|
||||||
error (:error out)]
|
error (:error out)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
@ -277,9 +277,9 @@
|
||||||
:profile-id (:id profile1)})
|
:profile-id (:id profile1)})
|
||||||
data {::th/type :rename-file
|
data {::th/type :rename-file
|
||||||
:id (:id file)
|
:id (:id file)
|
||||||
:profile-id (:id profile2)
|
::rpc/profile-id (:id profile2)
|
||||||
:name "foobar"}
|
:name "foobar"}
|
||||||
out (th/mutation! data)
|
out (th/command! data)
|
||||||
error (:error out)]
|
error (:error out)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
@ -293,9 +293,9 @@
|
||||||
file (th/create-file* 1 {:project-id (:default-project-id profile1)
|
file (th/create-file* 1 {:project-id (:default-project-id profile1)
|
||||||
:profile-id (:id profile1)})
|
:profile-id (:id profile1)})
|
||||||
data {::th/type :delete-file
|
data {::th/type :delete-file
|
||||||
:profile-id (:id profile2)
|
::rpc/profile-id (:id profile2)
|
||||||
:id (:id file)}
|
:id (:id file)}
|
||||||
out (th/mutation! data)
|
out (th/command! data)
|
||||||
error (:error out)]
|
error (:error out)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
@ -308,10 +308,10 @@
|
||||||
file (th/create-file* 1 {:project-id (:default-project-id profile1)
|
file (th/create-file* 1 {:project-id (:default-project-id profile1)
|
||||||
:profile-id (:id profile1)})
|
:profile-id (:id profile1)})
|
||||||
data {::th/type :set-file-shared
|
data {::th/type :set-file-shared
|
||||||
:profile-id (:id profile2)
|
::rpc/profile-id (:id profile2)
|
||||||
:id (:id file)
|
:id (:id file)
|
||||||
:is-shared true}
|
:is-shared true}
|
||||||
out (th/mutation! data)
|
out (th/command! data)
|
||||||
error (:error out)]
|
error (:error out)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
@ -328,11 +328,11 @@
|
||||||
:profile-id (:id profile1)})
|
:profile-id (:id profile1)})
|
||||||
|
|
||||||
data {::th/type :link-file-to-library
|
data {::th/type :link-file-to-library
|
||||||
:profile-id (:id profile2)
|
::rpc/profile-id (:id profile2)
|
||||||
:file-id (:id file2)
|
:file-id (:id file2)
|
||||||
:library-id (:id file1)}
|
:library-id (:id file1)}
|
||||||
|
|
||||||
out (th/mutation! data)
|
out (th/command! data)
|
||||||
error (:error out)]
|
error (:error out)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
@ -350,11 +350,11 @@
|
||||||
:profile-id (:id profile2)})
|
:profile-id (:id profile2)})
|
||||||
|
|
||||||
data {::th/type :link-file-to-library
|
data {::th/type :link-file-to-library
|
||||||
:profile-id (:id profile2)
|
::rpc/profile-id (:id profile2)
|
||||||
:file-id (:id file2)
|
:file-id (:id file2)
|
||||||
:library-id (:id file1)}
|
:library-id (:id file1)}
|
||||||
|
|
||||||
out (th/mutation! data)
|
out (th/command! data)
|
||||||
error (:error out)]
|
error (:error out)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
@ -372,10 +372,10 @@
|
||||||
(t/is (= 0 (:processed result))))
|
(t/is (= 0 (:processed result))))
|
||||||
|
|
||||||
;; query the list of files
|
;; query the list of files
|
||||||
(let [data {::th/type :project-files
|
(let [data {::th/type :get-project-files
|
||||||
:project-id (:default-project-id profile1)
|
::rpc/profile-id (:id profile1)
|
||||||
:profile-id (:id profile1)}
|
:project-id (:default-project-id profile1)}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
|
@ -384,15 +384,15 @@
|
||||||
;; Request file to be deleted
|
;; Request file to be deleted
|
||||||
(let [params {::th/type :delete-file
|
(let [params {::th/type :delete-file
|
||||||
:id (:id file)
|
:id (:id file)
|
||||||
:profile-id (:id profile1)}
|
::rpc/profile-id (:id profile1)}
|
||||||
out (th/mutation! params)]
|
out (th/command! params)]
|
||||||
(t/is (nil? (:error out))))
|
(t/is (nil? (:error out))))
|
||||||
|
|
||||||
;; query the list of files after soft deletion
|
;; query the list of files after soft deletion
|
||||||
(let [data {::th/type :project-files
|
(let [data {::th/type :get-project-files
|
||||||
:project-id (:default-project-id profile1)
|
::rpc/profile-id (:id profile1)
|
||||||
:profile-id (:id profile1)}
|
:project-id (:default-project-id profile1)}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
|
@ -403,10 +403,10 @@
|
||||||
(t/is (= 0 (:processed result))))
|
(t/is (= 0 (:processed result))))
|
||||||
|
|
||||||
;; query the list of file libraries of a after hard deletion
|
;; query the list of file libraries of a after hard deletion
|
||||||
(let [data {::th/type :file-libraries
|
(let [data {::th/type :get-file-libraries
|
||||||
:file-id (:id file)
|
::rpc/profile-id (:id profile1)
|
||||||
:profile-id (:id profile1)}
|
:file-id (:id file)}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
|
@ -417,10 +417,10 @@
|
||||||
(t/is (= 1 (:processed result))))
|
(t/is (= 1 (:processed result))))
|
||||||
|
|
||||||
;; query the list of file libraries of a after hard deletion
|
;; query the list of file libraries of a after hard deletion
|
||||||
(let [data {::th/type :file-libraries
|
(let [data {::th/type :get-file-libraries
|
||||||
:file-id (:id file)
|
::rpc/profile-id (:id profile1)
|
||||||
:profile-id (:id profile1)}
|
:file-id (:id file)}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(let [error (:error out)
|
(let [error (:error out)
|
||||||
error-data (ex-data error)]
|
error-data (ex-data error)]
|
||||||
|
@ -483,11 +483,11 @@
|
||||||
(t/testing "RPC page query (rendering purposes)"
|
(t/testing "RPC page query (rendering purposes)"
|
||||||
|
|
||||||
;; Query :page RPC method without passing page-id
|
;; Query :page RPC method without passing page-id
|
||||||
(let [data {::th/type :page
|
(let [data {::th/type :get-page
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:components-v2 true}
|
:components-v2 true}
|
||||||
{:keys [error result] :as out} (th/query! 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))
|
||||||
|
@ -500,12 +500,12 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
;; Query :page RPC method with page-id
|
;; Query :page RPC method with page-id
|
||||||
(let [data {::th/type :page
|
(let [data {::th/type :get-page
|
||||||
: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}
|
:components-v2 true}
|
||||||
{:keys [error result] :as out} (th/query! 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))
|
||||||
(t/is (contains? result :objects))
|
(t/is (contains? result :objects))
|
||||||
|
@ -516,13 +516,13 @@
|
||||||
(t/is (contains? (:objects result) uuid/zero)))
|
(t/is (contains? (:objects result) uuid/zero)))
|
||||||
|
|
||||||
;; Query :page RPC method with page-id and object-id
|
;; Query :page RPC method with page-id and object-id
|
||||||
(let [data {::th/type :page
|
(let [data {::th/type :get-page
|
||||||
: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
|
||||||
:object-id frame1-id
|
:object-id frame1-id
|
||||||
:components-v2 true}
|
:components-v2 true}
|
||||||
{:keys [error result] :as out} (th/query! 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))
|
||||||
(t/is (map? result))
|
(t/is (map? result))
|
||||||
|
@ -534,12 +534,12 @@
|
||||||
(t/is (not (contains? (:objects result) shape2-id))))
|
(t/is (not (contains? (:objects result) shape2-id))))
|
||||||
|
|
||||||
;; Query :page RPC method with wrong params
|
;; Query :page RPC method with wrong params
|
||||||
(let [data {::th/type :page
|
(let [data {::th/type :get-page
|
||||||
: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}
|
:components-v2 true}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
(t/is (not (th/success? out)))
|
||||||
(let [{:keys [type code]} (-> out :error ex-data)]
|
(let [{:keys [type code]} (-> out :error ex-data)]
|
||||||
|
@ -551,21 +551,21 @@
|
||||||
(t/testing "RPC :file-data-for-thumbnail"
|
(t/testing "RPC :file-data-for-thumbnail"
|
||||||
;; Insert a thumbnail data for the frame-id
|
;; Insert a thumbnail data for the frame-id
|
||||||
(let [data {::th/type :upsert-file-object-thumbnail
|
(let [data {::th/type :upsert-file-object-thumbnail
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:object-id (str page-id frame1-id)
|
:object-id (str page-id frame1-id)
|
||||||
:data "random-data-1"}
|
:data "random-data-1"}
|
||||||
|
|
||||||
{:keys [error result] :as out} (th/mutation! data)]
|
{:keys [error result] :as out} (th/command! data)]
|
||||||
(t/is (nil? error))
|
(t/is (nil? error))
|
||||||
(t/is (nil? result)))
|
(t/is (nil? result)))
|
||||||
|
|
||||||
;; Check the result
|
;; Check the result
|
||||||
(let [data {::th/type :file-data-for-thumbnail
|
(let [data {::th/type :get-file-data-for-thumbnail
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:components-v2 true}
|
:components-v2 true}
|
||||||
{:keys [error result] :as out} (th/query! 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))
|
||||||
(t/is (contains? result :page))
|
(t/is (contains? result :page))
|
||||||
|
@ -578,21 +578,21 @@
|
||||||
|
|
||||||
;; Delete thumbnail data
|
;; Delete thumbnail data
|
||||||
(let [data {::th/type :upsert-file-object-thumbnail
|
(let [data {::th/type :upsert-file-object-thumbnail
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:object-id (str page-id frame1-id)
|
:object-id (str page-id frame1-id)
|
||||||
:data nil}
|
:data nil}
|
||||||
{:keys [error result] :as out} (th/mutation! 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))
|
||||||
(t/is (nil? result)))
|
(t/is (nil? result)))
|
||||||
|
|
||||||
;; Check the result
|
;; Check the result
|
||||||
(let [data {::th/type :file-data-for-thumbnail
|
(let [data {::th/type :get-file-data-for-thumbnail
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:components-v2 true}
|
:components-v2 true}
|
||||||
{:keys [error result] :as out} (th/query! 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))
|
||||||
(t/is (contains? result :page))
|
(t/is (contains? result :page))
|
||||||
|
@ -606,11 +606,11 @@
|
||||||
|
|
||||||
;; insert object snapshot for known frame
|
;; insert object snapshot for known frame
|
||||||
(let [data {::th/type :upsert-file-object-thumbnail
|
(let [data {::th/type :upsert-file-object-thumbnail
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:object-id (str page-id frame1-id)
|
:object-id (str page-id frame1-id)
|
||||||
:data "new-data"}
|
:data "new-data"}
|
||||||
{:keys [error result] :as out} (th/mutation! data)]
|
{:keys [error result] :as out} (th/command! data)]
|
||||||
(t/is (nil? error))
|
(t/is (nil? error))
|
||||||
(t/is (nil? result)))
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
@ -629,11 +629,11 @@
|
||||||
|
|
||||||
;; insert object snapshot for for unknown frame
|
;; insert object snapshot for for unknown frame
|
||||||
(let [data {::th/type :upsert-file-object-thumbnail
|
(let [data {::th/type :upsert-file-object-thumbnail
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:object-id (str page-id (uuid/next))
|
:object-id (str page-id (uuid/next))
|
||||||
:data "new-data-2"}
|
:data "new-data-2"}
|
||||||
{:keys [error result] :as out} (th/mutation! data)]
|
{:keys [error result] :as out} (th/command! data)]
|
||||||
(t/is (nil? error))
|
(t/is (nil? error))
|
||||||
(t/is (nil? result)))
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
@ -661,8 +661,8 @@
|
||||||
:project-id (:default-project-id prof)
|
:project-id (:default-project-id prof)
|
||||||
:revn 2
|
:revn 2
|
||||||
:is-shared false})
|
:is-shared false})
|
||||||
data {::th/type :file-thumbnail
|
data {::th/type :get-file-thumbnail
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:file-id (:id file)}]
|
:file-id (:id file)}]
|
||||||
|
|
||||||
(t/testing "query a thumbnail with single revn"
|
(t/testing "query a thumbnail with single revn"
|
||||||
|
@ -673,7 +673,7 @@
|
||||||
:revn 1
|
:revn 1
|
||||||
:data "testvalue1"})
|
:data "testvalue1"})
|
||||||
|
|
||||||
(let [{:keys [result error] :as out} (th/query! data)]
|
(let [{:keys [result error] :as out} (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? error))
|
(t/is (nil? error))
|
||||||
(t/is (= 4 (count result)))
|
(t/is (= 4 (count result)))
|
||||||
|
@ -687,7 +687,7 @@
|
||||||
:revn 2
|
:revn 2
|
||||||
:data "testvalue2"})
|
:data "testvalue2"})
|
||||||
|
|
||||||
(let [{:keys [result error] :as out} (th/query! data)]
|
(let [{:keys [result error] :as out} (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? error))
|
(t/is (nil? error))
|
||||||
(t/is (= 4 (count result)))
|
(t/is (= 4 (count result)))
|
||||||
|
@ -695,7 +695,7 @@
|
||||||
(t/is (= 2 (:revn result))))
|
(t/is (= 2 (:revn result))))
|
||||||
|
|
||||||
;; Then query the specific revn
|
;; Then query the specific revn
|
||||||
(let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))]
|
(let [{:keys [result error] :as out} (th/command! (assoc data :revn 1))]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? error))
|
(t/is (nil? error))
|
||||||
(t/is (= 4 (count result)))
|
(t/is (= 4 (count result)))
|
||||||
|
@ -704,18 +704,18 @@
|
||||||
|
|
||||||
(t/testing "upsert file-thumbnail"
|
(t/testing "upsert file-thumbnail"
|
||||||
(let [data {::th/type :upsert-file-thumbnail
|
(let [data {::th/type :upsert-file-thumbnail
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:data "foobar"
|
:data "foobar"
|
||||||
:props {:baz 1}
|
:props {:baz 1}
|
||||||
:revn 2}
|
:revn 2}
|
||||||
{:keys [result error] :as out} (th/mutation! data)]
|
{:keys [result error] :as out} (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? error))
|
(t/is (nil? error))
|
||||||
(t/is (nil? result))))
|
(t/is (nil? result))))
|
||||||
|
|
||||||
(t/testing "query last result"
|
(t/testing "query last result"
|
||||||
(let [{:keys [result error] :as out} (th/query! data)]
|
(let [{:keys [result error] :as out} (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? error))
|
(t/is (nil? error))
|
||||||
(t/is (= 4 (count result)))
|
(t/is (= 4 (count result)))
|
||||||
|
@ -734,7 +734,7 @@
|
||||||
(t/is (= 1 (:processed res))))
|
(t/is (= 1 (:processed res))))
|
||||||
|
|
||||||
;; Then query the specific revn
|
;; Then query the specific revn
|
||||||
(let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))]
|
(let [{:keys [result error] :as out} (th/command! (assoc data :revn 1))]
|
||||||
(t/is (th/ex-of-type? error :not-found))
|
(t/is (th/ex-of-type? error :not-found))
|
||||||
(t/is (th/ex-of-code? error :file-thumbnail-not-found))))
|
(t/is (th/ex-of-code? error :file-thumbnail-not-found))))
|
||||||
))
|
))
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
|
|
||||||
(ns backend-tests.rpc-media-test
|
(ns backend-tests.rpc-media-test
|
||||||
(:require
|
(:require
|
||||||
[backend-tests.helpers :as th]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
|
[backend-tests.helpers :as th]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[datoteka.core :as fs]))
|
[datoteka.core :as fs]))
|
||||||
|
|
||||||
|
@ -134,3 +135,123 @@
|
||||||
(t/is (= "image/jpeg" (:mtype result)))
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
(t/is (uuid? (:media-id result)))
|
(t/is (uuid? (:media-id result)))
|
||||||
(t/is (uuid? (:thumbnail-id result))))))
|
(t/is (uuid? (:thumbnail-id result))))))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest media-object-from-url-command
|
||||||
|
(let [prof (th/create-profile* 1)
|
||||||
|
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||||
|
:team-id (:default-team-id prof)})
|
||||||
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
|
:project-id (:default-project-id prof)
|
||||||
|
:is-shared false})
|
||||||
|
url "https://raw.githubusercontent.com/uxbox/uxbox/develop/sample_media/images/unsplash/anna-pelzer.jpg"
|
||||||
|
params {::th/type :create-file-media-object-from-url
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:file-id (:id file)
|
||||||
|
:is-local true
|
||||||
|
:url url}
|
||||||
|
out (th/command! params)]
|
||||||
|
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
|
||||||
|
(t/is (= (:id file) (:file-id result)))
|
||||||
|
(t/is (= 1024 (:width result)))
|
||||||
|
(t/is (= 683 (:height result)))
|
||||||
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
|
(t/is (uuid? media-id))
|
||||||
|
(t/is (uuid? thumbnail-id))
|
||||||
|
(let [storage (:app.storage/storage th/*system*)
|
||||||
|
mobj1 @(sto/get-object storage media-id)
|
||||||
|
mobj2 @(sto/get-object storage thumbnail-id)]
|
||||||
|
(t/is (sto/storage-object? mobj1))
|
||||||
|
(t/is (sto/storage-object? mobj2))
|
||||||
|
(t/is (= 122785 (:size mobj1)))
|
||||||
|
;; This is because in ubuntu 21.04 generates different
|
||||||
|
;; thumbnail that in ubuntu 22.04. This hack should be removed
|
||||||
|
;; when we all use the ubuntu 22.04 devenv image.
|
||||||
|
(t/is (or (= 3302 (:size mobj2))
|
||||||
|
(= 3303 (:size mobj2))))))))
|
||||||
|
|
||||||
|
(t/deftest media-object-upload-command
|
||||||
|
(let [prof (th/create-profile* 1)
|
||||||
|
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||||
|
:team-id (:default-team-id prof)})
|
||||||
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
|
:project-id (:default-project-id prof)
|
||||||
|
:is-shared false})
|
||||||
|
mfile {:filename "sample.jpg"
|
||||||
|
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
||||||
|
:mtype "image/jpeg"
|
||||||
|
:size 312043}
|
||||||
|
|
||||||
|
params {::th/type :upload-file-media-object
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:file-id (:id file)
|
||||||
|
:is-local true
|
||||||
|
:name "testfile"
|
||||||
|
:content mfile}
|
||||||
|
out (th/command! params)]
|
||||||
|
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
|
||||||
|
(t/is (= (:id file) (:file-id result)))
|
||||||
|
(t/is (= 800 (:width result)))
|
||||||
|
(t/is (= 800 (:height result)))
|
||||||
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
|
(t/is (uuid? media-id))
|
||||||
|
(t/is (uuid? thumbnail-id))
|
||||||
|
(let [storage (:app.storage/storage th/*system*)
|
||||||
|
mobj1 @(sto/get-object storage media-id)
|
||||||
|
mobj2 @(sto/get-object storage thumbnail-id)]
|
||||||
|
(t/is (sto/storage-object? mobj1))
|
||||||
|
(t/is (sto/storage-object? mobj2))
|
||||||
|
(t/is (= 312043 (:size mobj1)))
|
||||||
|
(t/is (= 3887 (:size mobj2)))))
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest media-object-upload-idempotency-command
|
||||||
|
(let [prof (th/create-profile* 1)
|
||||||
|
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||||
|
:team-id (:default-team-id prof)})
|
||||||
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
|
:project-id (:default-project-id prof)
|
||||||
|
:is-shared false})
|
||||||
|
mfile {:filename "sample.jpg"
|
||||||
|
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
||||||
|
:mtype "image/jpeg"
|
||||||
|
:size 312043}
|
||||||
|
|
||||||
|
params {::th/type :upload-file-media-object
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:file-id (:id file)
|
||||||
|
:is-local true
|
||||||
|
:name "testfile"
|
||||||
|
:content mfile
|
||||||
|
:id (uuid/next)}]
|
||||||
|
|
||||||
|
;; First try
|
||||||
|
(let [{:keys [result error] :as out} (th/command! params)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? error))
|
||||||
|
(t/is (= (:id params) (:id result)))
|
||||||
|
(t/is (= (:file-id params) (:file-id result)))
|
||||||
|
(t/is (= 800 (:width result)))
|
||||||
|
(t/is (= 800 (:height result)))
|
||||||
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
|
(t/is (uuid? (:media-id result)))
|
||||||
|
(t/is (uuid? (:thumbnail-id result))))
|
||||||
|
|
||||||
|
;; Second try
|
||||||
|
(let [{:keys [result error] :as out} (th/command! params)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? error))
|
||||||
|
(t/is (= (:id params) (:id result)))
|
||||||
|
(t/is (= (:file-id params) (:file-id result)))
|
||||||
|
(t/is (= 800 (:width result)))
|
||||||
|
(t/is (= 800 (:height result)))
|
||||||
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
|
(t/is (uuid? (:media-id result)))
|
||||||
|
(t/is (uuid? (:thumbnail-id result))))))
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
(t/use-fixtures :once th/state-init)
|
(t/use-fixtures :once th/state-init)
|
||||||
(t/use-fixtures :each th/database-reset)
|
(t/use-fixtures :each th/database-reset)
|
||||||
|
|
||||||
(t/deftest invite-team-member
|
(t/deftest create-team-invitations
|
||||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||||
profile2 (th/create-profile* 2 {:is-active true})
|
profile2 (th/create-profile* 2 {:is-active true})
|
||||||
|
@ -30,14 +30,14 @@
|
||||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||||
|
|
||||||
pool (:app.db/pool th/*system*)
|
pool (:app.db/pool th/*system*)
|
||||||
data {::th/type :invite-team-member
|
data {::th/type :create-team-invitations
|
||||||
|
::rpc/profile-id (:id profile1)
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:role :editor
|
:role :editor}]
|
||||||
:profile-id (:id profile1)}]
|
|
||||||
|
|
||||||
;; invite external user without complaints
|
;; invite external user without complaints
|
||||||
(let [data (assoc data :email "foo@bar.com")
|
(let [data (assoc data :email "foo@bar.com")
|
||||||
out (th/mutation! data)
|
out (th/command! data)
|
||||||
;; retrieve the value from the database and check its content
|
;; retrieve the value from the database and check its content
|
||||||
invitation (db/exec-one!
|
invitation (db/exec-one!
|
||||||
th/*pool*
|
th/*pool*
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
;; invite internal user without complaints
|
;; invite internal user without complaints
|
||||||
(th/reset-mock! mock)
|
(th/reset-mock! mock)
|
||||||
(let [data (assoc data :email (:email profile2))
|
(let [data (assoc data :email (:email profile2))
|
||||||
out (th/mutation! data)]
|
out (th/command! data)]
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(t/is (= 1 (:call-count (deref mock)))))
|
(t/is (= 1 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
(th/create-global-complaint-for pool {:type :complaint :email "foo@bar.com"})
|
(th/create-global-complaint-for pool {:type :complaint :email "foo@bar.com"})
|
||||||
(th/reset-mock! mock)
|
(th/reset-mock! mock)
|
||||||
(let [data (assoc data :email "foo@bar.com")
|
(let [data (assoc data :email "foo@bar.com")
|
||||||
out (th/mutation! data)]
|
out (th/command! data)]
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(t/is (= 1 (:call-count (deref mock)))))
|
(t/is (= 1 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
|
|
||||||
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
|
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
|
||||||
(let [data (assoc data :email "foo@bar.com")
|
(let [data (assoc data :email "foo@bar.com")
|
||||||
out (th/mutation! data)]
|
out (th/command! data)]
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
(t/is (not (th/success? out)))
|
||||||
(t/is (= 0 (:call-count @mock)))
|
(t/is (= 0 (:call-count @mock)))
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
(th/reset-mock! mock)
|
(th/reset-mock! mock)
|
||||||
|
|
||||||
(let [data (assoc data :email (:email profile3))
|
(let [data (assoc data :email (:email profile3))
|
||||||
out (th/mutation! data)]
|
out (th/command! data)]
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
(t/is (not (th/success? out)))
|
||||||
(t/is (= 0 (:call-count @mock)))
|
(t/is (= 0 (:call-count @mock)))
|
||||||
|
@ -115,12 +115,12 @@
|
||||||
pool (:app.db/pool th/*system*)]
|
pool (:app.db/pool th/*system*)]
|
||||||
|
|
||||||
;; Try to invite a not existing user
|
;; Try to invite a not existing user
|
||||||
(let [data {::th/type :invite-team-member
|
(let [data {::th/type :create-team-invitations
|
||||||
|
::rpc/profile-id (:id profile1)
|
||||||
:email "notexisting@example.com"
|
:email "notexisting@example.com"
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:role :editor
|
:role :editor}
|
||||||
:profile-id (:id profile1)}
|
out (th/command! data)]
|
||||||
out (th/mutation! data)]
|
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
|
@ -139,12 +139,12 @@
|
||||||
(th/reset-mock! mock)
|
(th/reset-mock! mock)
|
||||||
|
|
||||||
;; Try to invite existing user
|
;; Try to invite existing user
|
||||||
(let [data {::th/type :invite-team-member
|
(let [data {::th/type :create-team-invitations
|
||||||
|
::rpc/profile-id (:id profile1)
|
||||||
:email (:email profile2)
|
:email (:email profile2)
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:role :editor
|
:role :editor}
|
||||||
:profile-id (:id profile1)}
|
out (th/command! data)]
|
||||||
out (th/mutation! data)]
|
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
|
@ -215,7 +215,9 @@
|
||||||
:role "editor"
|
:role "editor"
|
||||||
:valid-until (dt/in-future "48h")})
|
:valid-until (dt/in-future "48h")})
|
||||||
|
|
||||||
(let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile2)}
|
(let [data {::th/type :verify-token
|
||||||
|
::rpc/profile-id (:id profile2)
|
||||||
|
:token token}
|
||||||
out (th/command! data)]
|
out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
|
@ -236,7 +238,9 @@
|
||||||
:role "editor"
|
:role "editor"
|
||||||
:valid-until (dt/in-future "48h")})
|
:valid-until (dt/in-future "48h")})
|
||||||
|
|
||||||
(let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile1)}
|
(let [data {::th/type :verify-token
|
||||||
|
::rpc/profile-id (:id profile1)
|
||||||
|
:token token}
|
||||||
out (th/command! data)]
|
out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (not (th/success? out)))
|
(t/is (not (th/success? out)))
|
||||||
|
@ -246,7 +250,7 @@
|
||||||
|
|
||||||
)))
|
)))
|
||||||
|
|
||||||
(t/deftest invite-team-member-with-email-verification-disabled
|
(t/deftest create-team-invitations-with-email-verification-disabled
|
||||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||||
profile2 (th/create-profile* 2 {:is-active true})
|
profile2 (th/create-profile* 2 {:is-active true})
|
||||||
|
@ -255,16 +259,16 @@
|
||||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||||
|
|
||||||
pool (:app.db/pool th/*system*)
|
pool (:app.db/pool th/*system*)
|
||||||
data {::th/type :invite-team-member
|
data {::th/type :create-team-invitations
|
||||||
|
::rpc/profile-id (:id profile1)
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:role :editor
|
:role :editor}]
|
||||||
:profile-id (:id profile1)}]
|
|
||||||
|
|
||||||
;; invite internal user without complaints
|
;; invite internal user without complaints
|
||||||
(with-redefs [app.config/flags #{}]
|
(with-redefs [app.config/flags #{}]
|
||||||
(th/reset-mock! mock)
|
(th/reset-mock! mock)
|
||||||
(let [data (assoc data :email (:email profile2))
|
(let [data (assoc data :email (:email profile2))
|
||||||
out (th/mutation! data)]
|
out (th/command! data)]
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(t/is (= 0 (:call-count (deref mock)))))
|
(t/is (= 0 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
@ -279,8 +283,8 @@
|
||||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||||
pool (:app.db/pool th/*system*)
|
pool (:app.db/pool th/*system*)
|
||||||
data {::th/type :delete-team
|
data {::th/type :delete-team
|
||||||
:team-id (:id team)
|
::rpc/profile-id (:id profile1)
|
||||||
:profile-id (:id profile1)}]
|
:team-id (:id team)}]
|
||||||
|
|
||||||
;; team is not deleted because it does not meet all
|
;; team is not deleted because it does not meet all
|
||||||
;; conditions to be deleted.
|
;; conditions to be deleted.
|
||||||
|
@ -288,9 +292,9 @@
|
||||||
(t/is (= 0 (:processed result))))
|
(t/is (= 0 (:processed result))))
|
||||||
|
|
||||||
;; query the list of teams
|
;; query the list of teams
|
||||||
(let [data {::th/type :teams
|
(let [data {::th/type :get-teams
|
||||||
:profile-id (:id profile1)}
|
::rpc/profile-id (:id profile1)}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
|
@ -300,15 +304,15 @@
|
||||||
|
|
||||||
;; Request team to be deleted
|
;; Request team to be deleted
|
||||||
(let [params {::th/type :delete-team
|
(let [params {::th/type :delete-team
|
||||||
:id (:id team)
|
::rpc/profile-id (:id profile1)
|
||||||
:profile-id (:id profile1)}
|
:id (:id team)}
|
||||||
out (th/mutation! params)]
|
out (th/command! params)]
|
||||||
(t/is (th/success? out)))
|
(t/is (th/success? out)))
|
||||||
|
|
||||||
;; query the list of teams after soft deletion
|
;; query the list of teams after soft deletion
|
||||||
(let [data {::th/type :teams
|
(let [data {::th/type :get-teams
|
||||||
:profile-id (:id profile1)}
|
::rpc/profile-id (:id profile1)}
|
||||||
out (th/query! data)]
|
out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
|
@ -321,8 +325,8 @@
|
||||||
|
|
||||||
;; query the list of projects after hard deletion
|
;; query the list of projects after hard deletion
|
||||||
(let [data {::th/type :projects
|
(let [data {::th/type :projects
|
||||||
:team-id (:id team)
|
:profile-id (:id profile1)
|
||||||
:profile-id (:id profile1)}
|
:team-id (:id team)}
|
||||||
out (th/query! data)]
|
out (th/query! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (not (th/success? out)))
|
(t/is (not (th/success? out)))
|
||||||
|
@ -335,8 +339,8 @@
|
||||||
|
|
||||||
;; query the list of projects of a after hard deletion
|
;; query the list of projects of a after hard deletion
|
||||||
(let [data {::th/type :projects
|
(let [data {::th/type :projects
|
||||||
:team-id (:id team)
|
:profile-id (:id profile1)
|
||||||
:profile-id (:id profile1)}
|
:team-id (:id team)}
|
||||||
out (th/query! data)]
|
out (th/query! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
|
||||||
|
@ -348,8 +352,8 @@
|
||||||
(t/deftest query-team-invitations
|
(t/deftest query-team-invitations
|
||||||
(let [prof (th/create-profile* 1 {:is-active true})
|
(let [prof (th/create-profile* 1 {:is-active true})
|
||||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||||
data {::th/type :team-invitations
|
data {::th/type :get-team-invitations
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:team-id (:id team)}]
|
:team-id (:id team)}]
|
||||||
|
|
||||||
;; insert an entry on the database with an enabled invitation
|
;; insert an entry on the database with an enabled invitation
|
||||||
|
@ -366,7 +370,7 @@
|
||||||
:role "editor"
|
:role "editor"
|
||||||
:valid-until (dt/in-past "48h")})
|
:valid-until (dt/in-past "48h")})
|
||||||
|
|
||||||
(let [out (th/query! data)]
|
(let [out (th/command! data)]
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(let [result (:result out)
|
(let [result (:result out)
|
||||||
one (first result)
|
one (first result)
|
||||||
|
@ -381,7 +385,7 @@
|
||||||
(let [prof (th/create-profile* 1 {:is-active true})
|
(let [prof (th/create-profile* 1 {:is-active true})
|
||||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||||
data {::th/type :update-team-invitation-role
|
data {::th/type :update-team-invitation-role
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:email "TEST1@mail.com"
|
:email "TEST1@mail.com"
|
||||||
:role :admin}]
|
:role :admin}]
|
||||||
|
@ -393,7 +397,7 @@
|
||||||
:role "editor"
|
:role "editor"
|
||||||
:valid-until (dt/in-future "48h")})
|
:valid-until (dt/in-future "48h")})
|
||||||
|
|
||||||
(let [out (th/mutation! data)
|
(let [out (th/command! data)
|
||||||
;; retrieve the value from the database and check its content
|
;; retrieve the value from the database and check its content
|
||||||
res (db/get* th/*pool* :team-invitation
|
res (db/get* th/*pool* :team-invitation
|
||||||
{:team-id (:team-id data) :email-to "test1@mail.com"})]
|
{:team-id (:team-id data) :email-to "test1@mail.com"})]
|
||||||
|
@ -405,7 +409,7 @@
|
||||||
(let [prof (th/create-profile* 1 {:is-active true})
|
(let [prof (th/create-profile* 1 {:is-active true})
|
||||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||||
data {::th/type :delete-team-invitation
|
data {::th/type :delete-team-invitation
|
||||||
:profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:email "TEST1@mail.com"}]
|
:email "TEST1@mail.com"}]
|
||||||
|
|
||||||
|
@ -416,7 +420,7 @@
|
||||||
:role "editor"
|
:role "editor"
|
||||||
:valid-until (dt/in-future "48h")})
|
:valid-until (dt/in-future "48h")})
|
||||||
|
|
||||||
(let [out (th/mutation! data)
|
(let [out (th/command! data)
|
||||||
;; retrieve the value from the database and check its content
|
;; retrieve the value from the database and check its content
|
||||||
res (db/get* th/*pool* :team-invitation
|
res (db/get* th/*pool* :team-invitation
|
||||||
{:team-id (:team-id data) :email-to "test1@mail.com"})]
|
{:team-id (:team-id data) :email-to "test1@mail.com"})]
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
|
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
|
||||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||||
|
|
||||||
funcool/promesa {:mvn/version "10.0.571"}
|
funcool/promesa {:mvn/version "10.0.594"}
|
||||||
funcool/cuerdas {:mvn/version "2022.06.16-403"}
|
funcool/cuerdas {:mvn/version "2022.06.16-403"}
|
||||||
|
|
||||||
lambdaisland/uri {:mvn/version "1.13.95"
|
lambdaisland/uri {:mvn/version "1.13.95"
|
||||||
|
|
|
@ -89,9 +89,9 @@
|
||||||
(contains? data :explain))
|
(contains? data :explain))
|
||||||
(explain (:explain data) opts)
|
(explain (:explain data) opts)
|
||||||
|
|
||||||
(and (::s/problems data)
|
(and (contains? data ::s/problems)
|
||||||
(::s/value data)
|
(contains? data ::s/value)
|
||||||
(::s/spec data))
|
(contains? data ::s/spec))
|
||||||
(binding [s/*explain-out* expound/printer]
|
(binding [s/*explain-out* expound/printer]
|
||||||
(with-out-str
|
(with-out-str
|
||||||
(s/explain-out (update data ::s/problems #(take max-problems %))))))))
|
(s/explain-out (update data ::s/problems #(take max-problems %))))))))
|
||||||
|
|
|
@ -43,14 +43,16 @@
|
||||||
(mapv #(gpt/add % move-vec))))
|
(mapv #(gpt/add % move-vec))))
|
||||||
|
|
||||||
(defn move-position-data
|
(defn move-position-data
|
||||||
[position-data dx dy]
|
([position-data {:keys [x y]}]
|
||||||
|
(move-position-data position-data x y))
|
||||||
|
|
||||||
(when (some? position-data)
|
([position-data dx dy]
|
||||||
(cond->> position-data
|
(when (some? position-data)
|
||||||
(d/num? dx dy)
|
(cond->> position-data
|
||||||
(mapv #(-> %
|
(d/num? dx dy)
|
||||||
(update :x + dx)
|
(mapv #(-> %
|
||||||
(update :y + dy))))))
|
(update :x + dx)
|
||||||
|
(update :y + dy)))))))
|
||||||
|
|
||||||
(defn move
|
(defn move
|
||||||
"Move the shape relatively to its current
|
"Move the shape relatively to its current
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
|
|
||||||
(def defaults
|
(def defaults
|
||||||
{:public-uri "http://localhost:3449"
|
{:public-uri "http://localhost:3449"
|
||||||
:tenant "dev"
|
:tenant "default"
|
||||||
:host "devenv"
|
:host "localhost"
|
||||||
:http-server-port 6061
|
:http-server-port 6061
|
||||||
:http-server-host "localhost"
|
:http-server-host "0.0.0.0"
|
||||||
:redis-uri "redis://redis/0"})
|
:redis-uri "redis://redis/0"})
|
||||||
|
|
||||||
(s/def ::http-server-port ::us/integer)
|
(s/def ::http-server-port ::us/integer)
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
width: 65px;
|
width: 65px;
|
||||||
|
|
||||||
.color-bullet {
|
.color-bullet {
|
||||||
border: 2px solid $color-gray-60;
|
border: 2px solid $color-gray-30;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: var(--bullet-size);
|
width: var(--bullet-size);
|
||||||
height: var(--bullet-size);
|
height: var(--bullet-size);
|
||||||
|
@ -30,21 +30,21 @@
|
||||||
|
|
||||||
.color-cell.current {
|
.color-cell.current {
|
||||||
.color-bullet {
|
.color-bullet {
|
||||||
border-color: $color-gray-50;
|
border-color: $color-gray-30;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.palette-menu .color-bullet {
|
ul.palette-menu .color-bullet {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border: 1px solid $color-gray-10;
|
border: 1px solid $color-gray-30;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
background-size: 8px;
|
background-size: 8px;
|
||||||
}
|
}
|
||||||
.color-cell.add-color .color-bullet {
|
.color-cell.add-color .color-bullet {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: $color-gray-50;
|
background-color: $color-gray-50;
|
||||||
border: 3px dashed $color-gray-10;
|
border: 3px dashed $color-gray-30;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -62,17 +62,24 @@ ul.palette-menu .color-bullet {
|
||||||
grid-area: color;
|
grid-area: color;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border: 1px solid $color-gray-10;
|
border: 1px solid $color-gray-30;
|
||||||
background-size: 8px;
|
background-size: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-section .asset-list-item .color-bullet {
|
.asset-section .asset-list-item .color-bullet {
|
||||||
border: 1px solid $color-gray-20;
|
border: 1px solid $color-gray-30;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin-right: $size-1;
|
margin-right: $size-1;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
.asset-list .asset-list-item {
|
||||||
|
&:hover {
|
||||||
|
.color-bullet {
|
||||||
|
border: 1px solid $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.color-cell.add-color:hover .color-bullet {
|
.color-cell.add-color:hover .color-bullet {
|
||||||
border-color: $color-gray-30;
|
border-color: $color-gray-30;
|
||||||
|
|
|
@ -522,6 +522,7 @@
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: $color-primary !important;
|
border-color: $color-primary !important;
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
padding: $size-1 $size-4 $size-1 $size-2;
|
padding: $size-1 $size-4 $size-1 $size-2;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
user-select: none;
|
||||||
&.team {
|
&.team {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20% 1fr 20%;
|
grid-template-columns: 20% 1fr 20%;
|
||||||
|
@ -85,6 +86,7 @@
|
||||||
font-size: $fs22;
|
font-size: $fs22;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
user-select: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu.is-open {
|
.context-menu.is-open {
|
||||||
|
|
|
@ -186,6 +186,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
// TODO: should be deprecated / unclear name
|
// TODO: should be deprecated / unclear name
|
||||||
&.dashboard-common {
|
&.dashboard-common {
|
||||||
|
@ -223,7 +224,6 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
|
|
@ -106,6 +106,9 @@
|
||||||
border: 1px dashed $color-gray-20;
|
border: 1px dashed $color-gray-20;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
.table-header {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
|
@ -148,9 +151,11 @@
|
||||||
|
|
||||||
&.roles {
|
&.roles {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
user-select: none;
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
.rol-label {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
.rol-selector {
|
.rol-selector {
|
||||||
&.has-priv {
|
&.has-priv {
|
||||||
border: 1px solid $color-gray-20;
|
border: 1px solid $color-gray-20;
|
||||||
|
|
|
@ -164,8 +164,10 @@
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
margin-right: $size-4;
|
margin-right: $size-4;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
user-select: none;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
&.dashboard-projects {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
&.no-bg {
|
&.no-bg {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,7 +148,7 @@
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid $color-gray-60;
|
border: 1px solid $color-gray-30;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-color .color-bullet {
|
.hide-color .color-bullet {
|
||||||
|
@ -173,6 +173,11 @@
|
||||||
background-position: 95% 48%;
|
background-position: 95% 48%;
|
||||||
background-size: 10px;
|
background-size: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
|
border: 1px solid $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
option {
|
option {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
|
@ -65,6 +65,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 448px;
|
width: 448px;
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
|
max-height: 700px;
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -104,6 +105,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.component-list {
|
||||||
|
max-height: 408px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-item-element {
|
.modal-item-element {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-bottom: 3px;
|
padding-bottom: 3px;
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
color: lighten($color-gray-10, 8%);
|
color: lighten($color-gray-10, 8%);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,6 +100,10 @@
|
||||||
color: lighten($color-gray-10, 8%);
|
color: lighten($color-gray-10, 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
option {
|
option {
|
||||||
background: $color-white;
|
background: $color-white;
|
||||||
color: $color-gray-60;
|
color: $color-gray-60;
|
||||||
|
|
|
@ -4,6 +4,16 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) KALEIDOS INC
|
// Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
.history-debug-overlay {
|
||||||
|
background: $color-gray-50;
|
||||||
|
bottom: 0;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: absolute;
|
||||||
|
width: 500px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
.history-toolbox {
|
.history-toolbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -199,6 +199,7 @@
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
color: lighten($color-gray-10, 8%);
|
color: lighten($color-gray-10, 8%);
|
||||||
|
border-color: $color-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
option {
|
option {
|
||||||
|
@ -312,6 +313,14 @@
|
||||||
border: 1px solid $color-gray-20;
|
border: 1px solid $color-gray-20;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.opened {
|
||||||
|
border: 1px solid $color-primary;
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.custom-select-dropdown {
|
.custom-select-dropdown {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
|
@ -549,6 +558,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.presets {
|
.presets {
|
||||||
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid $color-primary;
|
||||||
|
}
|
||||||
.custom-select-dropdown {
|
.custom-select-dropdown {
|
||||||
width: 237px;
|
width: 237px;
|
||||||
|
|
||||||
|
@ -968,6 +982,15 @@
|
||||||
input {
|
input {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
width: 74px;
|
width: 74px;
|
||||||
|
&:focus {
|
||||||
|
border-color: $color-primary !important;
|
||||||
|
color: $color-white;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $color-gray-20;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,9 @@
|
||||||
span {
|
span {
|
||||||
color: $color-gray-60;
|
color: $color-gray-60;
|
||||||
}
|
}
|
||||||
|
input {
|
||||||
|
color: $color-white !important;
|
||||||
|
}
|
||||||
|
|
||||||
.toggle-content {
|
.toggle-content {
|
||||||
svg {
|
svg {
|
||||||
|
@ -149,6 +152,9 @@
|
||||||
span.element-name {
|
span.element-name {
|
||||||
color: $color-gray-60;
|
color: $color-gray-60;
|
||||||
}
|
}
|
||||||
|
input.element-name {
|
||||||
|
color: $color-white !important;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: $color-gray-60;
|
fill: $color-gray-60;
|
||||||
|
@ -203,6 +209,14 @@
|
||||||
|
|
||||||
input.element-name {
|
input.element-name {
|
||||||
max-width: 75%;
|
max-width: 75%;
|
||||||
|
margin-right: 5px;
|
||||||
|
background-color: $color-gray-50;
|
||||||
|
color: $color-white;
|
||||||
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid $color-primary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span.element-name {
|
span.element-name {
|
||||||
|
@ -225,7 +239,6 @@ span.element-name {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
right: 20px;
|
|
||||||
|
|
||||||
&.is-parent {
|
&.is-parent {
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -306,12 +319,16 @@ span.element-name {
|
||||||
|
|
||||||
&.search {
|
&.search {
|
||||||
.search-box {
|
.search-box {
|
||||||
border: 1px solid $color-primary;
|
border: 1px solid $color-gray-20;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
&:active,
|
||||||
|
&:focus-within {
|
||||||
|
border: 1px solid $color-primary;
|
||||||
|
}
|
||||||
input {
|
input {
|
||||||
border: 0;
|
border: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -319,6 +336,9 @@ span.element-name {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
|
|
@ -390,6 +390,7 @@ button.collapse-sidebar {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 10px;
|
padding: 12px 10px;
|
||||||
|
|
||||||
.search-box {
|
.search-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -397,6 +398,9 @@ button.collapse-sidebar {
|
||||||
border: 1px solid $color-gray-30;
|
border: 1px solid $color-gray-30;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
&:focus-within {
|
||||||
|
border: 1px solid $color-primary;
|
||||||
|
}
|
||||||
.input-text {
|
.input-text {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: $color-gray-50;
|
background: $color-gray-50;
|
||||||
|
|
|
@ -145,6 +145,12 @@
|
||||||
background: $color-gray-50;
|
background: $color-gray-50;
|
||||||
color: $color-gray-10;
|
color: $color-gray-10;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid $color-primary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1969,3 +1969,6 @@
|
||||||
(dm/export dwv/update-viewport-size)
|
(dm/export dwv/update-viewport-size)
|
||||||
(dm/export dwv/start-panning)
|
(dm/export dwv/start-panning)
|
||||||
(dm/export dwv/finish-panning)
|
(dm/export dwv/finish-panning)
|
||||||
|
|
||||||
|
;; Undo
|
||||||
|
(dm/export dwu/reinitialize-undo)
|
||||||
|
|
|
@ -93,7 +93,7 @@
|
||||||
(->> (rx/from uris)
|
(->> (rx/from uris)
|
||||||
(rx/filter (comp not svg-url?))
|
(rx/filter (comp not svg-url?))
|
||||||
(rx/map prepare)
|
(rx/map prepare)
|
||||||
(rx/mapcat #(rp/mutation! :create-file-media-object-from-url %))
|
(rx/mapcat #(rp/command! :create-file-media-object-from-url %))
|
||||||
(rx/do on-image))
|
(rx/do on-image))
|
||||||
|
|
||||||
(->> (rx/from uris)
|
(->> (rx/from uris)
|
||||||
|
|
|
@ -114,6 +114,18 @@
|
||||||
|
|
||||||
(reduce set-child ignore-tree children))))
|
(reduce set-child ignore-tree children))))
|
||||||
|
|
||||||
|
(defn assoc-position-data
|
||||||
|
[shape position-data old-shape]
|
||||||
|
(let [deltav (gpt/to-vec (gpt/point (:selrect old-shape))
|
||||||
|
(gpt/point (:selrect shape)))
|
||||||
|
position-data
|
||||||
|
(-> position-data
|
||||||
|
(gsh/move-position-data deltav))]
|
||||||
|
(cond-> shape
|
||||||
|
(d/not-empty? position-data)
|
||||||
|
(assoc :position-data position-data))))
|
||||||
|
|
||||||
|
|
||||||
(defn update-grow-type
|
(defn update-grow-type
|
||||||
[shape old-shape]
|
[shape old-shape]
|
||||||
(let [auto-width? (= :auto-width (:grow-type shape))
|
(let [auto-width? (= :auto-width (:grow-type shape))
|
||||||
|
@ -319,7 +331,8 @@
|
||||||
(ptk/reify ::apply-modifiers
|
(ptk/reify ::apply-modifiers
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [objects (wsh/lookup-page-objects state)
|
(let [text-modifiers (get state :workspace-text-modifier)
|
||||||
|
objects (wsh/lookup-page-objects state)
|
||||||
object-modifiers (if modifiers
|
object-modifiers (if modifiers
|
||||||
(calculate-modifiers state modifiers)
|
(calculate-modifiers state modifiers)
|
||||||
(get state :workspace-modifiers))
|
(get state :workspace-modifiers))
|
||||||
|
@ -342,9 +355,13 @@
|
||||||
ids
|
ids
|
||||||
(fn [shape]
|
(fn [shape]
|
||||||
(let [modif (get-in object-modifiers [(:id shape) :modifiers])
|
(let [modif (get-in object-modifiers [(:id shape) :modifiers])
|
||||||
text-shape? (cph/text-shape? shape)]
|
text-shape? (cph/text-shape? shape)
|
||||||
|
position-data (when text-shape?
|
||||||
|
(dm/get-in text-modifiers [(:id shape) :position-data]))]
|
||||||
(-> shape
|
(-> shape
|
||||||
(gsh/transform-shape modif)
|
(gsh/transform-shape modif)
|
||||||
|
(cond-> (d/not-empty? position-data)
|
||||||
|
(assoc-position-data position-data shape))
|
||||||
(cond-> text-shape?
|
(cond-> text-shape?
|
||||||
(update-grow-type shape)))))
|
(update-grow-type shape)))))
|
||||||
{:reg-objects? true
|
{:reg-objects? true
|
||||||
|
@ -366,7 +383,11 @@
|
||||||
:grow-type
|
:grow-type
|
||||||
:layout-item-h-sizing
|
:layout-item-h-sizing
|
||||||
:layout-item-v-sizing
|
:layout-item-v-sizing
|
||||||
|
:position-data
|
||||||
]})
|
]})
|
||||||
|
;; We've applied the text-modifier so we can dissoc the temporary data
|
||||||
|
(fn [state]
|
||||||
|
(update state :workspace-text-modifier #(apply dissoc % ids)))
|
||||||
(clear-local-transform))
|
(clear-local-transform))
|
||||||
(if undo-transation?
|
(if undo-transation?
|
||||||
(rx/of (dwu/commit-undo-transaction undo-id))
|
(rx/of (dwu/commit-undo-transaction undo-id))
|
||||||
|
|
|
@ -466,9 +466,10 @@
|
||||||
{:name (extract-name uri)
|
{:name (extract-name uri)
|
||||||
:url uri}))))
|
:url uri}))))
|
||||||
(rx/mapcat (fn [uri-data]
|
(rx/mapcat (fn [uri-data]
|
||||||
(->> (rp/mutation! (if (contains? uri-data :content)
|
(->> (rp/command! (if (contains? uri-data :content)
|
||||||
:upload-file-media-object
|
:upload-file-media-object
|
||||||
:create-file-media-object-from-url) uri-data)
|
:create-file-media-object-from-url)
|
||||||
|
uri-data)
|
||||||
;; 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))
|
||||||
|
|
|
@ -344,7 +344,7 @@
|
||||||
(when (or (and (not-changed? (:width shape) new-width) (= (:grow-type shape) :auto-width))
|
(when (or (and (not-changed? (:width shape) new-width) (= (:grow-type shape) :auto-width))
|
||||||
(and (not-changed? (:height shape) new-height)
|
(and (not-changed? (:height shape) new-height)
|
||||||
(or (= (:grow-type shape) :auto-height) (= (:grow-type shape) :auto-width))))
|
(or (= (:grow-type shape) :auto-height) (= (:grow-type shape) :auto-width))))
|
||||||
(rx/of (dch/update-shapes [id] update-fn {:reg-objects? true :save-undo? false})
|
(rx/of (dch/update-shapes [id] update-fn {:reg-objects? true :save-undo? true})
|
||||||
(ptk/data-event :layout/update [id]))))))))
|
(ptk/data-event :layout/update [id]))))))))
|
||||||
|
|
||||||
|
|
||||||
|
@ -377,7 +377,7 @@
|
||||||
(gpt/point (:selrect shape)))
|
(gpt/point (:selrect shape)))
|
||||||
|
|
||||||
new-shape
|
new-shape
|
||||||
(update new-shape :position-data gsh/move-position-data (:x delta-move) (:y delta-move))]
|
(update new-shape :position-data gsh/move-position-data delta-move)]
|
||||||
|
|
||||||
new-shape))
|
new-shape))
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,8 @@
|
||||||
(dom/blur! input-node)))
|
(dom/blur! input-node)))
|
||||||
(when esc?
|
(when esc?
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(update-input value)))))
|
(update-input value)
|
||||||
|
(dom/blur! input-node)))))
|
||||||
|
|
||||||
handle-blur
|
handle-blur
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
|
|
@ -163,7 +163,8 @@
|
||||||
(when enter?
|
(when enter?
|
||||||
(dom/blur! input-node))
|
(dom/blur! input-node))
|
||||||
(when esc?
|
(when esc?
|
||||||
(update-input value-str)))))
|
(update-input value-str)
|
||||||
|
(dom/blur! input-node)))))
|
||||||
|
|
||||||
handle-mouse-wheel
|
handle-mouse-wheel
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
(when (> (count items) 0)
|
(when (> (count items) 0)
|
||||||
[:*
|
[:*
|
||||||
[:p (tr "ds.component-subtitle")]
|
[:p (tr "ds.component-subtitle")]
|
||||||
[:ul
|
[:ul.component-list
|
||||||
(for [item items]
|
(for [item items]
|
||||||
[:li.modal-item-element
|
[:li.modal-item-element
|
||||||
[:span.modal-component-icon i/component]
|
[:span.modal-component-icon i/component]
|
||||||
|
|
|
@ -407,7 +407,7 @@
|
||||||
[:& interface-walkthrough
|
[:& interface-walkthrough
|
||||||
{:close-walkthrough close-walkthrough}])])
|
{:close-walkthrough close-walkthrough}])])
|
||||||
|
|
||||||
[:div.dashboard-container.no-bg
|
[:div.dashboard-container.no-bg.dashboard-projects
|
||||||
(for [{:keys [id] :as project} projects]
|
(for [{:keys [id] :as project} projects]
|
||||||
(let [files (when recent-map
|
(let [files (when recent-map
|
||||||
(->> (vals recent-map)
|
(->> (vals recent-map)
|
||||||
|
|
|
@ -67,7 +67,7 @@
|
||||||
(when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])]))
|
(when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])]))
|
||||||
|
|
||||||
(when-not (and on-change-format (:gradient color))
|
(when-not (and on-change-format (:gradient color))
|
||||||
[:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)}
|
[:select.color-format-select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)}
|
||||||
[:option {:value "hex"}
|
[:option {:value "hex"}
|
||||||
(tr "inspect.attributes.color.hex")]
|
(tr "inspect.attributes.color.hex")]
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
[app.main.ui.workspace.libraries]
|
[app.main.ui.workspace.libraries]
|
||||||
[app.main.ui.workspace.nudge]
|
[app.main.ui.workspace.nudge]
|
||||||
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
|
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
|
||||||
|
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]
|
||||||
[app.main.ui.workspace.textpalette :refer [textpalette]]
|
[app.main.ui.workspace.textpalette :refer [textpalette]]
|
||||||
[app.main.ui.workspace.viewport :refer [viewport]]
|
[app.main.ui.workspace.viewport :refer [viewport]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
|
@ -72,6 +73,10 @@
|
||||||
(when (debug? :coordinates)
|
(when (debug? :coordinates)
|
||||||
[:& coordinates/coordinates {:colorpalette? colorpalette?}])
|
[:& coordinates/coordinates {:colorpalette? colorpalette?}])
|
||||||
|
|
||||||
|
(when (debug? :history-overlay)
|
||||||
|
[:div.history-debug-overlay
|
||||||
|
[:button {:on-click #(st/emit! dw/reinitialize-undo)} "CLEAR"]
|
||||||
|
[:& history-toolbox]])
|
||||||
[:& viewport {:file file
|
[:& viewport {:file file
|
||||||
:wlocal wlocal
|
:wlocal wlocal
|
||||||
:wglobal wglobal
|
:wglobal wglobal
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
[app.util.keyboard :as kbd]
|
||||||
[app.util.strings :refer [matches-search]]
|
[app.util.strings :refer [matches-search]]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
|
@ -93,11 +94,22 @@
|
||||||
(st/emit! (modal/show
|
(st/emit! (modal/show
|
||||||
{:type :delete-shared
|
{:type :delete-shared
|
||||||
:origin :unpublish
|
:origin :unpublish
|
||||||
:on-accept (fn[]
|
:on-accept (fn []
|
||||||
(st/emit! (dwl/set-file-shared (:id file) false))
|
(st/emit! (dwl/set-file-shared (:id file) false))
|
||||||
(modal/show! :libraries-dialog {}))
|
(modal/show! :libraries-dialog {}))
|
||||||
:on-cancel #(modal/show! :libraries-dialog {})
|
:on-cancel #(modal/show! :libraries-dialog {})
|
||||||
:count-libraries 1}))))]
|
:count-libraries 1}))))
|
||||||
|
handle-key-down
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [event]
|
||||||
|
(let [enter? (kbd/enter? event)
|
||||||
|
esc? (kbd/esc? event)
|
||||||
|
input-node (dom/event->target event)]
|
||||||
|
|
||||||
|
(when enter?
|
||||||
|
(dom/blur! input-node))
|
||||||
|
(when esc?
|
||||||
|
(dom/blur! input-node)))))]
|
||||||
[:*
|
[:*
|
||||||
[:div.section
|
[:div.section
|
||||||
[:div.section-title (tr "workspace.libraries.in-this-file")]
|
[:div.section-title (tr "workspace.libraries.in-this-file")]
|
||||||
|
@ -130,7 +142,8 @@
|
||||||
{:placeholder (tr "workspace.libraries.search-shared-libraries")
|
{:placeholder (tr "workspace.libraries.search-shared-libraries")
|
||||||
:type "text"
|
:type "text"
|
||||||
:value @search-term
|
:value @search-term
|
||||||
:on-change on-search-term-change}]
|
:on-change on-search-term-change
|
||||||
|
:on-key-down handle-key-down}]
|
||||||
(if (str/empty? @search-term)
|
(if (str/empty? @search-term)
|
||||||
[:div.search-icon
|
[:div.search-icon
|
||||||
i/search]
|
i/search]
|
||||||
|
|
|
@ -140,6 +140,10 @@
|
||||||
(let [text-shapes (obj/get props "text-shapes")
|
(let [text-shapes (obj/get props "text-shapes")
|
||||||
prev-text-shapes (hooks/use-previous text-shapes)
|
prev-text-shapes (hooks/use-previous text-shapes)
|
||||||
|
|
||||||
|
;; We store in the state the texts still pending to be calculated so we can
|
||||||
|
;; get its position
|
||||||
|
pending-update (mf/use-state {})
|
||||||
|
|
||||||
text-change?
|
text-change?
|
||||||
(fn [id]
|
(fn [id]
|
||||||
(let [new-shape (get text-shapes id)
|
(let [new-shape (get text-shapes id)
|
||||||
|
@ -153,12 +157,23 @@
|
||||||
|
|
||||||
changed-texts
|
changed-texts
|
||||||
(mf/use-memo
|
(mf/use-memo
|
||||||
(mf/deps text-shapes)
|
(mf/deps text-shapes @pending-update)
|
||||||
#(->> (keys text-shapes)
|
#(let [pending-shapes (into #{} (vals @pending-update))]
|
||||||
(filter text-change?)
|
(->> (keys text-shapes)
|
||||||
(map (d/getf text-shapes))))
|
(filter (fn [id]
|
||||||
|
(or (contains? pending-shapes id)
|
||||||
|
(text-change? id))))
|
||||||
|
(map (d/getf text-shapes)))))
|
||||||
|
|
||||||
handle-update-shape (mf/use-callback update-text-shape)]
|
handle-update-shape
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [shape node]
|
||||||
|
;; Unique to indentify the pending state
|
||||||
|
(let [uid (js/Symbol)]
|
||||||
|
(swap! pending-update assoc uid (:id shape))
|
||||||
|
(p/then
|
||||||
|
(update-text-shape shape node)
|
||||||
|
#(swap! pending-update dissoc uid)))))]
|
||||||
|
|
||||||
[:.text-changes-renderer
|
[:.text-changes-renderer
|
||||||
(for [{:keys [id] :as shape} changed-texts]
|
(for [{:keys [id] :as shape} changed-texts]
|
||||||
|
|
|
@ -2243,7 +2243,19 @@
|
||||||
(let [value (-> (dom/get-target event)
|
(let [value (-> (dom/get-target event)
|
||||||
(dom/get-value)
|
(dom/get-value)
|
||||||
(d/read-string))]
|
(d/read-string))]
|
||||||
(swap! filters assoc :box value))))]
|
(swap! filters assoc :box value))))
|
||||||
|
|
||||||
|
handle-key-down
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [event]
|
||||||
|
(let [enter? (kbd/enter? event)
|
||||||
|
esc? (kbd/esc? event)
|
||||||
|
input-node (dom/event->target event)]
|
||||||
|
|
||||||
|
(when enter?
|
||||||
|
(dom/blur! input-node))
|
||||||
|
(when esc?
|
||||||
|
(dom/blur! input-node)))))]
|
||||||
|
|
||||||
[:div.assets-bar
|
[:div.assets-bar
|
||||||
[:div.tool-window
|
[:div.tool-window
|
||||||
|
@ -2260,7 +2272,8 @@
|
||||||
{:placeholder (tr "workspace.assets.search")
|
{:placeholder (tr "workspace.assets.search")
|
||||||
:type "text"
|
:type "text"
|
||||||
:value (:term @filters)
|
:value (:term @filters)
|
||||||
:on-change on-search-term-change}]
|
:on-change on-search-term-change
|
||||||
|
:on-key-down handle-key-down}]
|
||||||
(if (str/empty? (:term @filters))
|
(if (str/empty? (:term @filters))
|
||||||
[:div.search-icon
|
[:div.search-icon
|
||||||
i/search]
|
i/search]
|
||||||
|
|
|
@ -468,7 +468,19 @@
|
||||||
handle-show-more
|
handle-show-more
|
||||||
(fn []
|
(fn []
|
||||||
(when (<= (:num-items @filter-state) (count filtered-objects-total))
|
(when (<= (:num-items @filter-state) (count filtered-objects-total))
|
||||||
(swap! filter-state update :num-items + 100)))]
|
(swap! filter-state update :num-items + 100)))
|
||||||
|
|
||||||
|
handle-key-down
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [event]
|
||||||
|
(let [enter? (kbd/enter? event)
|
||||||
|
esc? (kbd/esc? event)
|
||||||
|
input-node (dom/event->target event)]
|
||||||
|
|
||||||
|
(when enter?
|
||||||
|
(dom/blur! input-node))
|
||||||
|
(when esc?
|
||||||
|
(dom/blur! input-node)))))]
|
||||||
|
|
||||||
[filtered-objects
|
[filtered-objects
|
||||||
handle-show-more
|
handle-show-more
|
||||||
|
@ -483,7 +495,8 @@
|
||||||
[:input {:on-change update-search-text
|
[:input {:on-change update-search-text
|
||||||
:value (:search-text @filter-state)
|
:value (:search-text @filter-state)
|
||||||
:auto-focus (:show-search-box @filter-state)
|
:auto-focus (:show-search-box @filter-state)
|
||||||
:placeholder (tr "workspace.sidebar.layers.search")}]]
|
:placeholder (tr "workspace.sidebar.layers.search")
|
||||||
|
:on-key-down handle-key-down}]]
|
||||||
(when (not (= "" (:search-text @filter-state)))
|
(when (not (= "" (:search-text @filter-state)))
|
||||||
[:span.clear {:on-click clear-search-text} i/exclude])]
|
[:span.clear {:on-click clear-search-text} i/exclude])]
|
||||||
[:span {:on-click toggle-search} i/cross]]
|
[:span {:on-click toggle-search} i/cross]]
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :refer [tr c]]
|
[app.util.i18n :refer [tr c]]
|
||||||
|
[app.util.keyboard :as kbd]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(def exports-attrs
|
(def exports-attrs
|
||||||
|
@ -123,7 +124,13 @@
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit! (dch/update-shapes ids
|
(st/emit! (dch/update-shapes ids
|
||||||
(fn [shape]
|
(fn [shape]
|
||||||
(assoc shape :exports []))))))]
|
(assoc shape :exports []))))))
|
||||||
|
manage-key-down
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [event]
|
||||||
|
(let [esc? (kbd/esc? event)]
|
||||||
|
(when esc?
|
||||||
|
(dom/blur! (dom/get-target event))))))]
|
||||||
|
|
||||||
[:div.element-set.exports-options
|
[:div.element-set.exports-options
|
||||||
[:div.element-set-title
|
[:div.element-set-title
|
||||||
|
@ -156,7 +163,8 @@
|
||||||
[:option {:value "6"} "6x"]])
|
[:option {:value "6"} "6x"]])
|
||||||
[:input.input-text {:value (:suffix export)
|
[:input.input-text {:value (:suffix export)
|
||||||
:placeholder (tr "workspace.options.export.suffix")
|
:placeholder (tr "workspace.options.export.suffix")
|
||||||
:on-change (partial on-suffix-change index)}]
|
:on-change (partial on-suffix-change index)
|
||||||
|
:on-key-down manage-key-down}]
|
||||||
[:select.input-select {:value (name (:type export))
|
[:select.input-select {:value (name (:type export))
|
||||||
:on-change (partial on-type-change index)}
|
:on-change (partial on-type-change index)}
|
||||||
[:option {:value "png"} "PNG"]
|
[:option {:value "png"} "PNG"]
|
||||||
|
|
|
@ -282,7 +282,8 @@
|
||||||
(when (and (options :presets)
|
(when (and (options :presets)
|
||||||
(or (nil? all-types) (= (count all-types) 1))) ;; Don't show presets if multi selected
|
(or (nil? all-types) (= (count all-types) 1))) ;; Don't show presets if multi selected
|
||||||
[:div.row-flex ;; some frames and some non frames
|
[:div.row-flex ;; some frames and some non frames
|
||||||
[:div.presets.custom-select.flex-grow {:on-click #(reset! show-presets-dropdown? true)}
|
[:div.presets.custom-select.flex-grow {:class (when @show-presets-dropdown? "opened")
|
||||||
|
:on-click #(reset! show-presets-dropdown? true)}
|
||||||
[:span (tr "workspace.options.size-presets")]
|
[:span (tr "workspace.options.size-presets")]
|
||||||
[:span.dropdown-button i/arrow-down]
|
[:span.dropdown-button i/arrow-down]
|
||||||
[:& dropdown {:show @show-presets-dropdown?
|
[:& dropdown {:show @show-presets-dropdown?
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
[app.main.data.workspace.changes :as dch]
|
[app.main.data.workspace.changes :as dch]
|
||||||
[app.main.data.workspace.libraries :as dwl]
|
[app.main.data.workspace.libraries :as dwl]
|
||||||
[app.main.data.workspace.texts :as dwt]
|
[app.main.data.workspace.texts :as dwt]
|
||||||
|
[app.main.data.workspace.undo :as dwu]
|
||||||
[app.main.fonts :as fonts]
|
[app.main.fonts :as fonts]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
@ -20,7 +21,7 @@
|
||||||
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry typography-options]]
|
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry typography-options]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.timers :as tm]
|
[app.util.timers :as ts]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
@ -164,7 +165,12 @@
|
||||||
(let [grow-type (:grow-type values)
|
(let [grow-type (:grow-type values)
|
||||||
handle-change-grow
|
handle-change-grow
|
||||||
(fn [_ grow-type]
|
(fn [_ grow-type]
|
||||||
(st/emit! (dch/update-shapes ids #(assoc % :grow-type grow-type)))
|
(let [uid (js/Symbol)]
|
||||||
|
(st/emit!
|
||||||
|
(dwu/start-undo-transaction uid)
|
||||||
|
(dch/update-shapes ids #(assoc % :grow-type grow-type)))
|
||||||
|
;; We asynchronously commit so every sychronous event is resolved first and inside the transaction
|
||||||
|
(ts/schedule #(st/emit! (dwu/commit-undo-transaction uid))))
|
||||||
(when (some? on-blur) (on-blur)))]
|
(when (some? on-blur) (on-blur)))]
|
||||||
|
|
||||||
[:div.align-icons
|
[:div.align-icons
|
||||||
|
@ -304,7 +310,7 @@
|
||||||
:show-recent true
|
:show-recent true
|
||||||
:on-blur
|
:on-blur
|
||||||
(fn []
|
(fn []
|
||||||
(tm/schedule
|
(ts/schedule
|
||||||
100
|
100
|
||||||
(fn []
|
(fn []
|
||||||
(when (not= "INPUT" (-> (dom/get-active) (dom/get-tag-name)))
|
(when (not= "INPUT" (-> (dom/get-active) (dom/get-tag-name)))
|
||||||
|
|
|
@ -225,7 +225,9 @@
|
||||||
;; inside a foreign object "dummy" so this awkward behaviour is take into account
|
;; inside a foreign object "dummy" so this awkward behaviour is take into account
|
||||||
[:svg {:style {:top 0 :left 0 :position "fixed" :width "100%" :height "100%" :opacity (when-not (debug? :html-text) 0)}}
|
[:svg {:style {:top 0 :left 0 :position "fixed" :width "100%" :height "100%" :opacity (when-not (debug? :html-text) 0)}}
|
||||||
[:foreignObject {:x 0 :y 0 :width "100%" :height "100%"}
|
[:foreignObject {:x 0 :y 0 :width "100%" :height "100%"}
|
||||||
[:div {:style {:pointer-events (when-not (debug? :html-text) "none")}}
|
[:div {:style {:pointer-events (when-not (debug? :html-text) "none")
|
||||||
|
;; some opacity because to debug auto-width events will fill the screen
|
||||||
|
:opacity 0.6}}
|
||||||
[:& stvh/viewport-texts
|
[:& stvh/viewport-texts
|
||||||
{:key (dm/str "texts-" page-id)
|
{:key (dm/str "texts-" page-id)
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue