mirror of
https://github.com/penpot/penpot.git
synced 2026-05-02 14:48:29 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
06033ea955
@ -3,15 +3,26 @@
|
||||
;; Optional: queue, ommited means Integer/MAX_VALUE
|
||||
;; Optional: timeout, ommited means no timeout
|
||||
;; Note: queue and timeout are excluding
|
||||
{:update-file/by-profile
|
||||
{:update-file/global {:permits 20}
|
||||
:update-file/by-profile
|
||||
{:permits 1 :queue 5}
|
||||
|
||||
:update-file/global {:permits 20}
|
||||
:process-font/global {:permits 4}
|
||||
:process-font/by-profile {:permits 1}
|
||||
|
||||
:derive-password/global {:permits 8}
|
||||
:process-font/global {:permits 4}
|
||||
:process-image/global {:permits 8}
|
||||
:process-image/by-profile {:permits 1}
|
||||
|
||||
:auth/global {:permits 8}
|
||||
|
||||
:root/global
|
||||
{:permits 40}
|
||||
|
||||
:root/by-profile
|
||||
{:permits 10}
|
||||
|
||||
:file-thumbnail-ops/global
|
||||
{:permits 20}
|
||||
:file-thumbnail-ops/by-profile
|
||||
{:permits 2}
|
||||
|
||||
|
||||
@ -129,6 +129,9 @@
|
||||
:id (str team-id)
|
||||
:fonts (count fonts))
|
||||
|
||||
(when-let [photo-id (:photo-id team)]
|
||||
(vswap! bfc/*state* update :storage-objects conj photo-id))
|
||||
|
||||
(vswap! bfc/*state* update :teams conj team-id)
|
||||
(vswap! bfc/*state* bfc/collect-storage-objects fonts)
|
||||
|
||||
|
||||
@ -237,8 +237,7 @@
|
||||
(jdbc/get-connection system-or-pool)
|
||||
(if (map? system-or-pool)
|
||||
(open (::pool system-or-pool))
|
||||
(ex/raise :type :internal
|
||||
:code :unable-resolve-pool))))
|
||||
(throw (IllegalArgumentException. "unable to resolve connection pool")))))
|
||||
|
||||
(defn get-update-count
|
||||
[result]
|
||||
@ -250,9 +249,7 @@
|
||||
cfg-or-conn
|
||||
(if (map? cfg-or-conn)
|
||||
(get-connection (::conn cfg-or-conn))
|
||||
(ex/raise :type :internal
|
||||
:code :unable-resolve-connection
|
||||
:hint "expected conn or system map"))))
|
||||
(throw (IllegalArgumentException. "unable to resolve connection")))))
|
||||
|
||||
(defn connection-map?
|
||||
"Check if the provided value is a map like data structure that
|
||||
@ -260,15 +257,15 @@
|
||||
[o]
|
||||
(and (map? o) (connection? (::conn o))))
|
||||
|
||||
(defn- get-connectable
|
||||
(defn get-connectable
|
||||
"Resolve to a connection or connection pool instance; if it is not
|
||||
possible, raises an exception"
|
||||
[o]
|
||||
(cond
|
||||
(connection? o) o
|
||||
(pool? o) o
|
||||
(map? o) (get-connectable (or (::conn o) (::pool o)))
|
||||
:else (ex/raise :type :internal
|
||||
:code :unable-resolve-connectable
|
||||
:hint "expected conn, pool or system")))
|
||||
:else (throw (IllegalArgumentException. "unable to resolve connectable"))))
|
||||
|
||||
(def ^:private params-mapping
|
||||
{::return-keys? :return-keys
|
||||
|
||||
@ -200,22 +200,15 @@
|
||||
;; NOTE: this operation may cause primary key conflicts on inserts
|
||||
;; because of the timestamp precission (two concurrent requests), in
|
||||
;; this case we just retry the operation.
|
||||
(let [cfg (-> cfg
|
||||
(assoc ::rtry/when rtry/conflict-exception?)
|
||||
(assoc ::rtry/max-retries 6)
|
||||
(assoc ::rtry/label "persist-audit-log"))
|
||||
(let [tnow (dt/now)
|
||||
params (-> params
|
||||
(assoc :created-at tnow)
|
||||
(assoc :tracked-at tnow)
|
||||
(update :props db/tjson)
|
||||
(update :context db/tjson)
|
||||
(update :ip-addr db/inet)
|
||||
(assoc :source "backend"))]
|
||||
|
||||
(rtry/invoke cfg (fn [cfg]
|
||||
(let [tnow (dt/now)
|
||||
params (-> params
|
||||
(assoc :created-at tnow)
|
||||
(assoc :tracked-at tnow))]
|
||||
(db/insert! cfg :audit-log params))))))
|
||||
(db/insert! cfg :audit-log params)))
|
||||
|
||||
(when (and (contains? cf/flags :webhooks)
|
||||
(::webhooks/event? event))
|
||||
@ -246,9 +239,13 @@
|
||||
"Submit audit event to the collector."
|
||||
[cfg params]
|
||||
(try
|
||||
(let [event (d/without-nils params)]
|
||||
(let [event (d/without-nils params)
|
||||
cfg (-> cfg
|
||||
(assoc ::rtry/when rtry/conflict-exception?)
|
||||
(assoc ::rtry/max-retries 6)
|
||||
(assoc ::rtry/label "persist-audit-log"))]
|
||||
(us/verify! ::event event)
|
||||
(db/tx-run! cfg handle-event! event))
|
||||
(rtry/invoke! cfg db/tx-run! handle-event! event))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error processing event" :cause cause))))
|
||||
|
||||
|
||||
@ -322,9 +322,7 @@
|
||||
::rpc/climit (ig/ref ::rpc/climit)
|
||||
::rpc/rlimit (ig/ref ::rpc/rlimit)
|
||||
::setup/templates (ig/ref ::setup/templates)
|
||||
::props (ig/ref ::setup/props)
|
||||
|
||||
:pool (ig/ref ::db/pool)}
|
||||
::props (ig/ref ::setup/props)}
|
||||
|
||||
:app.rpc.doc/routes
|
||||
{:methods (ig/ref :app.rpc/methods)}
|
||||
|
||||
@ -139,15 +139,10 @@
|
||||
(f cfg (us/conform spec params)))
|
||||
f)))
|
||||
|
||||
;; TODO: integrate with sm/define
|
||||
|
||||
(defn- wrap-params-validation
|
||||
[_ f mdata]
|
||||
(if-let [schema (::sm/params mdata)]
|
||||
(let [schema (if (sm/lazy-schema? schema)
|
||||
schema
|
||||
(sm/define schema))
|
||||
validate (sm/validator schema)
|
||||
(let [validate (sm/validator schema)
|
||||
explain (sm/explainer schema)
|
||||
decode (sm/decoder schema)]
|
||||
(fn [cfg params]
|
||||
@ -245,8 +240,7 @@
|
||||
::mtx/metrics
|
||||
::main/props]
|
||||
:opt [::climit
|
||||
::rlimit]
|
||||
:req-un [::db/pool]))
|
||||
::rlimit]))
|
||||
|
||||
(defmethod ig/init-key ::methods
|
||||
[_ cfg]
|
||||
|
||||
@ -21,26 +21,31 @@
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.bulkhead :as pbh])
|
||||
(:import
|
||||
clojure.lang.ExceptionInfo))
|
||||
clojure.lang.ExceptionInfo
|
||||
java.util.concurrent.atomic.AtomicLong))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(defn- id->str
|
||||
[id]
|
||||
(-> (str id)
|
||||
(subs 1)))
|
||||
([id]
|
||||
(-> (str id)
|
||||
(subs 1)))
|
||||
([id key]
|
||||
(if key
|
||||
(str (-> (str id) (subs 1)) "/" key)
|
||||
(id->str id))))
|
||||
|
||||
(defn- create-cache
|
||||
[{:keys [::wrk/executor]}]
|
||||
(letfn [(on-remove [key _ cause]
|
||||
(let [[id skey] key]
|
||||
(l/dbg :hint "destroy limiter" :id (id->str id) :key skey :reason (str cause))))]
|
||||
(l/dbg :hint "disposed" :id (id->str id skey) :reason (str cause))))]
|
||||
(cache/create :executor executor
|
||||
:on-remove on-remove
|
||||
:keepalive "5m")))
|
||||
@ -81,132 +86,179 @@
|
||||
|
||||
(defn- create-limiter
|
||||
[config [id skey]]
|
||||
(l/dbg :hint "create limiter" :id (id->str id) :key skey)
|
||||
(l/dbg :hint "created" :id (id->str id skey))
|
||||
(pbh/create :permits (or (:permits config) (:concurrency config))
|
||||
:queue (or (:queue config) (:queue-size config))
|
||||
:timeout (:timeout config)
|
||||
:type :semaphore))
|
||||
|
||||
(defn- invoke!
|
||||
[config cache metrics id key f]
|
||||
(if-let [limiter (cache/get cache [id key] (partial create-limiter config))]
|
||||
(let [tpoint (dt/tpoint)
|
||||
labels (into-array String [(id->str id)])
|
||||
wrapped (fn []
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
(l/trc :hint "acquired"
|
||||
:id (id->str id)
|
||||
:key key
|
||||
:permits (:permits stats)
|
||||
:queue (:queue stats)
|
||||
:max-permits (:max-permits stats)
|
||||
:max-queue (:max-queue stats)
|
||||
:elapsed (dt/format-duration elapsed))
|
||||
(defmacro ^:private measure-and-log!
|
||||
[metrics mlabels stats id action limit-id limit-label profile-id elapsed]
|
||||
`(let [mpermits# (:max-permits ~stats)
|
||||
mqueue# (:max-queue ~stats)
|
||||
permits# (:permits ~stats)
|
||||
queue# (:queue ~stats)
|
||||
queue# (- queue# mpermits#)
|
||||
queue# (if (neg? queue#) 0 queue#)
|
||||
level# (if (pos? queue#) :warn :trace)]
|
||||
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-timing
|
||||
:val (inst-ms elapsed)
|
||||
:labels labels)
|
||||
(try
|
||||
(f)
|
||||
(finally
|
||||
(let [elapsed (tpoint)]
|
||||
(l/trc :hint "finished"
|
||||
:id (id->str id)
|
||||
:key key
|
||||
:permits (:permits stats)
|
||||
:queue (:queue stats)
|
||||
:max-permits (:max-permits stats)
|
||||
:max-queue (:max-queue stats)
|
||||
:elapsed (dt/format-duration elapsed)))))))
|
||||
measure!
|
||||
(fn [stats]
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-queue
|
||||
:val (:queue stats)
|
||||
:labels labels)
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-permits
|
||||
:val (:permits stats)
|
||||
:labels labels))]
|
||||
(mtx/run! ~metrics
|
||||
:id :rpc-climit-queue
|
||||
:val queue#
|
||||
:labels ~mlabels)
|
||||
|
||||
(try
|
||||
(let [stats (pbh/get-stats limiter)]
|
||||
(measure! stats)
|
||||
(l/trc :hint "enqueued"
|
||||
:id (id->str id)
|
||||
:key key
|
||||
:permits (:permits stats)
|
||||
:queue (:queue stats)
|
||||
:max-permits (:max-permits stats)
|
||||
:max-queue (:max-queue stats))
|
||||
(px/invoke! limiter wrapped))
|
||||
(catch ExceptionInfo cause
|
||||
(let [{:keys [type code]} (ex-data cause)]
|
||||
(if (= :bulkhead-error type)
|
||||
(mtx/run! ~metrics
|
||||
:id :rpc-climit-permits
|
||||
:val permits#
|
||||
:labels ~mlabels)
|
||||
|
||||
(l/log level#
|
||||
:hint ~action
|
||||
:req ~id
|
||||
:id ~limit-id
|
||||
:label ~limit-label
|
||||
:profile-id (str ~profile-id)
|
||||
:permits permits#
|
||||
:queue queue#
|
||||
:max-permits mpermits#
|
||||
:max-queue mqueue#
|
||||
~@(if (some? elapsed)
|
||||
[:elapsed `(dt/format-duration ~elapsed)]
|
||||
[]))))
|
||||
|
||||
(def ^:private idseq (AtomicLong. 0))
|
||||
|
||||
(defn- invoke
|
||||
[limiter metrics limit-id limit-key limit-label profile-id f params]
|
||||
(let [tpoint (dt/tpoint)
|
||||
limit-id (id->str limit-id limit-key)
|
||||
mlabels (into-array String [limit-id])
|
||||
stats (pbh/get-stats limiter)
|
||||
id (.incrementAndGet ^AtomicLong idseq)]
|
||||
|
||||
(try
|
||||
(measure-and-log! metrics mlabels stats id "enqueued" limit-id limit-label profile-id nil)
|
||||
(px/invoke! limiter (fn []
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
(measure-and-log! metrics mlabels stats id "acquired" limit-id limit-label profile-id elapsed)
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-timing
|
||||
:val (inst-ms elapsed)
|
||||
:labels mlabels)
|
||||
(apply f params))))
|
||||
|
||||
(catch ExceptionInfo cause
|
||||
(let [{:keys [type code]} (ex-data cause)]
|
||||
(if (= :bulkhead-error type)
|
||||
(let [elapsed (tpoint)]
|
||||
(measure-and-log! metrics mlabels stats id "reject" limit-id limit-label profile-id elapsed)
|
||||
(ex/raise :type :concurrency-limit
|
||||
:code code
|
||||
:hint "concurrency limit reached")
|
||||
(throw cause))))
|
||||
:hint "concurrency limit reached"
|
||||
:cause cause))
|
||||
(throw cause))))
|
||||
|
||||
(finally
|
||||
(measure! (pbh/get-stats limiter)))))
|
||||
|
||||
(do
|
||||
(l/wrn :hint "no limiter found" :id (id->str id))
|
||||
(f))))
|
||||
(finally
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
(measure-and-log! metrics mlabels stats id "finished" limit-id limit-label profile-id elapsed))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MIDDLEWARE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def noop-fn (constantly nil))
|
||||
(def ^:private noop-fn (constantly nil))
|
||||
(def ^:private global-limits
|
||||
[[:root/global noop-fn]
|
||||
[:root/by-profile ::rpc/profile-id]])
|
||||
|
||||
(defn- get-limits
|
||||
[cfg]
|
||||
(when-let [ref (get cfg ::id)]
|
||||
(cond
|
||||
(keyword? ref)
|
||||
[[ref]]
|
||||
|
||||
(and (vector? ref)
|
||||
(keyword (first ref)))
|
||||
[ref]
|
||||
|
||||
(and (vector? ref)
|
||||
(vector? (first ref)))
|
||||
(rseq ref)
|
||||
|
||||
:else
|
||||
(throw (IllegalArgumentException. "unable to normalize limit")))))
|
||||
|
||||
(defn wrap
|
||||
[{:keys [::rpc/climit ::mtx/metrics]} f {:keys [::id ::key-fn] :or {key-fn noop-fn} :as mdata}]
|
||||
(if (and (some? climit) (some? id))
|
||||
(let [cache (::cache climit)
|
||||
config (::config climit)]
|
||||
(if-let [config (get config id)]
|
||||
(do
|
||||
(l/dbg :hint "instrumenting method"
|
||||
:limit (id->str id)
|
||||
:service-name (::sv/name mdata)
|
||||
:timeout (:timeout config)
|
||||
:permits (:permits config)
|
||||
:queue (:queue config)
|
||||
:keyed? (not= key-fn noop-fn))
|
||||
[{:keys [::rpc/climit ::mtx/metrics]} handler mdata]
|
||||
(let [cache (::cache climit)
|
||||
config (::config climit)
|
||||
label (::sv/name mdata)]
|
||||
|
||||
(fn [cfg params]
|
||||
(invoke! config cache metrics id (key-fn params) (partial f cfg params))))
|
||||
(reduce (fn [handler [limit-id key-fn]]
|
||||
(if-let [config (get config limit-id)]
|
||||
(let [key-fn (or key-fn noop-fn)]
|
||||
(l/dbg :hint "instrumenting method"
|
||||
:method label
|
||||
:limit (id->str limit-id)
|
||||
:timeout (:timeout config)
|
||||
:permits (:permits config)
|
||||
:queue (:queue config)
|
||||
:keyed (not= key-fn noop-fn))
|
||||
|
||||
(do
|
||||
(l/wrn :hint "no config found for specified queue" :id (id->str id))
|
||||
f)))
|
||||
|
||||
f))
|
||||
(if (and (= key-fn ::rpc/profile-id)
|
||||
(false? (::rpc/auth mdata true)))
|
||||
|
||||
;; We don't enforce by-profile limit on methods that does
|
||||
;; not require authentication
|
||||
handler
|
||||
|
||||
(fn [cfg params]
|
||||
(let [limit-key (key-fn params)
|
||||
cache-key [limit-id limit-key]
|
||||
limiter (cache/get cache cache-key (partial create-limiter config))
|
||||
profile-id (if (= key-fn ::rpc/profile-id)
|
||||
limit-key
|
||||
(get params ::rpc/profile-id))]
|
||||
(invoke limiter metrics limit-id limit-key label profile-id handler [cfg params])))))
|
||||
|
||||
(do
|
||||
(l/wrn :hint "no config found for specified queue" :id (id->str limit-id))
|
||||
handler)))
|
||||
|
||||
handler
|
||||
(concat global-limits (get-limits mdata)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PUBLIC API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn configure
|
||||
[{:keys [::rpc/climit]} id]
|
||||
(us/assert! ::rpc/climit climit)
|
||||
(assoc climit ::id id))
|
||||
(defn- build-exec-chain
|
||||
[{:keys [::label ::profile-id ::rpc/climit ::mtx/metrics] :as cfg} f]
|
||||
(let [config (get climit ::config)
|
||||
cache (get climit ::cache)]
|
||||
|
||||
(defn run!
|
||||
(reduce (fn [handler [limit-id limit-key :as ckey]]
|
||||
(let [config (get config limit-id)]
|
||||
(when-not config
|
||||
(throw (IllegalArgumentException.
|
||||
(str/ffmt "config not found for: %" limit-id))))
|
||||
|
||||
(fn [& params]
|
||||
(let [limiter (cache/get cache ckey (partial create-limiter config))]
|
||||
(invoke limiter metrics limit-id limit-key label profile-id handler params)))))
|
||||
f
|
||||
(get-limits cfg))))
|
||||
|
||||
(defn invoke!
|
||||
"Run a function in context of climit.
|
||||
Intended to be used in virtual threads."
|
||||
([{:keys [::id ::cache ::config ::mtx/metrics]} f]
|
||||
(if-let [config (get config id)]
|
||||
(invoke! config cache metrics id nil f)
|
||||
(f)))
|
||||
|
||||
([{:keys [::id ::cache ::config ::mtx/metrics]} f executor]
|
||||
(let [f #(p/await! (px/submit! executor f))]
|
||||
(if-let [config (get config id)]
|
||||
(invoke! config cache metrics id nil f)
|
||||
(f)))))
|
||||
|
||||
[{:keys [::executor] :as cfg} f & params]
|
||||
(let [f (if (some? executor)
|
||||
(fn [& params] (px/await! (px/submit! executor (fn [] (apply f params)))))
|
||||
f)
|
||||
f (build-exec-chain cfg f)]
|
||||
(apply f params)))
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
@ -39,7 +40,7 @@
|
||||
;; ---- COMMAND: login with password
|
||||
|
||||
(defn login-with-password
|
||||
[{:keys [::db/pool] :as cfg} {:keys [email password] :as params}]
|
||||
[cfg {:keys [email password] :as params}]
|
||||
|
||||
(when-not (or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password))
|
||||
@ -47,7 +48,7 @@
|
||||
:code :login-disabled
|
||||
:hint "login is disabled in this instance"))
|
||||
|
||||
(letfn [(check-password [conn profile password]
|
||||
(letfn [(check-password [cfg profile password]
|
||||
(if (= (:password profile) "!")
|
||||
(ex/raise :type :validation
|
||||
:code :account-without-password
|
||||
@ -57,10 +58,10 @@
|
||||
(l/trc :hint "updating profile password"
|
||||
:id (str (:id profile))
|
||||
:email (:email profile))
|
||||
(profile/update-profile-password! conn (assoc profile :password password)))
|
||||
(profile/update-profile-password! cfg (assoc profile :password password)))
|
||||
(:valid result))))
|
||||
|
||||
(validate-profile [conn profile]
|
||||
(validate-profile [cfg profile]
|
||||
(when-not profile
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
@ -70,7 +71,7 @@
|
||||
(when (:is-blocked profile)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
(when-not (check-password conn profile password)
|
||||
(when-not (check-password cfg profile password)
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
(when-let [deleted-at (:deleted-at profile)]
|
||||
@ -78,27 +79,29 @@
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials)))
|
||||
|
||||
profile)]
|
||||
profile)
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (->> (profile/get-profile-by-email conn email)
|
||||
(validate-profile conn)
|
||||
(profile/strip-private-attrs))
|
||||
(login [{:keys [::db/conn] :as cfg}]
|
||||
(let [profile (->> (profile/get-profile-by-email conn email)
|
||||
(validate-profile cfg)
|
||||
(profile/strip-private-attrs))
|
||||
|
||||
invitation (when-let [token (:invitation-token params)]
|
||||
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))
|
||||
invitation (when-let [token (:invitation-token params)]
|
||||
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))
|
||||
|
||||
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
|
||||
;; invitation because invitations matches exactly; and user can't login with other email and
|
||||
;; accept invitation with other email
|
||||
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
|
||||
{:invitation-token (:invitation-token params)}
|
||||
(assoc profile :is-admin (let [admins (cf/get :admins)]
|
||||
(contains? admins (:email profile)))))]
|
||||
(-> response
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-meta {::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))))))
|
||||
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
|
||||
;; invitation because invitations matches exactly; and user can't login with other email and
|
||||
;; accept invitation with other email
|
||||
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
|
||||
{:invitation-token (:invitation-token params)}
|
||||
(assoc profile :is-admin (let [admins (cf/get :admins)]
|
||||
(contains? admins (:email profile)))))]
|
||||
(-> response
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-meta {::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))))]
|
||||
|
||||
(db/tx-run! cfg login)))
|
||||
|
||||
(def schema:login-with-password
|
||||
[:map {:title "login-with-password"}
|
||||
@ -110,6 +113,7 @@
|
||||
"Performs authentication using penpot password."
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::climit/id :auth/global
|
||||
::sm/params schema:login-with-password}
|
||||
[cfg params]
|
||||
(login-with-password cfg params))
|
||||
@ -149,7 +153,8 @@
|
||||
(sv/defmethod ::recover-profile
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::sm/params schema:recover-profile}
|
||||
::sm/params schema:recover-profile
|
||||
::climit/id :auth/global}
|
||||
[cfg params]
|
||||
(recover-profile cfg params))
|
||||
|
||||
@ -360,7 +365,6 @@
|
||||
{::audit/type "fact"
|
||||
::audit/name "register-profile-retry"
|
||||
::audit/profile-id id}))
|
||||
|
||||
(cond
|
||||
;; If invitation token comes in params, this is because the
|
||||
;; user comes from team-invitation process; in this case,
|
||||
@ -402,7 +406,6 @@
|
||||
{::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})))))
|
||||
|
||||
|
||||
(def schema:register-profile
|
||||
[:map {:title "register-profile"}
|
||||
[:token schema:token]
|
||||
@ -411,7 +414,8 @@
|
||||
(sv/defmethod ::register-profile
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::sm/params schema:register-profile}
|
||||
::sm/params schema:register-profile
|
||||
::climit/id :auth/global}
|
||||
[{:keys [::db/pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(-> (assoc cfg ::db/conn conn)
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
@ -24,18 +24,21 @@
|
||||
[app.rpc.retry :as rtry]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[app.util.time :as dt]))
|
||||
|
||||
;; --- GENERAL PURPOSE INTERNAL HELPERS
|
||||
|
||||
(defn decode-row
|
||||
(defn- decode-row
|
||||
[{:keys [participants position] :as row}]
|
||||
(cond-> row
|
||||
(db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
|
||||
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
|
||||
|
||||
(def sql:get-file
|
||||
(def xf-decode-row
|
||||
(map decode-row))
|
||||
|
||||
(def ^:privateqpage-name
|
||||
sql:get-file
|
||||
"select f.id, f.modified_at, f.revn, f.features,
|
||||
f.project_id, p.team_id, f.data
|
||||
from file as f
|
||||
@ -45,17 +48,19 @@
|
||||
|
||||
(defn- get-file
|
||||
"A specialized version of get-file for comments module."
|
||||
[{:keys [::db/conn] :as cfg} file-id page-id]
|
||||
(if-let [{:keys [data] :as file} (some-> (db/exec-one! conn [sql:get-file file-id])
|
||||
(files/decode-row))]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(-> file
|
||||
(assoc :page-name (dm/get-in data [:pages-index page-id :name]))
|
||||
(assoc :page-id page-id)))
|
||||
[cfg file-id page-id]
|
||||
(let [file (db/exec-one! cfg [sql:get-file file-id])]
|
||||
(when-not file
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "file not found"))
|
||||
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "file not found")))
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(let [{:keys [data] :as file} (files/decode-row file)]
|
||||
(-> file
|
||||
(assoc :page-name (dm/get-in data [:pages-index page-id :name]))
|
||||
(assoc :page-id page-id)
|
||||
(dissoc :data))))))
|
||||
|
||||
(defn- get-comment-thread
|
||||
[conn thread-id & {:as opts}]
|
||||
@ -93,23 +98,25 @@
|
||||
|
||||
(declare ^:private get-comment-threads)
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::share-id (s/nilable ::us/uuid))
|
||||
|
||||
(s/def ::get-comment-threads
|
||||
(s/and (s/keys :req [::rpc/profile-id]
|
||||
:opt-un [::file-id ::share-id ::team-id])
|
||||
#(or (:file-id %) (:team-id %))))
|
||||
(def ^:private
|
||||
schema:get-comment-threads
|
||||
[:and
|
||||
[:map {:title "get-comment-threads"}
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:team-id {:optional true} ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]]
|
||||
[::sm/contains-any #{:file-id :team-id}]])
|
||||
|
||||
(sv/defmethod ::get-comment-threads
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-comment-threads conn profile-id file-id)))
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:get-comment-threads}
|
||||
[cfg {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
|
||||
(def sql:comment-threads
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-comment-threads conn profile-id file-id))))
|
||||
|
||||
(def ^:private sql:comment-threads
|
||||
"select distinct on (ct.id)
|
||||
ct.*,
|
||||
f.name as file_name,
|
||||
@ -134,23 +141,24 @@
|
||||
(defn- get-comment-threads
|
||||
[conn profile-id file-id]
|
||||
(->> (db/exec! conn [sql:comment-threads profile-id file-id])
|
||||
(into [] (map decode-row))))
|
||||
(into [] xf-decode-row)))
|
||||
|
||||
;; --- COMMAND: Get Unread Comment Threads
|
||||
|
||||
(declare ^:private get-unread-comment-threads)
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::get-unread-comment-threads
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::team-id]))
|
||||
(def ^:private
|
||||
schema:get-unread-comment-threads
|
||||
[:map {:title "get-unread-comment-threads"}
|
||||
[:team-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::get-unread-comment-threads
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(get-unread-comment-threads conn profile-id team-id)))
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:get-unread-comment-threads}
|
||||
[cfg {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(get-unread-comment-threads conn profile-id team-id))))
|
||||
|
||||
(def sql:comment-threads-by-team
|
||||
"select distinct on (ct.id)
|
||||
@ -182,62 +190,60 @@
|
||||
(defn- get-unread-comment-threads
|
||||
[conn profile-id team-id]
|
||||
(->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id])
|
||||
(into [] (map decode-row))))
|
||||
|
||||
(into [] xf-decode-row)))
|
||||
|
||||
;; --- COMMAND: Get Single Comment Thread
|
||||
|
||||
(s/def ::get-comment-thread
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::us/id]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:get-comment-thread
|
||||
[:map {:title "get-comment-thread"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::get-comment-thread
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(let [sql (str "with threads as (" sql:comment-threads ")"
|
||||
"select * from threads where id = ?")]
|
||||
(-> (db/exec-one! conn [sql profile-id file-id id])
|
||||
(decode-row)))))
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:get-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(let [sql (str "with threads as (" sql:comment-threads ")"
|
||||
"select * from threads where id = ?")]
|
||||
(-> (db/exec-one! conn [sql profile-id file-id id])
|
||||
(decode-row))))))
|
||||
|
||||
;; --- COMMAND: Retrieve Comments
|
||||
|
||||
(declare ^:private get-comments)
|
||||
|
||||
(s/def ::thread-id ::us/uuid)
|
||||
(s/def ::get-comments
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::thread-id]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:get-comments
|
||||
[:map {:title "get-comments"}
|
||||
[:thread-id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::get-comments
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-comments conn thread-id))))
|
||||
|
||||
(def sql:comments
|
||||
"select c.* from comment as c
|
||||
where c.thread_id = ?
|
||||
order by c.created_at asc")
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:get-comments}
|
||||
[cfg {:keys [::rpc/profile-id thread-id share-id]}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-comments conn thread-id)))))
|
||||
|
||||
(defn- get-comments
|
||||
[conn thread-id]
|
||||
(->> (db/query conn :comment
|
||||
{:thread-id thread-id}
|
||||
{:order-by [[:created-at :asc]]})
|
||||
(into [] (map decode-row))))
|
||||
(into [] xf-decode-row)))
|
||||
|
||||
;; --- COMMAND: Get file comments users
|
||||
|
||||
;; All the profiles that had comment the file, plus the current
|
||||
;; profile.
|
||||
|
||||
(def sql:file-comment-users
|
||||
(def ^:private sql:file-comment-users
|
||||
"WITH available_profiles AS (
|
||||
SELECT DISTINCT owner_id AS id
|
||||
FROM comment
|
||||
@ -256,20 +262,22 @@
|
||||
[conn file-id profile-id]
|
||||
(db/exec! conn [sql:file-comment-users file-id profile-id]))
|
||||
|
||||
(s/def ::get-profiles-for-file-comments
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:get-profiles-for-file-comments
|
||||
[:map {:title "get-profiles-for-file-comments"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::get-profiles-for-file-comments
|
||||
"Retrieves a list of profiles with limited set of properties of all
|
||||
participants on comment threads of the file."
|
||||
{::doc/added "1.15"
|
||||
::doc/changes ["1.15" "Imported from queries and renamed."]}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-file-comments-users conn file-id profile-id)))
|
||||
::doc/changes ["1.15" "Imported from queries and renamed."]
|
||||
::sm/params schema:get-profiles-for-file-comments}
|
||||
[cfg {:keys [::rpc/profile-id file-id share-id]}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-file-comments-users conn file-id profile-id))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MUTATION COMMANDS
|
||||
@ -279,52 +287,52 @@
|
||||
|
||||
;; --- COMMAND: Create Comment Thread
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::position ::gpt/point)
|
||||
(s/def ::content ::us/string)
|
||||
(s/def ::frame-id ::us/uuid)
|
||||
|
||||
(s/def ::create-comment-thread
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::position ::content ::page-id ::frame-id]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:create-comment-thread
|
||||
[:map {:title "create-comment-thread"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:position ::gpt/point]
|
||||
[:content :string]
|
||||
[:page-id ::sm/uuid]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::create-comment-thread
|
||||
{::doc/added "1.15"
|
||||
::webhooks/event? true}
|
||||
::webhooks/event? true
|
||||
::rtry/enabled true
|
||||
::rtry/when rtry/conflict-exception?
|
||||
::sm/params schema:create-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(let [{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/comment-threads-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}))
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)]
|
||||
|
||||
(run! (partial quotes/check-quote! cfg)
|
||||
(list {::quotes/id ::quotes/comment-threads-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}))
|
||||
|
||||
(-> cfg
|
||||
(assoc ::rtry/when rtry/conflict-exception?)
|
||||
(assoc ::rtry/label "create-comment-thread")
|
||||
(rtry/invoke create-comment-thread {:created-at request-at
|
||||
:profile-id profile-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id}))))))
|
||||
(create-comment-thread conn {:created-at request-at
|
||||
:profile-id profile-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id})))))
|
||||
|
||||
(defn- create-comment-thread
|
||||
[{:keys [::db/conn]} {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
|
||||
[conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
|
||||
|
||||
(let [;; NOTE: we take the next seq number from a separate query because the whole
|
||||
;; operation can be retried on conflict, and in this case the new seq shold be
|
||||
;; retrieved from the database.
|
||||
@ -364,68 +372,72 @@
|
||||
|
||||
;; --- COMMAND: Update Comment Thread Status
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::share-id (s/nilable ::us/uuid))
|
||||
|
||||
(s/def ::update-comment-thread-status
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:update-comment-thread-status
|
||||
[:map {:title "update-comment-thread-status"}
|
||||
[:id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::update-comment-thread-status
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(upsert-comment-thread-status! conn profile-id id))))
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread-status}
|
||||
[cfg {:keys [::rpc/profile-id id share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(upsert-comment-thread-status! conn profile-id id)))))
|
||||
|
||||
|
||||
;; --- COMMAND: Update Comment Thread
|
||||
|
||||
(s/def ::is-resolved ::us/boolean)
|
||||
(s/def ::update-comment-thread
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id ::is-resolved]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:update-comment-thread
|
||||
[:map {:title "update-comment-thread"}
|
||||
[:id ::sm/uuid]
|
||||
[:is-resolved :boolean]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::update-comment-thread
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:is-resolved is-resolved}
|
||||
{:id id})
|
||||
nil)))
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id id is-resolved share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:is-resolved is-resolved}
|
||||
{:id id})
|
||||
nil))))
|
||||
|
||||
|
||||
;; --- COMMAND: Add Comment
|
||||
|
||||
(declare ^:private get-comment-thread)
|
||||
|
||||
(s/def ::create-comment
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::thread-id ::content]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:create-comment
|
||||
[:map {:title "create-comment"}
|
||||
[:thread-id ::sm/uuid]
|
||||
[:content :string]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::create-comment
|
||||
{::doc/added "1.15"
|
||||
::webhooks/event? true}
|
||||
::webhooks/event? true
|
||||
::sm/params schema:create-comment}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true)
|
||||
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
|
||||
|
||||
(files/check-comment-permissions! conn profile-id (:id file) share-id)
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id (:id file)})
|
||||
::quotes/file-id file-id})
|
||||
|
||||
;; Update the page-name cached attribute on comment thread table.
|
||||
(when (not= page-name (:page-name thread))
|
||||
@ -461,15 +473,17 @@
|
||||
|
||||
;; --- COMMAND: Update Comment
|
||||
|
||||
(s/def ::update-comment
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id ::content]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:update-comment
|
||||
[:map {:title "update-comment"}
|
||||
[:id ::sm/uuid]
|
||||
[:content :string]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::update-comment
|
||||
{::doc/added "1.15"}
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at id share-id content]}]
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::sql/for-update true)
|
||||
@ -482,7 +496,7 @@
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
|
||||
(let [{:keys [page-name] :as file} (get-file cfg file-id page-id)]
|
||||
(let [{:keys [page-name]} (get-file cfg file-id page-id)]
|
||||
(db/update! conn :comment
|
||||
{:content content
|
||||
:modified-at request-at}
|
||||
@ -496,79 +510,90 @@
|
||||
|
||||
;; --- COMMAND: Delete Comment Thread
|
||||
|
||||
(s/def ::delete-comment-thread
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:delete-comment-thread
|
||||
[:map {:title "delete-comment-thread"}
|
||||
[:id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::delete-comment-thread
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:delete-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id id share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
|
||||
(db/delete! conn :comment-thread {:id id})
|
||||
nil)))
|
||||
(db/delete! conn :comment-thread {:id id})
|
||||
nil))))
|
||||
|
||||
;; --- COMMAND: Delete comment
|
||||
|
||||
(s/def ::delete-comment
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:delete-comment
|
||||
[:map {:title "delete-comment"}
|
||||
[:id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::delete-comment
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::sql/for-update true)
|
||||
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
(db/delete! conn :comment {:id id})
|
||||
nil)))
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:delete-comment}
|
||||
[cfg {:keys [::rpc/profile-id id share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::sql/for-update true)
|
||||
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
(db/delete! conn :comment {:id id})
|
||||
nil))))
|
||||
|
||||
;; --- COMMAND: Update comment thread position
|
||||
|
||||
(s/def ::update-comment-thread-position
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id ::position ::frame-id]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:update-comment-thread-position
|
||||
[:map {:title "update-comment-thread-position"}
|
||||
[:id ::sm/uuid]
|
||||
[:position ::gpt/point]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::update-comment-thread-position
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:position (db/pgpoint position)
|
||||
:frame-id frame-id}
|
||||
{:id (:id thread)})
|
||||
nil)))
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread-position}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:position (db/pgpoint position)
|
||||
:frame-id frame-id}
|
||||
{:id (:id thread)})
|
||||
nil))))
|
||||
|
||||
;; --- COMMAND: Update comment frame
|
||||
|
||||
(s/def ::update-comment-thread-frame
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id ::frame-id]
|
||||
:opt-un [::share-id]))
|
||||
(def ^:private
|
||||
schema:update-comment-thread-frame
|
||||
[:map {:title "update-comment-thread-frame"}
|
||||
[:id ::sm/uuid]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::update-comment-thread-frame
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:frame-id frame-id}
|
||||
{:id id})
|
||||
nil)))
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread-frame}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:frame-id frame-id}
|
||||
{:id id})
|
||||
nil))))
|
||||
|
||||
@ -285,26 +285,27 @@
|
||||
(sv/defmethod ::create-file-object-thumbnail
|
||||
{::doc/added "1.19"
|
||||
::doc/module :files
|
||||
::climit/id :file-thumbnail-ops
|
||||
::climit/key-fn ::rpc/profile-id
|
||||
::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id]
|
||||
[:file-thumbnail-ops/global]]
|
||||
::rtry/enabled true
|
||||
::rtry/when rtry/conflict-exception?
|
||||
::audit/skip true
|
||||
::sm/params schema:create-file-object-thumbnail}
|
||||
|
||||
[cfg {:keys [::rpc/profile-id file-id object-id media tag]}]
|
||||
(media/validate-media-type! media)
|
||||
(media/validate-media-size! media)
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(media/validate-media-type! media)
|
||||
(media/validate-media-size! media)
|
||||
|
||||
(when-not (db/read-only? conn)
|
||||
(let [cfg (-> cfg
|
||||
(update ::sto/storage media/configure-assets-storage)
|
||||
(assoc ::rtry/when rtry/conflict-exception?)
|
||||
(assoc ::rtry/max-retries 5)
|
||||
(assoc ::rtry/label "create-file-object-thumbnail"))]
|
||||
(rtry/invoke cfg create-file-object-thumbnail!
|
||||
file-id object-id media (or tag "frame")))))))
|
||||
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame")))))))
|
||||
|
||||
;; --- MUTATION COMMAND: delete-file-object-thumbnail
|
||||
|
||||
@ -329,8 +330,8 @@
|
||||
{::doc/added "1.19"
|
||||
::doc/module :files
|
||||
::doc/deprecated "1.20"
|
||||
::climit/id :file-thumbnail-ops
|
||||
::climit/key-fn ::rpc/profile-id
|
||||
::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id]
|
||||
[:file-thumbnail-ops/global]]
|
||||
::audit/skip true}
|
||||
[cfg {:keys [::rpc/profile-id file-id object-id]}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
@ -392,27 +393,29 @@
|
||||
|
||||
media))
|
||||
|
||||
(def ^:private
|
||||
schema:create-file-thumbnail
|
||||
[:map {:title "create-file-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:revn :int]
|
||||
[:media ::media/upload]])
|
||||
|
||||
(sv/defmethod ::create-file-thumbnail
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
grid thumbnails."
|
||||
{::doc/added "1.19"
|
||||
::doc/module :files
|
||||
::audit/skip true
|
||||
::climit/id :file-thumbnail-ops
|
||||
::climit/key-fn ::rpc/profile-id
|
||||
::sm/params [:map {:title "create-file-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:revn :int]
|
||||
[:media ::media/upload]]}
|
||||
::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id]
|
||||
[:file-thumbnail-ops/global]]
|
||||
::rtry/enabled true
|
||||
::rtry/when rtry/conflict-exception?
|
||||
::sm/params schema:create-file-thumbnail}
|
||||
|
||||
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(when-not (db/read-only? conn)
|
||||
(let [cfg (-> cfg
|
||||
(update ::sto/storage media/configure-assets-storage)
|
||||
(assoc ::rtry/when rtry/conflict-exception?)
|
||||
(assoc ::rtry/max-retries 5)
|
||||
(assoc ::rtry/label "create-thumbnail"))
|
||||
media (rtry/invoke cfg create-file-thumbnail! params)]
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)
|
||||
media (create-file-thumbnail! cfg params)]
|
||||
{:uri (files/resolve-public-uri (:id media))})))))
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.set :as set]))
|
||||
[clojure.set :as set]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
;; --- SCHEMA
|
||||
|
||||
@ -132,8 +133,8 @@
|
||||
;; database.
|
||||
|
||||
(sv/defmethod ::update-file
|
||||
{::climit/id :update-file/by-profile
|
||||
::climit/key-fn ::rpc/profile-id
|
||||
{::climit/id [[:update-file/by-profile ::rpc/profile-id]
|
||||
[:update-file/global]]
|
||||
::webhooks/event? true
|
||||
::webhooks/batch-timeout (dt/duration "2m")
|
||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||
@ -232,13 +233,9 @@
|
||||
(defn- update-file*
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg}
|
||||
{:keys [profile-id file changes session-id ::created-at skip-validate] :as params}]
|
||||
(let [;; Process the file data in the CLIMIT context; scheduling it
|
||||
;; to be executed on a separated executor for avoid to do the
|
||||
;; CPU intensive operation on vthread.
|
||||
|
||||
update-fdata-fn (partial update-file-data cfg file changes skip-validate)
|
||||
file (-> (climit/configure cfg :update-file/global)
|
||||
(climit/run! update-fdata-fn executor))]
|
||||
(let [;; Process the file data on separated thread for avoid to do
|
||||
;; the CPU intensive operation on vthread.
|
||||
file (px/invoke! executor (partial update-file-data cfg file changes skip-validate))]
|
||||
|
||||
(db/insert! conn :file-change
|
||||
{:id (uuid/next)
|
||||
@ -306,7 +303,6 @@
|
||||
(fmg/migrate-file))
|
||||
file)
|
||||
|
||||
|
||||
;; WARNING: this ruins performance; maybe we need to find
|
||||
;; some other way to do general validation
|
||||
libs (when (and (or (contains? cf/flags :file-validation)
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.media :as media]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as climit]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.projects :as projects]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
@ -26,7 +26,8 @@
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]))
|
||||
[app.worker :as-alias wrk]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
|
||||
(def valid-style #{"normal" "italic"})
|
||||
@ -87,6 +88,8 @@
|
||||
|
||||
(sv/defmethod ::create-font-variant
|
||||
{::doc/added "1.18"
|
||||
::climit/id [[:process-font/by-profile ::rpc/profile-id]
|
||||
[:process-font/global]]
|
||||
::webhooks/event? true
|
||||
::sm/params schema:create-font-variant}
|
||||
[cfg {:keys [::rpc/profile-id team-id] :as params}]
|
||||
@ -100,7 +103,7 @@
|
||||
(create-font-variant cfg (assoc params :profile-id profile-id))))))
|
||||
|
||||
(defn create-font-variant
|
||||
[{:keys [::sto/storage ::db/conn] :as cfg} {:keys [data] :as params}]
|
||||
[{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}]
|
||||
(letfn [(generate-missing! [data]
|
||||
(let [data (media/run {:cmd :generate-fonts :input data})]
|
||||
(when (and (not (contains? data "font/otf"))
|
||||
@ -152,9 +155,7 @@
|
||||
:otf-file-id (:id otf)
|
||||
:ttf-file-id (:id ttf)}))]
|
||||
|
||||
(let [data (-> (climit/configure cfg :process-font/global)
|
||||
(climit/run! (partial generate-missing! data)
|
||||
(::wrk/executor cfg)))
|
||||
(let [data (px/invoke! executor (partial generate-missing! data))
|
||||
assets (persist-fonts-files! data)
|
||||
result (insert-font-variant! assets)]
|
||||
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))
|
||||
|
||||
@ -27,7 +27,8 @@
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]))
|
||||
[datoteka.io :as io]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(def default-max-file-size
|
||||
(* 1024 1024 10)) ; 10 MiB
|
||||
@ -56,20 +57,25 @@
|
||||
:opt-un [::id]))
|
||||
|
||||
(sv/defmethod ::upload-file-media-object
|
||||
{::doc/added "1.17"}
|
||||
{::doc/added "1.17"
|
||||
::climit/id [[:process-image/by-profile ::rpc/profile-id]
|
||||
[:process-image/global]]}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}]
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(media/validate-media-type! content)
|
||||
(media/validate-media-size! content)
|
||||
(let [object (db/run! cfg #(create-file-media-object % params))
|
||||
props {:name (:name params)
|
||||
:file-id file-id
|
||||
:is-local (:is-local params)
|
||||
:size (:size content)
|
||||
:mtype (:mtype content)}]
|
||||
(with-meta object
|
||||
{::audit/replace-props props}))))
|
||||
|
||||
(db/run! cfg (fn [cfg]
|
||||
(let [object (create-file-media-object cfg params)
|
||||
props {:name (:name params)
|
||||
:file-id file-id
|
||||
:is-local (:is-local params)
|
||||
:size (:size content)
|
||||
:mtype (:mtype content)}]
|
||||
(with-meta object
|
||||
{::audit/replace-props props}))))))
|
||||
|
||||
(defn- big-enough-for-thumbnail?
|
||||
"Checks if the provided image info is big enough for
|
||||
@ -144,12 +150,10 @@
|
||||
(assoc ::image (process-main-image info)))))
|
||||
|
||||
(defn create-file-media-object
|
||||
[{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg}
|
||||
[{:keys [::sto/storage ::db/conn ::wrk/executor]}
|
||||
{:keys [id file-id is-local name content]}]
|
||||
|
||||
(let [result (-> (climit/configure cfg :process-image/global)
|
||||
(climit/run! (partial process-image content) executor))
|
||||
|
||||
(let [result (px/invoke! executor (partial process-image content))
|
||||
image (sto/put-object! storage (::image result))
|
||||
thumb (when-let [params (::thumb result)]
|
||||
(sto/put-object! storage params))]
|
||||
@ -183,7 +187,7 @@
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(db/run! cfg #(create-file-media-object-from-url % params))))
|
||||
(create-file-media-object-from-url cfg (assoc params :profile-id profile-id))))
|
||||
|
||||
(defn download-image
|
||||
[{:keys [::http/client]} uri]
|
||||
@ -235,7 +239,16 @@
|
||||
params (-> params
|
||||
(assoc :content content)
|
||||
(assoc :name (or name (:filename content))))]
|
||||
(create-file-media-object cfg params)))
|
||||
|
||||
;; NOTE: we use the climit here in a dynamic invocation because we
|
||||
;; don't want saturate the process-image limit with IO (download
|
||||
;; of external image)
|
||||
(-> cfg
|
||||
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
|
||||
[:process-image/global]])
|
||||
(assoc ::climit/profile-id (:profile-id params))
|
||||
(assoc ::climit/label "create-file-media-object-from-url")
|
||||
(climit/invoke! db/run! cfg create-file-media-object params))))
|
||||
|
||||
;; --- Clone File Media object (Upload and create from url)
|
||||
|
||||
|
||||
@ -28,7 +28,8 @@
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[cuerdas.core :as str]))
|
||||
[cuerdas.core :as str]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(declare check-profile-existence!)
|
||||
(declare decode-row)
|
||||
@ -137,25 +138,24 @@
|
||||
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]]))
|
||||
|
||||
(sv/defmethod ::update-profile-password
|
||||
{:doc/added "1.0"
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema:update-profile-password
|
||||
::sm/result :nil}
|
||||
::climit/id :auth/global}
|
||||
[cfg {:keys [::rpc/profile-id password] :as params}]
|
||||
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg ::db/conn conn)
|
||||
profile (validate-password! cfg (assoc params :profile-id profile-id))
|
||||
session-id (::session/id params)]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(let [profile (validate-password! cfg (assoc params :profile-id profile-id))
|
||||
session-id (::session/id params)]
|
||||
|
||||
(when (= (str/lower (:email profile))
|
||||
(str/lower (:password params)))
|
||||
(ex/raise :type :validation
|
||||
:code :email-as-password
|
||||
:hint "you can't use your email as password"))
|
||||
(when (= (str/lower (:email profile))
|
||||
(str/lower (:password params)))
|
||||
(ex/raise :type :validation
|
||||
:code :email-as-password
|
||||
:hint "you can't use your email as password"))
|
||||
|
||||
(update-profile-password! conn (assoc profile :password password))
|
||||
(invalidate-profile-session! cfg profile-id session-id)
|
||||
nil)))
|
||||
(update-profile-password! cfg (assoc profile :password password))
|
||||
(invalidate-profile-session! cfg profile-id session-id)
|
||||
nil))))
|
||||
|
||||
(defn- invalidate-profile-session!
|
||||
"Removes all sessions except the current one."
|
||||
@ -173,10 +173,10 @@
|
||||
profile))
|
||||
|
||||
(defn update-profile-password!
|
||||
[conn {:keys [id password] :as profile}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}]
|
||||
(when-not (db/read-only? conn)
|
||||
(db/update! conn :profile
|
||||
{:password (auth/derive-password password)}
|
||||
{:password (derive-password cfg password)}
|
||||
{:id id})
|
||||
nil))
|
||||
|
||||
@ -203,6 +203,7 @@
|
||||
|
||||
(defn update-profile-photo
|
||||
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}]
|
||||
|
||||
(let [photo (upload-photo cfg params)
|
||||
profile (db/get-by-id pool :profile profile-id ::sql/for-update true)]
|
||||
|
||||
@ -241,8 +242,11 @@
|
||||
|
||||
(defn upload-photo
|
||||
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file]}]
|
||||
(let [params (-> (climit/configure cfg :process-image/global)
|
||||
(climit/run! (partial generate-thumbnail! file) executor))]
|
||||
(let [params (-> cfg
|
||||
(assoc ::climit/id :process-image/global)
|
||||
(assoc ::climit/label "upload-photo")
|
||||
(assoc ::climit/executor executor)
|
||||
(climit/invoke! generate-thumbnail! file))]
|
||||
(sto/put-object! storage params)))
|
||||
|
||||
|
||||
@ -438,17 +442,13 @@
|
||||
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
|
||||
|
||||
(defn derive-password
|
||||
[cfg password]
|
||||
[{:keys [::wrk/executor]} password]
|
||||
(when password
|
||||
(-> (climit/configure cfg :derive-password/global)
|
||||
(climit/run! (partial auth/derive-password password)
|
||||
(::wrk/executor cfg)))))
|
||||
(px/invoke! executor (partial auth/derive-password password))))
|
||||
|
||||
(defn verify-password
|
||||
[cfg password password-data]
|
||||
(-> (climit/configure cfg :derive-password/global)
|
||||
(climit/run! (partial auth/verify-password password password-data)
|
||||
(::wrk/executor cfg))))
|
||||
[{:keys [::wrk/executor]} password password-data]
|
||||
(px/invoke! executor (partial auth/verify-password password password-data)))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [props] :as row}]
|
||||
|
||||
@ -7,8 +7,10 @@
|
||||
(ns app.rpc.quotes
|
||||
"Penpot resource usage quotes."
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@ -23,21 +25,15 @@
|
||||
;; PUBLIC API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::conn ::db/pool-or-conn)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::incr (s/and int? pos?))
|
||||
(s/def ::target ::us/string)
|
||||
|
||||
(s/def ::quote
|
||||
(s/keys :req [::id ::profile-id]
|
||||
:opt [::conn
|
||||
::team-id
|
||||
::project-id
|
||||
::file-id
|
||||
::incr]))
|
||||
(def ^:private schema:quote
|
||||
(sm/define
|
||||
[:map {:title "Quote"}
|
||||
[::team-id {:optional true} ::sm/uuid]
|
||||
[::project-id {:optional true} ::sm/uuid]
|
||||
[::file-id {:optional true} ::sm/uuid]
|
||||
[::incr {:optional true} [:int {:min 0}]]
|
||||
[::id :keyword]
|
||||
[::profile-id ::sm/uuid]]))
|
||||
|
||||
(def ^:private enabled (volatile! true))
|
||||
|
||||
@ -52,15 +48,22 @@
|
||||
(vswap! enabled (constantly false)))
|
||||
|
||||
(defn check-quote!
|
||||
[conn quote]
|
||||
(us/assert! ::db/pool-or-conn conn)
|
||||
(us/assert! ::quote quote)
|
||||
[ds quote]
|
||||
(dm/assert!
|
||||
"expected valid quote map"
|
||||
(sm/validate schema:quote quote))
|
||||
|
||||
(when (contains? cf/flags :quotes)
|
||||
(when @enabled
|
||||
(check-quote (assoc quote ::conn conn ::target (name (::id quote)))))))
|
||||
;; This approach add flexibility on how and where the
|
||||
;; check-quote! can be called (in or out of transaction)
|
||||
(db/run! ds (fn [cfg]
|
||||
(-> (merge cfg quote)
|
||||
(assoc ::target (name (::id quote)))
|
||||
(check-quote)))))))
|
||||
|
||||
(defn- send-notification!
|
||||
[{:keys [::conn] :as params}]
|
||||
[{:keys [::db/conn] :as params}]
|
||||
(l/warn :hint "max quote reached"
|
||||
:target (::target params)
|
||||
:profile-id (some-> params ::profile-id str)
|
||||
@ -93,7 +96,7 @@
|
||||
:content content}]}))))
|
||||
|
||||
(defn- generic-check!
|
||||
[{:keys [::conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}]
|
||||
[{:keys [::db/conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}]
|
||||
(let [quote (->> (db/exec! conn quote-sql)
|
||||
(map :quote)
|
||||
(reduce max (- Integer/MAX_VALUE)))
|
||||
@ -347,7 +350,6 @@
|
||||
(assoc ::count-sql [sql:get-comments-per-file file-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: DEFAULT
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
|
||||
(ns app.rpc.retry
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.util.services :as sv])
|
||||
(:import
|
||||
org.postgresql.util.PSQLException))
|
||||
@ -15,12 +15,29 @@
|
||||
(defn conflict-exception?
|
||||
"Check if exception matches a insertion conflict on postgresql."
|
||||
[e]
|
||||
(and (instance? PSQLException e)
|
||||
(= "23505" (.getSQLState ^PSQLException e))))
|
||||
(when-let [cause (ex/instance? PSQLException e)]
|
||||
(= "23505" (.getSQLState ^PSQLException cause))))
|
||||
|
||||
(def ^:private always-false
|
||||
(constantly false))
|
||||
|
||||
(defn invoke!
|
||||
[{:keys [::max-retries] :or {max-retries 3} :as cfg} f & args]
|
||||
(loop [rnum 1]
|
||||
(let [match? (get cfg ::when always-false)
|
||||
result (try
|
||||
(apply f cfg args)
|
||||
(catch Throwable cause
|
||||
(if (and (match? cause) (<= rnum max-retries))
|
||||
::retry
|
||||
(throw cause))))]
|
||||
(if (= ::retry result)
|
||||
(let [label (get cfg ::label "anonymous")]
|
||||
(l/warn :hint "retrying operation" :label label :retry rnum)
|
||||
(recur (inc rnum)))
|
||||
result))))
|
||||
|
||||
|
||||
(defn wrap-retry
|
||||
[_ f {:keys [::sv/name] :as mdata}]
|
||||
|
||||
@ -29,36 +46,10 @@
|
||||
matches? (get mdata ::when always-false)]
|
||||
(l/dbg :hint "wrapping retry" :name name :max-retries max-retries)
|
||||
(fn [cfg params]
|
||||
((fn recursive-invoke [retry]
|
||||
(try
|
||||
(f cfg params)
|
||||
(catch Throwable cause
|
||||
(if (matches? cause)
|
||||
(let [current-retry (inc retry)]
|
||||
(l/wrn :hint "retrying operation" :retry current-retry :service name)
|
||||
(if (<= current-retry max-retries)
|
||||
(recursive-invoke current-retry)
|
||||
(throw cause)))
|
||||
(throw cause))))) 1)))
|
||||
(-> cfg
|
||||
(assoc ::max-retries max-retries)
|
||||
(assoc ::when matches?)
|
||||
(assoc ::label name)
|
||||
(invoke! f params))))
|
||||
f))
|
||||
|
||||
(defn invoke
|
||||
[{:keys [::db/conn ::max-retries] :or {max-retries 3} :as cfg} f & args]
|
||||
(assert (db/connection? conn) "invalid database connection")
|
||||
(loop [rnum 1]
|
||||
(let [match? (get cfg ::when always-false)
|
||||
result (let [spoint (db/savepoint conn)]
|
||||
(try
|
||||
(let [result (apply f cfg args)]
|
||||
(db/release! conn spoint)
|
||||
result)
|
||||
(catch Throwable cause
|
||||
(db/rollback! conn spoint)
|
||||
(if (and (match? cause) (<= rnum max-retries))
|
||||
::retry
|
||||
(throw cause)))))]
|
||||
(if (= ::retry result)
|
||||
(let [label (get cfg ::label "anonymous")]
|
||||
(l/warn :hint "retrying operation" :label label :retry rnum)
|
||||
(recur (inc rnum)))
|
||||
result))))
|
||||
|
||||
@ -71,6 +71,7 @@
|
||||
:enable-email-verification
|
||||
:enable-smtp
|
||||
:enable-quotes
|
||||
:enable-rpc-climit
|
||||
:enable-feature-fdata-pointer-map
|
||||
:enable-feature-fdata-objets-map
|
||||
:enable-feature-components-v2
|
||||
|
||||
@ -74,10 +74,9 @@
|
||||
[class cause]
|
||||
(loop [cause cause]
|
||||
(if (c/instance? class cause)
|
||||
true
|
||||
(if-let [cause (ex-cause cause)]
|
||||
(recur cause)
|
||||
false)))))
|
||||
cause
|
||||
(when-let [cause (ex-cause cause)]
|
||||
(recur cause))))))
|
||||
|
||||
;; NOTE: idea for a macro for error handling
|
||||
;; (pu/try-let [cause (p/await (get-object-data backend object))]
|
||||
|
||||
@ -111,6 +111,7 @@
|
||||
|
||||
shadow-width
|
||||
(->> (:shadow shape)
|
||||
(remove :hidden)
|
||||
(map #(case (:style % :drop-shadow)
|
||||
:drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10)
|
||||
0))
|
||||
@ -118,6 +119,7 @@
|
||||
|
||||
shadow-height
|
||||
(->> (:shadow shape)
|
||||
(remove :hidden)
|
||||
(map #(case (:style % :drop-shadow)
|
||||
:drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10)
|
||||
0))
|
||||
|
||||
@ -340,30 +340,43 @@
|
||||
(grc/points->rect points))))
|
||||
|
||||
(defn content->selrect [content]
|
||||
(let [calc-extremities
|
||||
(fn [command prev]
|
||||
(case (:command command)
|
||||
:move-to [(command->point command)]
|
||||
(let [extremities
|
||||
(loop [points #{}
|
||||
from-p nil
|
||||
move-p nil
|
||||
content (seq content)]
|
||||
(if content
|
||||
(let [command (first content)
|
||||
to-p (command->point command)
|
||||
|
||||
;; If it's a line we add the beginning point and endpoint
|
||||
:line-to [(command->point prev)
|
||||
(command->point command)]
|
||||
[from-p move-p command-pts]
|
||||
(case (:command command)
|
||||
:move-to [to-p to-p (when to-p [to-p])]
|
||||
:close-path [move-p move-p (when move-p [move-p])]
|
||||
:line-to [to-p move-p (when (and from-p to-p) [from-p to-p])]
|
||||
:curve-to [to-p move-p
|
||||
(let [c1 (command->point command :c1)
|
||||
c2 (command->point command :c2)
|
||||
curve [from-p to-p c1 c2]]
|
||||
(when (and from-p to-p c1 c2)
|
||||
(into [from-p to-p]
|
||||
(->> (curve-extremities curve)
|
||||
(map #(curve-values curve %))))))]
|
||||
[to-p move-p []])]
|
||||
|
||||
;; We return the bezier extremities
|
||||
:curve-to (into [(command->point prev)
|
||||
(command->point command)]
|
||||
(let [curve [(command->point prev)
|
||||
(command->point command)
|
||||
(command->point command :c1)
|
||||
(command->point command :c2)]]
|
||||
(->> (curve-extremities curve)
|
||||
(map #(curve-values curve %)))))
|
||||
[]))
|
||||
(recur (apply conj points command-pts) from-p move-p (next content)))
|
||||
points))
|
||||
|
||||
extremities (mapcat calc-extremities
|
||||
content
|
||||
(concat [nil] content))]
|
||||
(grc/points->rect extremities)))
|
||||
;; We haven't found any extremes so we turn the commands to points
|
||||
extremities
|
||||
(if (empty? extremities)
|
||||
(->> content (keep command->point))
|
||||
extremities)]
|
||||
|
||||
;; If no points are returned we return an empty rect.
|
||||
(if (d/not-empty? extremities)
|
||||
(grc/points->rect extremities)
|
||||
(grc/make-rect))))
|
||||
|
||||
(defn move-content [content move-vec]
|
||||
(let [dx (:x move-vec)
|
||||
|
||||
@ -140,6 +140,28 @@
|
||||
|
||||
(gmt/translate (gpt/negate shape-center)))))
|
||||
|
||||
(defn inverse-transform-matrix
|
||||
([shape]
|
||||
(inverse-transform-matrix shape nil))
|
||||
|
||||
([shape params]
|
||||
(inverse-transform-matrix shape params (or (gco/shape->center shape) (gpt/point 0 0))))
|
||||
|
||||
([{:keys [flip-x flip-y transform-inverse] :as shape} {:keys [no-flip]} shape-center]
|
||||
(-> (gmt/matrix)
|
||||
(gmt/translate shape-center)
|
||||
|
||||
(cond-> (and flip-x no-flip)
|
||||
(gmt/scale (gpt/point -1 1)))
|
||||
|
||||
(cond-> (and flip-y no-flip)
|
||||
(gmt/scale (gpt/point 1 -1)))
|
||||
|
||||
(cond-> (some? transform-inverse)
|
||||
(gmt/multiply transform-inverse))
|
||||
|
||||
(gmt/translate (gpt/negate shape-center)))))
|
||||
|
||||
(defn transform-str
|
||||
([shape]
|
||||
(transform-str shape nil))
|
||||
@ -152,21 +174,6 @@
|
||||
(dm/str (transform-matrix shape params))
|
||||
"")))
|
||||
|
||||
;; FIXME: performance
|
||||
(defn inverse-transform-matrix
|
||||
([shape]
|
||||
(let [shape-center (or (gco/shape->center shape)
|
||||
(gpt/point 0 0))]
|
||||
(inverse-transform-matrix shape shape-center)))
|
||||
([{:keys [flip-x flip-y] :as shape} center]
|
||||
(-> (gmt/matrix)
|
||||
(gmt/translate center)
|
||||
(cond->
|
||||
flip-x (gmt/scale (gpt/point -1 1))
|
||||
flip-y (gmt/scale (gpt/point 1 -1)))
|
||||
(gmt/multiply (:transform-inverse shape (gmt/matrix)))
|
||||
(gmt/translate (gpt/negate center)))))
|
||||
|
||||
;; FIXME: move to geom rect?
|
||||
(defn transform-rect
|
||||
"Transform a rectangles and changes its attributes"
|
||||
|
||||
@ -17,7 +17,11 @@
|
||||
[id objects]
|
||||
(->> (tree-seq
|
||||
#(d/not-empty? (dm/get-in objects [% :shapes]))
|
||||
#(dm/get-in objects [% :shapes])
|
||||
(fn [id]
|
||||
(let [shape (get objects id)]
|
||||
(cond->> (:shapes shape)
|
||||
(and (ctl/flex-layout? shape) (ctl/reverse? shape))
|
||||
(reverse))))
|
||||
id)
|
||||
(map #(get objects %))))
|
||||
|
||||
|
||||
@ -15,15 +15,16 @@
|
||||
|
||||
(defn rect->snap-points
|
||||
[rect]
|
||||
(let [x (dm/get-prop rect :x)
|
||||
y (dm/get-prop rect :y)
|
||||
w (dm/get-prop rect :width)
|
||||
h (dm/get-prop rect :height)]
|
||||
#{(gpt/point x y)
|
||||
(gpt/point (+ x w) y)
|
||||
(gpt/point (+ x w) (+ y h))
|
||||
(gpt/point x (+ y h))
|
||||
(grc/rect->center rect)}))
|
||||
(when (some? rect)
|
||||
(let [x (dm/get-prop rect :x)
|
||||
y (dm/get-prop rect :y)
|
||||
w (dm/get-prop rect :width)
|
||||
h (dm/get-prop rect :height)]
|
||||
#{(gpt/point x y)
|
||||
(gpt/point (+ x w) y)
|
||||
(gpt/point (+ x w) (+ y h))
|
||||
(gpt/point x (+ y h))
|
||||
(grc/rect->center rect)})))
|
||||
|
||||
(defn- frame->snap-points
|
||||
[frame]
|
||||
|
||||
@ -319,6 +319,12 @@
|
||||
::message (delay ~message)})
|
||||
nil)))
|
||||
|
||||
(defmacro log
|
||||
[level & params]
|
||||
`(do
|
||||
(log! ::logger ~(str *ns*) ::level ~level ~@params)
|
||||
nil))
|
||||
|
||||
(defmacro info
|
||||
[& params]
|
||||
`(do
|
||||
|
||||
@ -35,8 +35,18 @@
|
||||
|
||||
(def tags-to-remove #{:linearGradient :radialGradient :metadata :mask :clipPath :filter :title})
|
||||
|
||||
(defn- camelize
|
||||
[s]
|
||||
(when (string? s)
|
||||
(let [vendor? (str/starts-with? s "-")
|
||||
result #?(:cljs (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", s)
|
||||
:clj (str/camel s))]
|
||||
(if ^boolean vendor?
|
||||
(str/capital result)
|
||||
result))))
|
||||
|
||||
;; https://www.w3.org/TR/SVG11/eltindex.html
|
||||
(def svg-tags-list
|
||||
(def svg-tags
|
||||
#{:a
|
||||
:altGlyph
|
||||
:altGlyphDef
|
||||
@ -118,7 +128,7 @@
|
||||
:vkern})
|
||||
|
||||
;; https://www.w3.org/TR/SVG11/attindex.html
|
||||
(def svg-attr-list
|
||||
(def svg-attrs
|
||||
#{:accent-height
|
||||
:accumulate
|
||||
:additive
|
||||
@ -212,26 +222,6 @@
|
||||
:name
|
||||
:numOctaves
|
||||
:offset
|
||||
;; We don't support events
|
||||
;;:onabort
|
||||
;;:onactivate
|
||||
;;:onbegin
|
||||
;;:onclick
|
||||
;;:onend
|
||||
;;:onerror
|
||||
;;:onfocusin
|
||||
;;:onfocusout
|
||||
;;:onload
|
||||
;;:onmousedown
|
||||
;;:onmousemove
|
||||
;;:onmouseout
|
||||
;;:onmouseover
|
||||
;;:onmouseup
|
||||
;;:onrepeat
|
||||
;;:onresize
|
||||
;;:onscroll
|
||||
;;:onunload
|
||||
;;:onzoom
|
||||
:operator
|
||||
:order
|
||||
:orient
|
||||
@ -336,7 +326,8 @@
|
||||
:z
|
||||
:zoomAndPan})
|
||||
|
||||
(def svg-present-list
|
||||
(def svg-presentation-attrs
|
||||
"A set of presentation SVG attributes as per SVG spec."
|
||||
#{:alignment-baseline
|
||||
:baseline-shift
|
||||
:clip-path
|
||||
@ -399,52 +390,52 @@
|
||||
:mask-type})
|
||||
|
||||
(def inheritable-props
|
||||
[:style
|
||||
:clip-rule
|
||||
:color
|
||||
:color-interpolation
|
||||
:color-interpolation-filters
|
||||
:color-profile
|
||||
:color-rendering
|
||||
:cursor
|
||||
:direction
|
||||
:dominant-baseline
|
||||
:fill
|
||||
:fill-opacity
|
||||
:fill-rule
|
||||
:font
|
||||
:font-family
|
||||
:font-size
|
||||
:font-size-adjust
|
||||
:font-stretch
|
||||
:font-style
|
||||
:font-variant
|
||||
:font-weight
|
||||
:glyph-orientation-horizontal
|
||||
:glyph-orientation-vertical
|
||||
:image-rendering
|
||||
:letter-spacing
|
||||
:marker
|
||||
:marker-end
|
||||
:marker-mid
|
||||
:marker-start
|
||||
:paint-order
|
||||
:pointer-events
|
||||
:shape-rendering
|
||||
:stroke
|
||||
:stroke-dasharray
|
||||
:stroke-dashoffset
|
||||
:stroke-linecap
|
||||
:stroke-linejoin
|
||||
:stroke-miterlimit
|
||||
:stroke-opacity
|
||||
:stroke-width
|
||||
:text-anchor
|
||||
:text-rendering
|
||||
:transform
|
||||
:visibility
|
||||
:word-spacing
|
||||
:writing-mode])
|
||||
#{:style
|
||||
:clip-rule
|
||||
:color
|
||||
:color-interpolation
|
||||
:color-interpolation-filters
|
||||
:color-profile
|
||||
:color-rendering
|
||||
:cursor
|
||||
:direction
|
||||
:dominant-baseline
|
||||
:fill
|
||||
:fill-opacity
|
||||
:fill-rule
|
||||
:font
|
||||
:font-family
|
||||
:font-size
|
||||
:font-size-adjust
|
||||
:font-stretch
|
||||
:font-style
|
||||
:font-variant
|
||||
:font-weight
|
||||
:glyph-orientation-horizontal
|
||||
:glyph-orientation-vertical
|
||||
:image-rendering
|
||||
:letter-spacing
|
||||
:marker
|
||||
:marker-end
|
||||
:marker-mid
|
||||
:marker-start
|
||||
:paint-order
|
||||
:pointer-events
|
||||
:shape-rendering
|
||||
:stroke
|
||||
:stroke-dasharray
|
||||
:stroke-dashoffset
|
||||
:stroke-linecap
|
||||
:stroke-linejoin
|
||||
:stroke-miterlimit
|
||||
:stroke-opacity
|
||||
:stroke-width
|
||||
:text-anchor
|
||||
:text-rendering
|
||||
:transform
|
||||
:visibility
|
||||
:word-spacing
|
||||
:writing-mode})
|
||||
|
||||
(def gradient-tags
|
||||
#{:linearGradient
|
||||
@ -517,9 +508,28 @@
|
||||
:text
|
||||
:view})
|
||||
|
||||
;; Props not supported by react we need to keep them lowercase
|
||||
(def non-react-props
|
||||
#{:mask-type})
|
||||
(defn prop-key
|
||||
"Convert an attr key to a react compatible prop key. Returns nil if key is empty or invalid"
|
||||
[k]
|
||||
(let [kn (cond
|
||||
(string? k) k
|
||||
(keyword? k) (name k))]
|
||||
(case kn
|
||||
("" nil) nil
|
||||
"class" :className
|
||||
"for" :htmlFor
|
||||
(let [kn1 (subs kn 0 1)]
|
||||
(if (= kn1 (str/upper kn1))
|
||||
(-> kn camelize str/capital keyword)
|
||||
(-> kn camelize keyword))))))
|
||||
|
||||
(def svg-props
|
||||
"A set of all attrs (including the presentation) converted to
|
||||
camelCase for make it React compatible."
|
||||
(let [xf (map prop-key)]
|
||||
(-> #{}
|
||||
(into xf svg-attrs)
|
||||
(into xf svg-presentation-attrs))))
|
||||
|
||||
;; Defaults for some tags per spec https://www.w3.org/TR/SVG11/single-page.html
|
||||
;; they are basically the defaults that can be percents and we need to replace because
|
||||
@ -564,16 +574,6 @@
|
||||
:else
|
||||
num-str))
|
||||
|
||||
(defn- camelize
|
||||
[s]
|
||||
(when (string? s)
|
||||
(let [vendor? (str/starts-with? s "-")
|
||||
result #?(:cljs (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", s)
|
||||
:clj (str/camel s))]
|
||||
(if ^boolean vendor?
|
||||
(str/capital result)
|
||||
result))))
|
||||
|
||||
(defn parse-style
|
||||
[style]
|
||||
(reduce (fn [res item]
|
||||
@ -604,15 +604,13 @@
|
||||
|
||||
([attrs whitelist?]
|
||||
(reduce-kv (fn [res k v]
|
||||
(if (or (not whitelist?)
|
||||
(contains? svg-attr-list k)
|
||||
(contains? svg-present-list k))
|
||||
(let [k (prop-key k)]
|
||||
(cond
|
||||
(nil? v)
|
||||
(nil? k)
|
||||
res
|
||||
|
||||
(= k :class)
|
||||
(assoc res :className v)
|
||||
(nil? v)
|
||||
res
|
||||
|
||||
(= k :style)
|
||||
(let [v (if (string? v) (parse-style v) v)
|
||||
@ -622,11 +620,10 @@
|
||||
res))
|
||||
|
||||
:else
|
||||
(let [k (if (contains? non-react-props k)
|
||||
k
|
||||
(-> k d/name camelize keyword))]
|
||||
(assoc res k v)))
|
||||
res))
|
||||
(if (or (not whitelist?) (contains? svg-props k))
|
||||
(let [v (if (string? v) (str/trim v) v)]
|
||||
(assoc res k v))
|
||||
res))))
|
||||
{}
|
||||
attrs)))
|
||||
|
||||
@ -674,7 +671,7 @@
|
||||
|
||||
(let [remove-node? (fn [{:keys [tag]}] (and (some? tag)
|
||||
(or (contains? tags-to-remove tag)
|
||||
(not (contains? svg-tags-list tag)))))
|
||||
(not (contains? svg-tags tag)))))
|
||||
rec-result (->> (:content node) (map extract-defs))
|
||||
node (assoc node :content (->> rec-result (map second) (filterv (comp not remove-node?))))
|
||||
|
||||
|
||||
@ -53,6 +53,9 @@
|
||||
(def text-transform-attrs
|
||||
[:text-transform])
|
||||
|
||||
(def text-fills
|
||||
[:fills])
|
||||
|
||||
(def shape-attrs
|
||||
[:grow-type])
|
||||
|
||||
@ -70,7 +73,8 @@
|
||||
text-font-attrs
|
||||
text-spacing-attrs
|
||||
text-decoration-attrs
|
||||
text-transform-attrs))
|
||||
text-transform-attrs
|
||||
text-fills))
|
||||
|
||||
(def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs))
|
||||
|
||||
@ -240,6 +244,21 @@
|
||||
(run! #(.appendCodePoint sb (int %)) (subvec cpoints start end))
|
||||
(.toString sb))))
|
||||
|
||||
(defn- fix-gradients
|
||||
"Conversion from draft doesn't convert correctly the fills gradient types. This
|
||||
function change the type from string to keyword of the gradient type"
|
||||
[data]
|
||||
(letfn [(fix-type [type]
|
||||
(cond-> type
|
||||
(string? type) keyword))
|
||||
|
||||
(update-fill [fill]
|
||||
(d/update-in-when fill [:fill-color-gradient :type] fix-type))
|
||||
|
||||
(update-all-fills [fills]
|
||||
(mapv update-fill fills))]
|
||||
(d/update-when data :fills update-all-fills)))
|
||||
|
||||
(defn convert-from-draft
|
||||
[content]
|
||||
(letfn [(extract-text [cpoints part]
|
||||
@ -247,7 +266,9 @@
|
||||
end (inc (first (last part)))
|
||||
text (code-points->text cpoints start end)
|
||||
attrs (second (first part))]
|
||||
(assoc attrs :text text)))
|
||||
(-> attrs
|
||||
(fix-gradients)
|
||||
(assoc :text text))))
|
||||
|
||||
(split-texts [text styles]
|
||||
(let [cpoints (text->code-points text)
|
||||
@ -264,7 +285,8 @@
|
||||
(let [key (get block :key)
|
||||
text (get block :text)
|
||||
styles (get block :inlineStyleRanges)
|
||||
data (get block :data)]
|
||||
data (->> (get block :data)
|
||||
fix-gradients)]
|
||||
(-> data
|
||||
(assoc :key key)
|
||||
(assoc :type "paragraph")
|
||||
|
||||
@ -756,6 +756,9 @@
|
||||
(ctl/flex-layout? shape)
|
||||
(ctl/update-flex-scale value)
|
||||
|
||||
(ctl/grid-layout? shape)
|
||||
(ctl/update-grid-scale value)
|
||||
|
||||
:always
|
||||
(ctl/update-flex-child value)))]
|
||||
|
||||
|
||||
@ -601,6 +601,16 @@
|
||||
(d/update-in-when [:layout-padding :p3] * scale)
|
||||
(d/update-in-when [:layout-padding :p4] * scale)))
|
||||
|
||||
(defn update-grid-scale
|
||||
[shape scale]
|
||||
(letfn [(scale-track [track]
|
||||
(cond-> track
|
||||
(= (:type track) :fixed)
|
||||
(update :value * scale)))]
|
||||
(-> shape
|
||||
(update :layout-grid-columns #(mapv scale-track %))
|
||||
(update :layout-grid-rows #(mapv scale-track %)))))
|
||||
|
||||
(defn update-flex-child
|
||||
[shape scale]
|
||||
(-> shape
|
||||
|
||||
@ -400,9 +400,21 @@
|
||||
:parent-id parent-id
|
||||
:frame-id frame-id)
|
||||
|
||||
:always
|
||||
;; Store in the meta the old id so we can do a remap afterwards
|
||||
;; in the parent
|
||||
(with-meta {::old-id (:id shape)})
|
||||
|
||||
(some? (:shapes shape))
|
||||
(assoc :shapes (mapv :id new-direct-children)))
|
||||
|
||||
;; For a GRID layout remap the cells shapes' old-id to the new id given in the clone
|
||||
new-shape
|
||||
(if (ctl/grid-layout? new-shape)
|
||||
(let [ids-map (into {} (map #(vector (-> % meta ::old-id) (:id %))) new-children)]
|
||||
(ctl/remap-grid-cells new-shape ids-map))
|
||||
new-shape)
|
||||
|
||||
new-shape (update-new-shape new-shape shape)
|
||||
new-shapes (into [new-shape] new-children)
|
||||
|
||||
|
||||
@ -29,7 +29,8 @@
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--color-accent-primary-muted);
|
||||
background: var(--text-editor-selection-background-color);
|
||||
color: var(--text-editor-selection-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
--da-secondary: #bb97d8;
|
||||
--da-tertiary: #00d1b8;
|
||||
--da-tertiary-10: #{color.change(#00d1b8, $alpha: 0.1)};
|
||||
--da-tertiary-70: #{color.change(#00d1b8, $alpha: 0.7)};
|
||||
--da-quaternary: #ff6fe0;
|
||||
|
||||
// LIGHT
|
||||
@ -50,6 +51,7 @@
|
||||
--la-secondary: #1345aa;
|
||||
--la-tertiary: #8c33eb;
|
||||
--la-tertiary-10: #{color.change(#8c33eb, $alpha: 0.1)};
|
||||
--la-tertiary-70: #{color.change(#8c33eb, $alpha: 0.7)};
|
||||
--la-quaternary: #ff6fe0;
|
||||
|
||||
// STATUS COLOR
|
||||
|
||||
@ -357,6 +357,14 @@
|
||||
--viewer-thumbnails-control-foreground-color: var(--color-foreground-secondary);
|
||||
--viewer-thumbnail-border-color: var(--color-accent-primary);
|
||||
--viewer-thumbnail-background-color-selected: var(--color-accent-primary-muted);
|
||||
|
||||
// TEXT SELECTION
|
||||
--text-editor-selection-background-color: var(--da-tertiary-70);
|
||||
--text-editor-selection-foreground-color: var(--app-white);
|
||||
|
||||
// NEW TEAM BUTTON
|
||||
// TODO: we should not put these functional tokens here, but rather in the components they belong to
|
||||
--new-team-button-background-color: var(--color-background-primary);
|
||||
}
|
||||
|
||||
#app {
|
||||
@ -383,4 +391,6 @@
|
||||
|
||||
--assets-item-name-background-color: var(--color-background-primary);
|
||||
--assets-item-name-foreground-color: var(--color-foreground-primary);
|
||||
|
||||
--text-editor-selection-background-color: var(--la-tertiary-70);
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.persistence :as dwp]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
@ -166,6 +167,13 @@
|
||||
:wait true}]
|
||||
(rx/concat
|
||||
(rx/of ::dwp/force-persist)
|
||||
|
||||
;; Wait the persist to be succesfull
|
||||
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
||||
(rx/filter #(or (nil? %) (= :saved %)))
|
||||
(rx/first)
|
||||
(rx/timeout 400 (rx/empty)))
|
||||
|
||||
(->> (rp/cmd! :export params)
|
||||
(rx/mapcat (fn [{:keys [id filename]}]
|
||||
(->> (rp/cmd! :export {:cmd :get-resource :blob? true :id id})
|
||||
|
||||
@ -1013,20 +1013,21 @@
|
||||
(d/ordered-set)))]
|
||||
(rx/of (dws/select-shapes shapes-to-select)))
|
||||
|
||||
(let [{:keys [id type shapes]} (get objects (first selected))]
|
||||
(case type
|
||||
:text
|
||||
(rx/of (dwe/start-edition-mode id))
|
||||
(when (d/not-empty? selected)
|
||||
(let [{:keys [id type shapes]} (get objects (first selected))]
|
||||
(case type
|
||||
:text
|
||||
(rx/of (dwe/start-edition-mode id))
|
||||
|
||||
(:group :bool :frame)
|
||||
(let [shapes-ids (into (d/ordered-set) shapes)]
|
||||
(rx/of (dws/select-shapes shapes-ids)))
|
||||
(:group :bool :frame)
|
||||
(let [shapes-ids (into (d/ordered-set) shapes)]
|
||||
(rx/of (dws/select-shapes shapes-ids)))
|
||||
|
||||
:svg-raw
|
||||
nil
|
||||
:svg-raw
|
||||
nil
|
||||
|
||||
(rx/of (dwe/start-edition-mode id)
|
||||
(dwdp/start-path-edit id)))))))))
|
||||
(rx/of (dwe/start-edition-mode id)
|
||||
(dwdp/start-path-edit id))))))))))
|
||||
|
||||
(defn select-parent-layer
|
||||
[]
|
||||
|
||||
@ -81,7 +81,8 @@
|
||||
:page-id page-id
|
||||
:rect (:selrect shape)
|
||||
:include-frames? true
|
||||
:full-frame? true})
|
||||
:full-frame? true
|
||||
:using-selrect? true})
|
||||
(rx/map #(cfh/clean-loops objects %))
|
||||
(rx/map #(dwsh/move-shapes-into-frame (:id shape) %)))
|
||||
(rx/of (dwu/commit-undo-transaction (:id shape))))
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
[app.main.data.workspace.specialized-panel :as dwsp]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.thumbnails :as dwt]
|
||||
[app.main.data.workspace.transforms :as dwtr]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.features :as features]
|
||||
[app.main.features.pointer-map :as fpmap]
|
||||
@ -534,34 +535,38 @@
|
||||
(defn instantiate-component
|
||||
"Create a new shape in the current page, from the component with the given id
|
||||
in the given file library. Then selects the newly created instance."
|
||||
[file-id component-id position]
|
||||
(dm/assert! (uuid? file-id))
|
||||
(dm/assert! (uuid? component-id))
|
||||
(dm/assert! (gpt/point? position))
|
||||
(ptk/reify ::instantiate-component
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page (wsh/lookup-page state)
|
||||
libraries (wsh/get-libraries state)
|
||||
([file-id component-id position]
|
||||
(instantiate-component file-id component-id position nil))
|
||||
([file-id component-id position {:keys [start-move? initial-point]}]
|
||||
(dm/assert! (uuid? file-id))
|
||||
(dm/assert! (uuid? component-id))
|
||||
(dm/assert! (gpt/point? position))
|
||||
(ptk/reify ::instantiate-component
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page (wsh/lookup-page state)
|
||||
libraries (wsh/get-libraries state)
|
||||
|
||||
objects (:objects page)
|
||||
changes (-> (pcb/empty-changes it (:id page))
|
||||
(pcb/with-objects objects))
|
||||
objects (:objects page)
|
||||
changes (-> (pcb/empty-changes it (:id page))
|
||||
(pcb/with-objects objects))
|
||||
|
||||
[new-shape changes]
|
||||
(dwlh/generate-instantiate-component changes
|
||||
objects
|
||||
file-id
|
||||
component-id
|
||||
position
|
||||
page
|
||||
libraries)
|
||||
undo-id (js/Symbol)]
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(ptk/data-event :layout/update [(:id new-shape)])
|
||||
(dws/select-shapes (d/ordered-set (:id new-shape)))
|
||||
(dwu/commit-undo-transaction undo-id))))))
|
||||
[new-shape changes]
|
||||
(dwlh/generate-instantiate-component changes
|
||||
objects
|
||||
file-id
|
||||
component-id
|
||||
position
|
||||
page
|
||||
libraries)
|
||||
undo-id (js/Symbol)]
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(ptk/data-event :layout/update [(:id new-shape)])
|
||||
(dws/select-shapes (d/ordered-set (:id new-shape)))
|
||||
(when start-move?
|
||||
(dwtr/start-move initial-point #{(:id new-shape)}))
|
||||
(dwu/commit-undo-transaction undo-id)))))))
|
||||
|
||||
(defn detach-component
|
||||
"Remove all references to components in the shape with the given id,
|
||||
|
||||
@ -159,7 +159,9 @@
|
||||
component
|
||||
(:data library)
|
||||
position
|
||||
components-v2)
|
||||
components-v2
|
||||
;; The position can generate a frame calculation inside the base component so we force the frame-id
|
||||
{:force-frame-id frame-id})
|
||||
|
||||
first-shape (cond-> (first new-shapes)
|
||||
(not (nil? parent-id))
|
||||
|
||||
@ -83,6 +83,7 @@
|
||||
;; position changes.
|
||||
(->> stream
|
||||
(rx/filter mse/pointer-event?)
|
||||
(rx/filter #(= :viewport (mse/get-pointer-source %)))
|
||||
(rx/pipe (rxs/throttle 100))
|
||||
(rx/map #(handle-pointer-send file-id (:pt %)))))
|
||||
|
||||
|
||||
@ -496,7 +496,7 @@
|
||||
(when-let [node (dom/get-element-by-class "ghost-outline")]
|
||||
(dom/set-property! node "transform" (gmt/translate-matrix move-vector))))))
|
||||
|
||||
(defn- start-move
|
||||
(defn start-move
|
||||
([from-position] (start-move from-position nil))
|
||||
([from-position ids]
|
||||
(ptk/reify ::start-move
|
||||
|
||||
@ -591,3 +591,6 @@
|
||||
|
||||
(def updating-library
|
||||
(l/derived :updating-library st/state))
|
||||
|
||||
(def persistence-state
|
||||
(l/derived (comp :status :workspace-persistence) st/state))
|
||||
|
||||
@ -57,6 +57,7 @@
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
text-align: left;
|
||||
border: 1px solid transparent;
|
||||
.icon-btn {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
on-clear (unchecked-get props "clear-action")
|
||||
placeholder (unchecked-get props "placeholder")
|
||||
icon (unchecked-get props "icon")
|
||||
autofocus (unchecked-get props "auto-focus")
|
||||
|
||||
handle-change
|
||||
(mf/use-fn
|
||||
@ -52,6 +53,7 @@
|
||||
icon
|
||||
[:input {:on-change handle-change
|
||||
:value value
|
||||
:auto-focus autofocus
|
||||
:placeholder placeholder
|
||||
:on-key-down handle-key-down}]
|
||||
(when (not= "" value)
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
background-color: var(--input-background-color);
|
||||
font-size: $fs-12;
|
||||
color: var(--input-foreground-color);
|
||||
border-radius: $br-8;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@ -12,15 +12,21 @@
|
||||
(:require
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def pin-icon (icon-xref :pin-refactor (stl/css :icon)))
|
||||
(def ^:private pin-icon
|
||||
(icon-xref :pin-refactor (stl/css :icon)))
|
||||
|
||||
(mf/defc pin-button*
|
||||
{::mf/props :obj}
|
||||
[{:keys [aria-label is-pinned class] :as props}]
|
||||
(let [aria-label (or aria-label (tr "dashboard.pin-unpin"))
|
||||
class (dm/str (or class "") " " (stl/css-case :button true :button-active is-pinned))
|
||||
props (mf/spread-props props {:class class
|
||||
:aria-label aria-label})]
|
||||
[:> "button" props pin-icon]))
|
||||
|
||||
props (-> (obj/clone props)
|
||||
(obj/unset! "isPinned")
|
||||
(obj/set! "className" class)
|
||||
(obj/set! "aria-label" aria-label))]
|
||||
|
||||
[:> "button" props pin-icon]))
|
||||
|
||||
@ -232,13 +232,10 @@
|
||||
width: $s-168;
|
||||
}
|
||||
|
||||
.new-team {
|
||||
background-color: $db-quaternary;
|
||||
}
|
||||
|
||||
&.action {
|
||||
.team-icon {
|
||||
background-color: #2e3434;
|
||||
background-color: var(--new-team-button-background-color);
|
||||
border-radius: 50%;
|
||||
height: $s-24;
|
||||
margin-right: $s-12;
|
||||
|
||||
@ -121,9 +121,9 @@
|
||||
pill-height (/ fcc/flex-display-pill-height zoom)
|
||||
workspace-modifiers (mf/deref refs/workspace-modifiers)
|
||||
gap-selected (mf/deref refs/workspace-gap-selected)
|
||||
hover (mf/use-var nil)
|
||||
hover-value (mf/use-var 0)
|
||||
mouse-pos (mf/use-var nil)
|
||||
hover (mf/use-state nil)
|
||||
hover-value (mf/use-state 0)
|
||||
mouse-pos (mf/use-state nil)
|
||||
padding (:layout-padding frame)
|
||||
gap (:layout-gap frame)
|
||||
{:keys [width height x1 y1]} (:selrect frame)
|
||||
|
||||
@ -89,9 +89,9 @@
|
||||
pill-width (/ fcc/flex-display-pill-width zoom)
|
||||
pill-height (/ fcc/flex-display-pill-height zoom)
|
||||
margins-selected (mf/deref refs/workspace-margins-selected)
|
||||
hover-value (mf/use-var 0)
|
||||
mouse-pos (mf/use-var nil)
|
||||
hover (mf/use-var nil)
|
||||
hover-value (mf/use-state 0)
|
||||
mouse-pos (mf/use-state nil)
|
||||
hover (mf/use-state nil)
|
||||
hover-all? (and (not (nil? @hover)) alt?)
|
||||
hover-v? (and (or (= @hover :m1) (= @hover :m3)) shift?)
|
||||
hover-h? (and (or (= @hover :m2) (= @hover :m4)) shift?)
|
||||
|
||||
@ -77,7 +77,6 @@
|
||||
:y (:y rect-data)
|
||||
:width (max 0 (:width rect-data))
|
||||
:height (max 0 (:height rect-data))
|
||||
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-pointer-move on-pointer-move
|
||||
@ -115,9 +114,9 @@
|
||||
[{:keys [frame zoom alt? shift? on-move-selected on-context-menu]}]
|
||||
(let [frame-id (:id frame)
|
||||
paddings-selected (mf/deref refs/workspace-paddings-selected)
|
||||
hover-value (mf/use-var 0)
|
||||
mouse-pos (mf/use-var nil)
|
||||
hover (mf/use-var nil)
|
||||
hover-value (mf/use-state 0)
|
||||
mouse-pos (mf/use-state nil)
|
||||
hover (mf/use-state nil)
|
||||
hover-all? (and (not (nil? @hover)) alt?)
|
||||
hover-v? (and (or (= @hover :p1) (= @hover :p3)) shift?)
|
||||
hover-h? (and (or (= @hover :p2) (= @hover :p4)) shift?)
|
||||
|
||||
@ -50,13 +50,6 @@
|
||||
(fn []
|
||||
(st/emit! (dsc/pop-shortcuts key))))))
|
||||
|
||||
(defn invisible-image
|
||||
[]
|
||||
(let [img (js/Image.)
|
||||
imd "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="]
|
||||
(set! (.-src img) imd)
|
||||
img))
|
||||
|
||||
(defn- set-timer
|
||||
[state ms func]
|
||||
(assoc state :timer (ts/schedule ms func)))
|
||||
@ -128,7 +121,7 @@
|
||||
(do
|
||||
(dom/stop-propagation event)
|
||||
(dnd/set-data! event data-type data)
|
||||
(dnd/set-drag-image! event (invisible-image))
|
||||
(dnd/set-drag-image! event (dnd/invisible-image))
|
||||
(dnd/set-allowed-effect! event "move")
|
||||
(when (fn? on-drag)
|
||||
(on-drag data)))))
|
||||
|
||||
@ -8,7 +8,10 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.text :as gst]
|
||||
[app.config :as cf]
|
||||
[app.main.ui.shapes.attrs :as attrs]
|
||||
[app.main.ui.shapes.gradients :as grad]
|
||||
@ -28,7 +31,12 @@
|
||||
fills (get shape :fills [])
|
||||
|
||||
selrect (dm/get-prop shape :selrect)
|
||||
|
||||
bounds (when (cfh/text-shape? shape)
|
||||
(gst/shape->rect shape))
|
||||
|
||||
metadata (get shape :metadata)
|
||||
|
||||
x (dm/get-prop selrect :x)
|
||||
y (dm/get-prop selrect :y)
|
||||
width (dm/get-prop selrect :width)
|
||||
@ -62,32 +70,50 @@
|
||||
(obj/set! pat-props "patternTransform" transform)
|
||||
pat-props)]
|
||||
|
||||
(for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))]
|
||||
[:* {:key (dm/str shape-index)}
|
||||
(for [[fill-index value] (reverse (d/enumerate (get shape :fills [])))]
|
||||
(for [[obj-index obj] (d/enumerate (or (:position-data shape) [shape]))]
|
||||
[:* {:key (dm/str obj-index)}
|
||||
(for [[fill-index value] (reverse (d/enumerate (get obj :fills [])))]
|
||||
(when (some? (:fill-color-gradient value))
|
||||
(let [gradient (:fill-color-gradient value)
|
||||
|
||||
from-p (-> (gpt/point (+ x (* width (:start-x gradient)))
|
||||
(+ y (* height (:start-y gradient)))))
|
||||
to-p (-> (gpt/point (+ x (* width (:end-x gradient)))
|
||||
(+ y (* height (:end-y gradient)))))
|
||||
|
||||
gradient
|
||||
(cond-> gradient
|
||||
(some? bounds)
|
||||
(assoc
|
||||
:start-x (/ (- (:x from-p) (:x bounds)) (:width bounds))
|
||||
:start-y (/ (- (:y from-p) (:y bounds)) (:height bounds))
|
||||
:end-x (/ (- (:x to-p) (:x bounds)) (:width bounds))
|
||||
:end-y (/ (- (:y to-p) (:y bounds)) (:height bounds))))
|
||||
|
||||
props #js {:id (dm/str "fill-color-gradient-" render-id "-" fill-index)
|
||||
:key (dm/str fill-index)
|
||||
:gradient gradient
|
||||
:shape shape}]
|
||||
(case (:type gradient)
|
||||
:linear [:> grad/linear-gradient props]
|
||||
:radial [:> grad/radial-gradient props]))))
|
||||
:shape obj}]
|
||||
(case (d/name (:type gradient))
|
||||
"linear" [:> grad/linear-gradient props]
|
||||
"radial" [:> grad/radial-gradient props]))))
|
||||
|
||||
|
||||
(let [fill-id (dm/str "fill-" shape-index "-" render-id)]
|
||||
(let [fill-id (dm/str "fill-" obj-index "-" render-id)]
|
||||
[:> :pattern (-> (obj/clone pat-props)
|
||||
(obj/set! "id" fill-id)
|
||||
(cond-> has-image?
|
||||
(cond-> (and has-image? (nil? bounds))
|
||||
(-> (obj/set! "width" (* width no-repeat-padding))
|
||||
(obj/set! "height" (* height no-repeat-padding)))))
|
||||
(obj/set! "height" (* height no-repeat-padding))))
|
||||
(cond-> (some? bounds)
|
||||
(-> (obj/set! "width" (:width bounds))
|
||||
(obj/set! "height" (:height bounds)))))
|
||||
[:g
|
||||
(for [[fill-index value] (reverse (d/enumerate (get shape :fills [])))]
|
||||
(for [[fill-index value] (reverse (d/enumerate (get obj :fills [])))]
|
||||
(let [style (attrs/get-fill-style value fill-index render-id type)
|
||||
props #js {:key (dm/str fill-index)
|
||||
:width width
|
||||
:height height
|
||||
:width (d/nilv (:width bounds) width)
|
||||
:height (d/nilv (:height bounds) height)
|
||||
:style style}]
|
||||
(if (:fill-image value)
|
||||
(let [uri (cf/resolve-file-media (:fill-image value))
|
||||
|
||||
@ -104,7 +104,7 @@
|
||||
svg-root? (and (map? content) (= tag :svg))
|
||||
svg-tag? (map? content)
|
||||
svg-leaf? (string? content)
|
||||
valid-tag? (contains? csvg/svg-tags-list tag)]
|
||||
valid-tag? (contains? csvg/svg-tags tag)]
|
||||
|
||||
(cond
|
||||
^boolean svg-root?
|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
:textTransform text-transform
|
||||
:color (if (and show-text? (not gradient?)) text-color "transparent")
|
||||
:background (when (and show-text? gradient?) text-color)
|
||||
:caretColor (or text-color "black")
|
||||
:caretColor (if (and (not gradient?) text-color) text-color "black")
|
||||
:overflowWrap "initial"
|
||||
:lineBreak "auto"
|
||||
:whiteSpace "break-spaces"
|
||||
|
||||
@ -79,11 +79,9 @@
|
||||
[:span {:class (stl/css-case :color-value-wrapper true
|
||||
:gradient-name (:gradient color))}
|
||||
(if (:gradient color)
|
||||
[:& cbn/color-name {:color color
|
||||
:size 80}]
|
||||
[:& cbn/color-name {:color color :size 80}]
|
||||
(case format
|
||||
:hex [:& cbn/color-name {:color color
|
||||
:size 80}]
|
||||
:hex [:& cbn/color-name {:color color}]
|
||||
:rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))]
|
||||
[:* (str/fmt "%s, %s, %s, %s" r g b a)])
|
||||
:hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color))
|
||||
@ -105,8 +103,7 @@
|
||||
[:span {:class (stl/css-case :color-value-wrapper true
|
||||
:gradient-name (:gradient color))}
|
||||
(if (:gradient color)
|
||||
[:& cbn/color-name {:color color
|
||||
:size 80}]
|
||||
[:& cbn/color-name {:color color}]
|
||||
(case format
|
||||
:hex [:& cbn/color-name {:color color
|
||||
:size 80}]
|
||||
|
||||
@ -58,6 +58,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.name-opacity {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.color-name-wrapper {
|
||||
@include titleTipography;
|
||||
@include flexColumn;
|
||||
@ -113,8 +118,11 @@
|
||||
.color-value-wrapper {
|
||||
@include inspectValue;
|
||||
text-transform: uppercase;
|
||||
max-width: $s-124;
|
||||
max-width: $s-80;
|
||||
padding-right: $s-8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
&.gradient-name {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.components.title-bar :refer [title-bar]]
|
||||
[app.util.code-gen.style-css :as css]
|
||||
@ -19,9 +20,10 @@
|
||||
(mf/defc geometry-block
|
||||
[{:keys [objects shape]}]
|
||||
[:*
|
||||
(for [property properties]
|
||||
(for [[idx property] (d/enumerate properties)]
|
||||
(when-let [value (css/get-css-value objects shape property)]
|
||||
[:div {:class (stl/css :geometry-row)}
|
||||
[:div {:key (dm/str "block-" idx "-" (d/name property))
|
||||
:class (stl/css :geometry-row)}
|
||||
[:div {:class (stl/css :global/attr-label)} (d/name property)]
|
||||
[:div {:class (stl/css :global/attr-value)}
|
||||
[:& copy-button {:data (css/get-css-property objects shape property)}
|
||||
|
||||
@ -30,7 +30,6 @@
|
||||
}
|
||||
|
||||
.attributes-content-row {
|
||||
width: $s-252;
|
||||
max-width: $s-252;
|
||||
min-height: calc($s-2 + $s-32);
|
||||
border-radius: $br-8;
|
||||
@ -39,7 +38,7 @@
|
||||
.content {
|
||||
@include titleTipography;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
padding: $s-4 0;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
|
||||
@ -8,727 +8,28 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.exports :as de]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.shortcuts :as scd]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.colors :as dc]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.hooks.resize :as r]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.main-menu :as main-menu]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.router :as rt]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- Header menu and submenus
|
||||
|
||||
(mf/defc help-info-menu
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [layout on-close]}]
|
||||
(let [nav-to-helpc-center
|
||||
(mf/use-fn #(dom/open-new-window "https://help.penpot.app"))
|
||||
|
||||
nav-to-community
|
||||
(mf/use-fn #(dom/open-new-window "https://community.penpot.app"))
|
||||
|
||||
nav-to-youtube
|
||||
(mf/use-fn #(dom/open-new-window "https://www.youtube.com/c/Penpot"))
|
||||
|
||||
nav-to-templates
|
||||
(mf/use-fn #(dom/open-new-window "https://penpot.app/libraries-templates"))
|
||||
|
||||
nav-to-github
|
||||
(mf/use-fn #(dom/open-new-window "https://github.com/penpot/penpot"))
|
||||
|
||||
nav-to-terms
|
||||
(mf/use-fn #(dom/open-new-window "https://penpot.app/terms"))
|
||||
|
||||
nav-to-feedback
|
||||
(mf/use-fn #(st/emit! (rt/nav-new-window* {:rname :settings-feedback})))
|
||||
|
||||
show-shortcuts
|
||||
(mf/use-fn
|
||||
(mf/deps layout)
|
||||
(fn []
|
||||
(when (contains? layout :collapse-left-sidebar)
|
||||
(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
|
||||
|
||||
(st/emit!
|
||||
(-> (dw/toggle-layout-flag :shortcuts)
|
||||
(vary-meta assoc ::ev/origin "workspace-header")))))
|
||||
|
||||
show-release-notes
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [version (:main cf/version)]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
|
||||
(if (and (kbd/alt? event) (kbd/mod? event))
|
||||
(st/emit! (modal/show {:type :onboarding}))
|
||||
(st/emit! (modal/show {:type :release-notes :version version}))))))]
|
||||
|
||||
[:& dropdown-menu {:show true
|
||||
:on-close on-close
|
||||
:list-class (stl/css-case :sub-menu true
|
||||
:help-info true)}
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-helpc-center
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-helpc-center event)))
|
||||
:id "file-menu-help-center"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.help-center")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-community
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-community event)))
|
||||
:id "file-menu-community"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.community")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-youtube
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-youtube event)))
|
||||
:id "file-menu-youtube"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.tutorials")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click show-release-notes
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(show-release-notes event)))
|
||||
:id "file-menu-release-notes"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.release-notes")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-templates
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-templates event)))
|
||||
:id "file-menu-templates"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.libraries-and-templates")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-github
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-github event)))
|
||||
:id "file-menu-github"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.github-repo")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-terms
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-terms event)))
|
||||
:id "file-menu-terms"}
|
||||
[:span {:class (stl/css :item-name)} (tr "auth.terms-of-service")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click show-shortcuts
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(show-shortcuts event)))
|
||||
:id "file-menu-shortcuts"}
|
||||
[:span {:class (stl/css :item-name)} (tr "label.shortcuts")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :show-shortcuts))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
(when (contains? cf/flags :user-feedback)
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-feedback
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-feedback event)))
|
||||
:id "file-menu-feedback"}
|
||||
[:span {:class (stl/css-case :feedback true
|
||||
:item-name true)} (tr "labels.give-feedback")]])]))
|
||||
|
||||
(mf/defc preferences-menu
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [layout toggle-flag on-close]}]
|
||||
(let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))]
|
||||
|
||||
[:& dropdown-menu {:show true
|
||||
:list-class (stl/css-case :sub-menu true
|
||||
:preferences true)
|
||||
:on-close on-close}
|
||||
[:> dropdown-menu-item* {:on-click toggle-flag
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "scale-text"
|
||||
:id "file-menu-scale-text"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :scale-text)
|
||||
(tr "workspace.header.menu.disable-scale-content")
|
||||
(tr "workspace.header.menu.enable-scale-content"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-scale-text))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click toggle-flag
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "snap-guides"
|
||||
:id "file-menu-snap-guides"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :snap-guides)
|
||||
(tr "workspace.header.menu.disable-snap-guides")
|
||||
(tr "workspace.header.menu.enable-snap-guides"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-guide))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click toggle-flag
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "snap-grid"
|
||||
:id "file-menu-snap-grid"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :snap-grid)
|
||||
(tr "workspace.header.menu.disable-snap-grid")
|
||||
(tr "workspace.header.menu.enable-snap-grid"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-grid))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click toggle-flag
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "dynamic-alignment"
|
||||
:id "file-menu-dynamic-alignment"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :dynamic-alignment)
|
||||
(tr "workspace.header.menu.disable-dynamic-alignment")
|
||||
(tr "workspace.header.menu.enable-dynamic-alignment"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-alignment))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click toggle-flag
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "snap-pixel-grid"
|
||||
:id "file-menu-pixel-grid"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :snap-pixel-grid)
|
||||
(tr "workspace.header.menu.disable-snap-pixel-grid")
|
||||
(tr "workspace.header.menu.enable-snap-pixel-grid"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :snap-pixel-grid))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click show-nudge-options
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(show-nudge-options event)))
|
||||
:data-test "snap-pixel-grid"
|
||||
:id "file-menu-nudge"}
|
||||
[:span {:class (stl/css :item-name)} (tr "modals.nudge-title")]]]))
|
||||
|
||||
(mf/defc view-menu
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [layout toggle-flag on-close]}]
|
||||
(let [read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
|
||||
toggle-color-palette
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(r/set-resize-type! :bottom)
|
||||
(st/emit! (dw/remove-layout-flag :textpalette)
|
||||
(-> (dw/toggle-layout-flag :colorpalette)
|
||||
(vary-meta assoc ::ev/origin "workspace-menu")))))
|
||||
|
||||
toggle-text-palette
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(r/set-resize-type! :bottom)
|
||||
(st/emit! (dw/remove-layout-flag :colorpalette)
|
||||
(-> (dw/toggle-layout-flag :textpalette)
|
||||
(vary-meta assoc ::ev/origin "workspace-menu")))))]
|
||||
|
||||
[:& dropdown-menu {:show true
|
||||
:list-class (stl/css-case :sub-menu true
|
||||
:view true)
|
||||
:on-close on-close}
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-flag
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "rules"
|
||||
:id "file-menu-rules"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :rules)
|
||||
(tr "workspace.header.menu.hide-rules")
|
||||
(tr "workspace.header.menu.show-rules"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-rules))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-flag
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "display-grid"
|
||||
:id "file-menu-grid"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :display-grid)
|
||||
(tr "workspace.header.menu.hide-grid")
|
||||
(tr "workspace.header.menu.show-grid"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-grid))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
|
||||
(when-not ^boolean read-only?
|
||||
[:*
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-color-palette
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-color-palette event)))
|
||||
:id "file-menu-color-palette"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :colorpalette)
|
||||
(tr "workspace.header.menu.hide-palette")
|
||||
(tr "workspace.header.menu.show-palette"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-colorpalette))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-text-palette
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-text-palette event)))
|
||||
:id "file-menu-text-palette"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :textpalette)
|
||||
(tr "workspace.header.menu.hide-textpalette")
|
||||
(tr "workspace.header.menu.show-textpalette"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-textpalette))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]])
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-flag
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "display-artboard-names"
|
||||
:id "file-menu-artboards"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :display-artboard-names)
|
||||
(tr "workspace.header.menu.hide-artboard-names")
|
||||
(tr "workspace.header.menu.show-artboard-names"))]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-flag
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "show-pixel-grid"
|
||||
:id "file-menu-pixel-grid"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :show-pixel-grid)
|
||||
(tr "workspace.header.menu.hide-pixel-grid")
|
||||
(tr "workspace.header.menu.show-pixel-grid"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :show-pixel-grid))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-flag
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "hide-ui"
|
||||
:id "file-menu-hide-ui"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "workspace.shape.menu.hide-ui")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :hide-ui))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]]))
|
||||
|
||||
(mf/defc edit-menu
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [on-close]}]
|
||||
(let [select-all (mf/use-fn #(st/emit! (dw/select-all)))
|
||||
undo (mf/use-fn #(st/emit! dwc/undo))
|
||||
redo (mf/use-fn #(st/emit! dwc/redo))]
|
||||
[:& dropdown-menu {:show true
|
||||
:list-class (stl/css-case :sub-menu true
|
||||
:edit true)
|
||||
:on-close on-close}
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click select-all
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(select-all event)))
|
||||
:id "file-menu-select-all"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "workspace.header.menu.select-all")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :select-all))]
|
||||
[:span {:class (stl/css :shortcut-key)
|
||||
:key sc}
|
||||
sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click undo
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(undo event)))
|
||||
:id "file-menu-undo"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.undo")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :undo))]
|
||||
[:span {:class (stl/css :shortcut-key)
|
||||
:key sc}
|
||||
sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click redo
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(redo event)))
|
||||
:id "file-menu-redo"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.redo")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :redo))]
|
||||
[:span {:class (stl/css :shortcut-key)
|
||||
:key sc}
|
||||
sc])]]]))
|
||||
|
||||
(mf/defc file-menu
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [on-close file]}]
|
||||
(let [file-id (:id file)
|
||||
shared? (:is-shared file)
|
||||
|
||||
objects (mf/deref refs/workspace-page-objects)
|
||||
frames (->> (cfh/get-immediate-children objects uuid/zero)
|
||||
(filterv cfh/frame-shape?))
|
||||
|
||||
on-remove-shared
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(modal/show!
|
||||
{:type :delete-shared-libraries
|
||||
:origin :unpublish
|
||||
:ids #{file-id}
|
||||
:on-accept #(st/emit! (dwl/set-file-shared file-id false))
|
||||
:count-libraries 1})))
|
||||
|
||||
on-remove-shared-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-remove-shared)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-remove-shared event))))
|
||||
|
||||
on-add-shared
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [_event]
|
||||
(let [on-accept #(st/emit! (dwl/set-file-shared file-id true))]
|
||||
(st/emit! (dcm/show-shared-dialog file-id on-accept)))))
|
||||
|
||||
on-add-shared-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-add-shared)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-add-shared event))))
|
||||
|
||||
on-export-shapes
|
||||
(mf/use-fn #(st/emit! (de/show-workspace-export-dialog)))
|
||||
|
||||
on-export-shapes-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-export-shapes)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-export-shapes event))))
|
||||
|
||||
on-export-file
|
||||
(mf/use-fn
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(let [target (dom/get-current-target event)
|
||||
binary? (= (dom/get-data target "binary") "true")
|
||||
evname (if binary?
|
||||
"export-binary-files"
|
||||
"export-standard-files")]
|
||||
(st/emit!
|
||||
(ptk/event ::ev/event {::ev/name evname
|
||||
::ev/origin "workspace"
|
||||
:num-files 1})
|
||||
(dcm/export-files [file] binary?)))))
|
||||
|
||||
on-export-file-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-export-file)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-export-file event))))
|
||||
|
||||
on-export-frames
|
||||
(mf/use-fn
|
||||
(mf/deps frames)
|
||||
(fn [_]
|
||||
(st/emit! (de/show-workspace-export-frames-dialog (reverse frames)))))
|
||||
|
||||
on-export-frames-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-export-frames)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-export-frames event))))]
|
||||
|
||||
[:& dropdown-menu {:show true
|
||||
:list-class (stl/css-case :sub-menu true
|
||||
:file true)
|
||||
:on-close on-close}
|
||||
|
||||
(if ^boolean shared?
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-remove-shared
|
||||
:on-key-down on-remove-shared-key-down
|
||||
:id "file-menu-remove-shared"}
|
||||
[:span {:class (stl/css :item-name)} (tr "dashboard.unpublish-shared")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-add-shared
|
||||
:on-key-down on-add-shared-key-down
|
||||
:id "file-menu-add-shared"}
|
||||
[:span {:class (stl/css :item-name)} (tr "dashboard.add-shared")]])
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-export-shapes
|
||||
:on-key-down on-export-shapes-key-down
|
||||
:id "file-menu-export-shapes"}
|
||||
[:span {:class (stl/css :item-name)} (tr "dashboard.export-shapes")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :export-shapes))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-export-file
|
||||
:on-key-down on-export-file-key-down
|
||||
:data-binary true
|
||||
:id "file-menu-binary-file"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.download-binary-file")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-export-file
|
||||
:on-key-down on-export-file-key-down
|
||||
:data-binary false
|
||||
:id "file-menu-standard-file"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.download-standard-file")]]
|
||||
|
||||
(when (seq frames)
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-export-frames
|
||||
:on-key-down on-export-frames-key-down
|
||||
:id "file-menu-export-frames"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.export-frames")]])]))
|
||||
|
||||
(mf/defc menu
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [layout file]}]
|
||||
(let [show-menu* (mf/use-state false)
|
||||
show-menu? (deref show-menu*)
|
||||
sub-menu* (mf/use-state false)
|
||||
sub-menu (deref sub-menu*)
|
||||
|
||||
open-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show-menu* true)))
|
||||
|
||||
close-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show-menu* false)))
|
||||
|
||||
close-sub-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! sub-menu* nil)))
|
||||
|
||||
on-menu-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [menu (-> (dom/get-current-target event)
|
||||
(dom/get-data "test")
|
||||
(keyword))]
|
||||
(reset! sub-menu* menu))))
|
||||
|
||||
toggle-flag
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [flag (-> (dom/get-current-target event)
|
||||
(dom/get-data "test")
|
||||
(keyword))]
|
||||
(st/emit!
|
||||
(-> (dw/toggle-layout-flag flag)
|
||||
(vary-meta assoc ::ev/origin "workspace-menu")))
|
||||
(reset! show-menu* false)
|
||||
(reset! sub-menu* nil))))]
|
||||
|
||||
|
||||
[:*
|
||||
[:div {:on-click open-menu
|
||||
:class (stl/css :menu-btn)} i/menu-refactor]
|
||||
|
||||
[:& dropdown-menu {:show show-menu?
|
||||
:on-close close-menu
|
||||
:list-class (stl/css :menu)}
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-item)
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:data-test "file"
|
||||
:id "file-menu-file"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.file")]
|
||||
[:span {:class (stl/css :open-arrow)} i/arrow-refactor]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-item)
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:data-test "edit"
|
||||
:id "file-menu-edit"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.edit")]
|
||||
[:span {:class (stl/css :open-arrow)} i/arrow-refactor]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-item)
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:data-test "view"
|
||||
:id "file-menu-view"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.view")]
|
||||
[:span {:class (stl/css :open-arrow)} i/arrow-refactor]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-item)
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:data-test "preferences"
|
||||
:id "file-menu-preferences"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.preferences")]
|
||||
[:span {:class (stl/css :open-arrow)} i/arrow-refactor]]
|
||||
[:div {:class (stl/css :separator)}]
|
||||
[:> dropdown-menu-item* {:class (stl/css-case :menu-item true)
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:data-test "help-info"
|
||||
:id "file-menu-help-info"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.help-info")]
|
||||
[:span {:class (stl/css :open-arrow)} i/arrow-refactor]]]
|
||||
|
||||
(case sub-menu
|
||||
:file
|
||||
[:& file-menu
|
||||
{:file file
|
||||
:on-close close-sub-menu}]
|
||||
|
||||
:edit
|
||||
[:& edit-menu
|
||||
{:on-close close-sub-menu}]
|
||||
|
||||
:view
|
||||
[:& view-menu
|
||||
{:layout layout
|
||||
:toggle-flag toggle-flag
|
||||
:on-close close-sub-menu}]
|
||||
|
||||
:preferences
|
||||
[:& preferences-menu
|
||||
{:layout layout
|
||||
:toggle-flag toggle-flag
|
||||
:on-close close-sub-menu}]
|
||||
|
||||
:help-info
|
||||
[:& help-info-menu
|
||||
{:layout layout
|
||||
:on-close close-sub-menu}]
|
||||
|
||||
nil)]))
|
||||
|
||||
;; --- Header Component
|
||||
|
||||
(mf/defc left-header
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [file layout project page-id class]}]
|
||||
(let [file-id (:id file)
|
||||
(let [profile (mf/deref refs/profile)
|
||||
file-id (:id file)
|
||||
file-name (:name file)
|
||||
project-id (:id project)
|
||||
team-id (:team-id project)
|
||||
@ -809,9 +110,10 @@
|
||||
(when ^boolean shared?
|
||||
[:span {:class (stl/css :shared-badge)} i/library-refactor])
|
||||
[:div {:class (stl/css :menu-section)}
|
||||
[:& menu {:layout layout
|
||||
:file file
|
||||
:read-only? read-only?
|
||||
:team-id team-id
|
||||
:page-id page-id}]]]))
|
||||
|
||||
[:& main-menu/menu
|
||||
{:layout layout
|
||||
:file file
|
||||
:profile profile
|
||||
:read-only? read-only?
|
||||
:team-id team-id
|
||||
:page-id page-id}]]]))
|
||||
|
||||
@ -77,97 +77,3 @@
|
||||
width: $s-16;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-btn {
|
||||
@extend .button-tertiary;
|
||||
height: $s-32;
|
||||
width: calc($s-24 + $s-4);
|
||||
padding: 0;
|
||||
border-radius: $br-8;
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
@extend .menu-dropdown;
|
||||
top: $s-48;
|
||||
left: calc(var(--width, $s-256) - $s-16);
|
||||
width: $s-192;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
@extend .menu-item-base;
|
||||
cursor: pointer;
|
||||
.open-arrow {
|
||||
@include flexCenter;
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
.open-arrow {
|
||||
svg {
|
||||
stroke: var(--menu-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
.shortcut-key {
|
||||
color: var(--menu-shortcut-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-top: $s-8;
|
||||
height: $s-4;
|
||||
border-top: $s-1 solid $db-secondary;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
@extend .shortcut-base;
|
||||
}
|
||||
.shortcut-key {
|
||||
@extend .shortcut-key-base;
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
@extend .menu-dropdown;
|
||||
left: calc(var(--width, $s-256) + $s-180);
|
||||
width: $s-192;
|
||||
min-width: calc($s-272 - $s-2);
|
||||
width: 110%;
|
||||
|
||||
.submenu-item {
|
||||
@extend .menu-item-base;
|
||||
&:hover {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
.shortcut-key {
|
||||
color: var(--menu-shortcut-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.file {
|
||||
top: $s-48;
|
||||
}
|
||||
|
||||
&.edit {
|
||||
top: $s-76;
|
||||
}
|
||||
|
||||
&.view {
|
||||
top: $s-116;
|
||||
}
|
||||
|
||||
&.preferences {
|
||||
top: $s-148;
|
||||
}
|
||||
|
||||
&.help-info {
|
||||
top: $s-196;
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,11 +123,12 @@
|
||||
|
||||
shared-libraries
|
||||
(mf/with-memo [shared-libraries linked-libraries file-id search-term]
|
||||
(->> shared-libraries
|
||||
(remove #(= (:id %) file-id))
|
||||
(remove #(contains? linked-libraries (:id %)))
|
||||
(filter #(matches-search (:name %) search-term))
|
||||
(sort-by (comp str/lower :name))))
|
||||
(when shared-libraries
|
||||
(->> shared-libraries
|
||||
(remove #(= (:id %) file-id))
|
||||
(remove #(contains? linked-libraries (:id %)))
|
||||
(filter #(matches-search (:name %) search-term))
|
||||
(sort-by (comp str/lower :name)))))
|
||||
|
||||
linked-libraries
|
||||
(mf/with-memo [linked-libraries]
|
||||
@ -275,12 +276,17 @@
|
||||
:on-click link-library}
|
||||
i/add-refactor]])]
|
||||
|
||||
[:div {:class (stl/css :section-list-empty)}
|
||||
(if (nil? shared-libraries)
|
||||
i/loader-pencil
|
||||
(if (str/empty? search-term)
|
||||
(when (empty? shared-libraries)
|
||||
[:div {:class (stl/css :section-list-empty)}
|
||||
(cond
|
||||
(nil? shared-libraries)
|
||||
(tr "workspace.libraries.loading")
|
||||
|
||||
(str/empty? search-term)
|
||||
(tr "workspace.libraries.no-shared-libraries-available")
|
||||
(tr "workspace.libraries.no-matches-for" search-term)))])]]))
|
||||
|
||||
:else
|
||||
(tr "workspace.libraries.no-matches-for" search-term))]))]]))
|
||||
|
||||
(defn- extract-assets
|
||||
[file-data library summary?]
|
||||
@ -519,4 +525,3 @@
|
||||
[:& updates-tab {:file-id file-id
|
||||
:file-data file-data
|
||||
:libraries libraries}]]]]]]]]))
|
||||
|
||||
|
||||
747
frontend/src/app/main/ui/workspace/main_menu.cljs
Normal file
747
frontend/src/app/main/ui/workspace/main_menu.cljs
Normal file
@ -0,0 +1,747 @@
|
||||
;; 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.main.ui.workspace.main-menu
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.exports :as de]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.shortcuts :as scd]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.hooks.resize :as r]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.router :as rt]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- Header menu and submenus
|
||||
|
||||
(mf/defc help-info-menu
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [layout on-close]}]
|
||||
(let [nav-to-helpc-center
|
||||
(mf/use-fn #(dom/open-new-window "https://help.penpot.app"))
|
||||
|
||||
nav-to-community
|
||||
(mf/use-fn #(dom/open-new-window "https://community.penpot.app"))
|
||||
|
||||
nav-to-youtube
|
||||
(mf/use-fn #(dom/open-new-window "https://www.youtube.com/c/Penpot"))
|
||||
|
||||
nav-to-templates
|
||||
(mf/use-fn #(dom/open-new-window "https://penpot.app/libraries-templates"))
|
||||
|
||||
nav-to-github
|
||||
(mf/use-fn #(dom/open-new-window "https://github.com/penpot/penpot"))
|
||||
|
||||
nav-to-terms
|
||||
(mf/use-fn #(dom/open-new-window "https://penpot.app/terms"))
|
||||
|
||||
nav-to-feedback
|
||||
(mf/use-fn #(st/emit! (rt/nav-new-window* {:rname :settings-feedback})))
|
||||
|
||||
show-shortcuts
|
||||
(mf/use-fn
|
||||
(mf/deps layout)
|
||||
(fn []
|
||||
(when (contains? layout :collapse-left-sidebar)
|
||||
(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
|
||||
|
||||
(st/emit!
|
||||
(-> (dw/toggle-layout-flag :shortcuts)
|
||||
(vary-meta assoc ::ev/origin "workspace-header")))))
|
||||
|
||||
show-release-notes
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [version (:main cf/version)]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
|
||||
(if (and (kbd/alt? event) (kbd/mod? event))
|
||||
(st/emit! (modal/show {:type :onboarding}))
|
||||
(st/emit! (modal/show {:type :release-notes :version version}))))))]
|
||||
|
||||
[:& dropdown-menu {:show true
|
||||
:on-close on-close
|
||||
:list-class (stl/css-case :sub-menu true
|
||||
:help-info true)}
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-helpc-center
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-helpc-center event)))
|
||||
:id "file-menu-help-center"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.help-center")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-community
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-community event)))
|
||||
:id "file-menu-community"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.community")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-youtube
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-youtube event)))
|
||||
:id "file-menu-youtube"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.tutorials")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click show-release-notes
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(show-release-notes event)))
|
||||
:id "file-menu-release-notes"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.release-notes")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-templates
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-templates event)))
|
||||
:id "file-menu-templates"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.libraries-and-templates")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-github
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-github event)))
|
||||
:id "file-menu-github"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.github-repo")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-terms
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-terms event)))
|
||||
:id "file-menu-terms"}
|
||||
[:span {:class (stl/css :item-name)} (tr "auth.terms-of-service")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click show-shortcuts
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(show-shortcuts event)))
|
||||
:id "file-menu-shortcuts"}
|
||||
[:span {:class (stl/css :item-name)} (tr "label.shortcuts")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :show-shortcuts))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
(when (contains? cf/flags :user-feedback)
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click nav-to-feedback
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(nav-to-feedback event)))
|
||||
:id "file-menu-feedback"}
|
||||
[:span {:class (stl/css-case :feedback true
|
||||
:item-name true)} (tr "labels.give-feedback")]])]))
|
||||
|
||||
(mf/defc preferences-menu
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [layout profile toggle-flag on-close toggle-theme]}]
|
||||
(let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))]
|
||||
|
||||
[:& dropdown-menu {:show true
|
||||
:list-class (stl/css-case :sub-menu true
|
||||
:preferences true)
|
||||
:on-close on-close}
|
||||
[:> dropdown-menu-item* {:on-click toggle-flag
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "scale-text"
|
||||
:id "file-menu-scale-text"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :scale-text)
|
||||
(tr "workspace.header.menu.disable-scale-content")
|
||||
(tr "workspace.header.menu.enable-scale-content"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-scale-text))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click toggle-flag
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "snap-guides"
|
||||
:id "file-menu-snap-guides"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :snap-guides)
|
||||
(tr "workspace.header.menu.disable-snap-guides")
|
||||
(tr "workspace.header.menu.enable-snap-guides"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-guide))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click toggle-flag
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "snap-grid"
|
||||
:id "file-menu-snap-grid"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :snap-grid)
|
||||
(tr "workspace.header.menu.disable-snap-grid")
|
||||
(tr "workspace.header.menu.enable-snap-grid"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-grid))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click toggle-flag
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "dynamic-alignment"
|
||||
:id "file-menu-dynamic-alignment"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :dynamic-alignment)
|
||||
(tr "workspace.header.menu.disable-dynamic-alignment")
|
||||
(tr "workspace.header.menu.enable-dynamic-alignment"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-alignment))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click toggle-flag
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "snap-pixel-grid"
|
||||
:id "file-menu-pixel-grid"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :snap-pixel-grid)
|
||||
(tr "workspace.header.menu.disable-snap-pixel-grid")
|
||||
(tr "workspace.header.menu.enable-snap-pixel-grid"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :snap-pixel-grid))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click show-nudge-options
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(show-nudge-options event)))
|
||||
:data-test "snap-pixel-grid"
|
||||
:id "file-menu-nudge"}
|
||||
[:span {:class (stl/css :item-name)} (tr "modals.nudge-title")]]
|
||||
|
||||
|
||||
[:> dropdown-menu-item* {:on-click toggle-theme
|
||||
:class (stl/css :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-theme event)))
|
||||
:data-test "toggle-theme"
|
||||
:id "file-menu-toggle-theme"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (= (:theme profile) "default")
|
||||
(tr "workspace.header.menu.toggle-light-theme")
|
||||
(tr "workspace.header.menu.toggle-dark-theme"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-theme))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]]))
|
||||
|
||||
(mf/defc view-menu
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [layout toggle-flag on-close]}]
|
||||
(let [read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
|
||||
toggle-color-palette
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(r/set-resize-type! :bottom)
|
||||
(st/emit! (dw/remove-layout-flag :textpalette)
|
||||
(-> (dw/toggle-layout-flag :colorpalette)
|
||||
(vary-meta assoc ::ev/origin "workspace-menu")))))
|
||||
|
||||
toggle-text-palette
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(r/set-resize-type! :bottom)
|
||||
(st/emit! (dw/remove-layout-flag :colorpalette)
|
||||
(-> (dw/toggle-layout-flag :textpalette)
|
||||
(vary-meta assoc ::ev/origin "workspace-menu")))))]
|
||||
|
||||
[:& dropdown-menu {:show true
|
||||
:list-class (stl/css-case :sub-menu true
|
||||
:view true)
|
||||
:on-close on-close}
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-flag
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "rules"
|
||||
:id "file-menu-rules"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :rules)
|
||||
(tr "workspace.header.menu.hide-rules")
|
||||
(tr "workspace.header.menu.show-rules"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-rules))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-flag
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "display-grid"
|
||||
:id "file-menu-grid"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :display-grid)
|
||||
(tr "workspace.header.menu.hide-grid")
|
||||
(tr "workspace.header.menu.show-grid"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-grid))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
|
||||
(when-not ^boolean read-only?
|
||||
[:*
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-color-palette
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-color-palette event)))
|
||||
:id "file-menu-color-palette"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :colorpalette)
|
||||
(tr "workspace.header.menu.hide-palette")
|
||||
(tr "workspace.header.menu.show-palette"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-colorpalette))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-text-palette
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-text-palette event)))
|
||||
:id "file-menu-text-palette"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :textpalette)
|
||||
(tr "workspace.header.menu.hide-textpalette")
|
||||
(tr "workspace.header.menu.show-textpalette"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :toggle-textpalette))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]])
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-flag
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "display-artboard-names"
|
||||
:id "file-menu-artboards"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :display-artboard-names)
|
||||
(tr "workspace.header.menu.hide-artboard-names")
|
||||
(tr "workspace.header.menu.show-artboard-names"))]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-flag
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "show-pixel-grid"
|
||||
:id "file-menu-pixel-grid"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if (contains? layout :show-pixel-grid)
|
||||
(tr "workspace.header.menu.hide-pixel-grid")
|
||||
(tr "workspace.header.menu.show-pixel-grid"))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :show-pixel-grid))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click toggle-flag
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(toggle-flag event)))
|
||||
:data-test "hide-ui"
|
||||
:id "file-menu-hide-ui"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "workspace.shape.menu.hide-ui")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :hide-ui))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]]))
|
||||
|
||||
(mf/defc edit-menu
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [on-close]}]
|
||||
(let [select-all (mf/use-fn #(st/emit! (dw/select-all)))
|
||||
undo (mf/use-fn #(st/emit! dwc/undo))
|
||||
redo (mf/use-fn #(st/emit! dwc/redo))]
|
||||
[:& dropdown-menu {:show true
|
||||
:list-class (stl/css-case :sub-menu true
|
||||
:edit true)
|
||||
:on-close on-close}
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click select-all
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(select-all event)))
|
||||
:id "file-menu-select-all"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "workspace.header.menu.select-all")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :select-all))]
|
||||
[:span {:class (stl/css :shortcut-key)
|
||||
:key sc}
|
||||
sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click undo
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(undo event)))
|
||||
:id "file-menu-undo"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.undo")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :undo))]
|
||||
[:span {:class (stl/css :shortcut-key)
|
||||
:key sc}
|
||||
sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click redo
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(redo event)))
|
||||
:id "file-menu-redo"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.redo")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :redo))]
|
||||
[:span {:class (stl/css :shortcut-key)
|
||||
:key sc}
|
||||
sc])]]]))
|
||||
|
||||
(mf/defc file-menu
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [on-close file]}]
|
||||
(let [file-id (:id file)
|
||||
shared? (:is-shared file)
|
||||
|
||||
objects (mf/deref refs/workspace-page-objects)
|
||||
frames (->> (cfh/get-immediate-children objects uuid/zero)
|
||||
(filterv cfh/frame-shape?))
|
||||
|
||||
on-remove-shared
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(modal/show!
|
||||
{:type :delete-shared-libraries
|
||||
:origin :unpublish
|
||||
:ids #{file-id}
|
||||
:on-accept #(st/emit! (dwl/set-file-shared file-id false))
|
||||
:count-libraries 1})))
|
||||
|
||||
on-remove-shared-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-remove-shared)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-remove-shared event))))
|
||||
|
||||
on-add-shared
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [_event]
|
||||
(let [on-accept #(st/emit! (dwl/set-file-shared file-id true))]
|
||||
(st/emit! (dcm/show-shared-dialog file-id on-accept)))))
|
||||
|
||||
on-add-shared-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-add-shared)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-add-shared event))))
|
||||
|
||||
on-export-shapes
|
||||
(mf/use-fn #(st/emit! (de/show-workspace-export-dialog)))
|
||||
|
||||
on-export-shapes-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-export-shapes)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-export-shapes event))))
|
||||
|
||||
on-export-file
|
||||
(mf/use-fn
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(let [target (dom/get-current-target event)
|
||||
binary? (= (dom/get-data target "binary") "true")
|
||||
evname (if binary?
|
||||
"export-binary-files"
|
||||
"export-standard-files")]
|
||||
(st/emit!
|
||||
(ptk/event ::ev/event {::ev/name evname
|
||||
::ev/origin "workspace"
|
||||
:num-files 1})
|
||||
(dcm/export-files [file] binary?)))))
|
||||
|
||||
on-export-file-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-export-file)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-export-file event))))
|
||||
|
||||
on-export-frames
|
||||
(mf/use-fn
|
||||
(mf/deps frames)
|
||||
(fn [_]
|
||||
(st/emit! (de/show-workspace-export-frames-dialog (reverse frames)))))
|
||||
|
||||
on-export-frames-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-export-frames)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-export-frames event))))]
|
||||
|
||||
[:& dropdown-menu {:show true
|
||||
:list-class (stl/css-case :sub-menu true
|
||||
:file true)
|
||||
:on-close on-close}
|
||||
|
||||
(if ^boolean shared?
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-remove-shared
|
||||
:on-key-down on-remove-shared-key-down
|
||||
:id "file-menu-remove-shared"}
|
||||
[:span {:class (stl/css :item-name)} (tr "dashboard.unpublish-shared")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-add-shared
|
||||
:on-key-down on-add-shared-key-down
|
||||
:id "file-menu-add-shared"}
|
||||
[:span {:class (stl/css :item-name)} (tr "dashboard.add-shared")]])
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-export-shapes
|
||||
:on-key-down on-export-shapes-key-down
|
||||
:id "file-menu-export-shapes"}
|
||||
[:span {:class (stl/css :item-name)} (tr "dashboard.export-shapes")]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :export-shapes))]
|
||||
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-export-file
|
||||
:on-key-down on-export-file-key-down
|
||||
:data-binary true
|
||||
:id "file-menu-binary-file"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.download-binary-file")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-export-file
|
||||
:on-key-down on-export-file-key-down
|
||||
:data-binary false
|
||||
:id "file-menu-standard-file"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.download-standard-file")]]
|
||||
|
||||
(when (seq frames)
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-export-frames
|
||||
:on-key-down on-export-frames-key-down
|
||||
:id "file-menu-export-frames"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.export-frames")]])]))
|
||||
|
||||
(mf/defc menu
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [layout file profile]}]
|
||||
(let [show-menu* (mf/use-state false)
|
||||
show-menu? (deref show-menu*)
|
||||
sub-menu* (mf/use-state false)
|
||||
sub-menu (deref sub-menu*)
|
||||
|
||||
open-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show-menu* true)))
|
||||
|
||||
close-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show-menu* false)))
|
||||
|
||||
close-sub-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! sub-menu* nil)))
|
||||
|
||||
on-menu-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [menu (-> (dom/get-current-target event)
|
||||
(dom/get-data "test")
|
||||
(keyword))]
|
||||
(reset! sub-menu* menu))))
|
||||
|
||||
toggle-flag
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [flag (-> (dom/get-current-target event)
|
||||
(dom/get-data "test")
|
||||
(keyword))]
|
||||
(st/emit!
|
||||
(-> (dw/toggle-layout-flag flag)
|
||||
(vary-meta assoc ::ev/origin "workspace-menu")))
|
||||
(reset! show-menu* false)
|
||||
(reset! sub-menu* nil))))
|
||||
|
||||
|
||||
toggle-theme
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (du/toggle-theme))))]
|
||||
|
||||
|
||||
[:*
|
||||
[:div {:on-click open-menu
|
||||
:class (stl/css :menu-btn)} i/menu-refactor]
|
||||
|
||||
[:& dropdown-menu {:show show-menu?
|
||||
:on-close close-menu
|
||||
:list-class (stl/css :menu)}
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-item)
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:data-test "file"
|
||||
:id "file-menu-file"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.file")]
|
||||
[:span {:class (stl/css :open-arrow)} i/arrow-refactor]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-item)
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:data-test "edit"
|
||||
:id "file-menu-edit"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.edit")]
|
||||
[:span {:class (stl/css :open-arrow)} i/arrow-refactor]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-item)
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:data-test "view"
|
||||
:id "file-menu-view"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.view")]
|
||||
[:span {:class (stl/css :open-arrow)} i/arrow-refactor]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-item)
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:data-test "preferences"
|
||||
:id "file-menu-preferences"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.preferences")]
|
||||
[:span {:class (stl/css :open-arrow)} i/arrow-refactor]]
|
||||
[:div {:class (stl/css :separator)}]
|
||||
[:> dropdown-menu-item* {:class (stl/css-case :menu-item true)
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:data-test "help-info"
|
||||
:id "file-menu-help-info"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.help-info")]
|
||||
[:span {:class (stl/css :open-arrow)} i/arrow-refactor]]]
|
||||
|
||||
(case sub-menu
|
||||
:file
|
||||
[:& file-menu
|
||||
{:file file
|
||||
:on-close close-sub-menu}]
|
||||
|
||||
:edit
|
||||
[:& edit-menu
|
||||
{:on-close close-sub-menu}]
|
||||
|
||||
:view
|
||||
[:& view-menu
|
||||
{:layout layout
|
||||
:toggle-flag toggle-flag
|
||||
:on-close close-sub-menu}]
|
||||
|
||||
:preferences
|
||||
[:& preferences-menu
|
||||
{:layout layout
|
||||
:profile profile
|
||||
:toggle-flag toggle-flag
|
||||
:toggle-theme toggle-theme
|
||||
:on-close close-sub-menu}]
|
||||
|
||||
:help-info
|
||||
[:& help-info-menu
|
||||
{:layout layout
|
||||
:on-close close-sub-menu}]
|
||||
|
||||
nil)]))
|
||||
101
frontend/src/app/main/ui/workspace/main_menu.scss
Normal file
101
frontend/src/app/main/ui/workspace/main_menu.scss
Normal file
@ -0,0 +1,101 @@
|
||||
// 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
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.menu-btn {
|
||||
@extend .button-tertiary;
|
||||
height: $s-32;
|
||||
width: calc($s-24 + $s-4);
|
||||
padding: 0;
|
||||
border-radius: $br-8;
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
@extend .menu-dropdown;
|
||||
top: $s-48;
|
||||
left: calc(var(--width, $s-256) - $s-16);
|
||||
width: $s-192;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
@extend .menu-item-base;
|
||||
cursor: pointer;
|
||||
.open-arrow {
|
||||
@include flexCenter;
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
.open-arrow {
|
||||
svg {
|
||||
stroke: var(--menu-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
.shortcut-key {
|
||||
color: var(--menu-shortcut-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-top: $s-8;
|
||||
height: $s-4;
|
||||
border-top: $s-1 solid $db-secondary;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
@extend .shortcut-base;
|
||||
}
|
||||
.shortcut-key {
|
||||
@extend .shortcut-key-base;
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
@extend .menu-dropdown;
|
||||
left: calc(var(--width, $s-256) + $s-180);
|
||||
width: $s-192;
|
||||
min-width: calc($s-272 - $s-2);
|
||||
width: 110%;
|
||||
|
||||
.submenu-item {
|
||||
@extend .menu-item-base;
|
||||
&:hover {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
.shortcut-key {
|
||||
color: var(--menu-shortcut-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.file {
|
||||
top: $s-48;
|
||||
}
|
||||
|
||||
&.edit {
|
||||
top: $s-76;
|
||||
}
|
||||
|
||||
&.view {
|
||||
top: $s-116;
|
||||
}
|
||||
|
||||
&.preferences {
|
||||
top: $s-148;
|
||||
}
|
||||
|
||||
&.help-info {
|
||||
top: $s-196;
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@
|
||||
(ns app.main.ui.workspace.shapes.text.editor
|
||||
(:require
|
||||
["draft-js" :as draft]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
@ -48,7 +49,7 @@
|
||||
(let [children (obj/get props "children")]
|
||||
[:span {:style {:background "#ccc" :display "inline-block"}} children]))
|
||||
|
||||
(defn render-block
|
||||
(defn- render-block
|
||||
[block shape]
|
||||
(let [type (ted/get-editor-block-type block)]
|
||||
(case type
|
||||
@ -59,7 +60,7 @@
|
||||
:shape shape}}
|
||||
nil)))
|
||||
|
||||
(defn styles-fn [shape styles content]
|
||||
(defn- styles-fn [shape styles content]
|
||||
(let [data (if (= (.getText content) "")
|
||||
(-> (.getData content)
|
||||
(.toJS)
|
||||
@ -73,19 +74,27 @@
|
||||
(def empty-editor-state
|
||||
(ted/create-editor-state nil default-decorator))
|
||||
|
||||
(defn get-blocks-to-setup [block-changes]
|
||||
(defn- get-blocks-to-setup [block-changes]
|
||||
(->> block-changes
|
||||
(filter (fn [[_ v]]
|
||||
(nil? (:old v))))
|
||||
(mapv first)))
|
||||
|
||||
(defn get-blocks-to-add-styles
|
||||
(defn- get-blocks-to-add-styles
|
||||
[block-changes]
|
||||
(->> block-changes
|
||||
(filter (fn [[_ v]]
|
||||
(and (not= (:old v) (:new v)) (= (:old v) ""))))
|
||||
(mapv first)))
|
||||
|
||||
(defn- shape->justify
|
||||
[{:keys [content]}]
|
||||
(case (d/nilv (:vertical-align content) "top")
|
||||
"center" "center"
|
||||
"top" "flex-start"
|
||||
"bottom" "flex-end"
|
||||
nil))
|
||||
|
||||
(mf/defc text-shape-edit-html
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/wrap-props false
|
||||
@ -247,7 +256,8 @@
|
||||
:custom-style-fn (partial styles-fn shape)
|
||||
:block-renderer-fn #(render-block % shape)
|
||||
:ref on-editor
|
||||
:editor-state state}]]))
|
||||
:editor-state state
|
||||
:style #js {:border "1px solid red"}}]]))
|
||||
|
||||
(defn translate-point-from-viewport
|
||||
"Translate a point in the viewport into client coordinates"
|
||||
@ -303,7 +313,19 @@
|
||||
(dm/get-prop shape :height))
|
||||
|
||||
style
|
||||
(cond-> #js {:pointer-events "all"}
|
||||
(cond-> #js {:pointerEvents "all"}
|
||||
|
||||
(not (cf/check-browser? :safari))
|
||||
(obj/merge!
|
||||
#js {:transform (dm/fmt "translate(%px, %px)" (- (dm/get-prop shape :x) x) (- (dm/get-prop shape :y) y))})
|
||||
|
||||
(cf/check-browser? :safari-17)
|
||||
(obj/merge!
|
||||
#js {:height "100%"
|
||||
:display "flex"
|
||||
:flexDirection "column"
|
||||
:justifyContent (shape->justify shape)})
|
||||
|
||||
(cf/check-browser? :safari-16)
|
||||
(obj/merge!
|
||||
#js {:position "fixed"
|
||||
|
||||
@ -472,13 +472,26 @@
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [component event]
|
||||
;; dnd api only allow to acces to the dataTransfer data on on-drop (https://html.spec.whatwg.org/dev/dnd.html#concept-dnd-p)
|
||||
;; We need to know if the dragged element is from the local library on on-drag-enter, so we need to keep the info elsewhere
|
||||
(set-drag-data! {:local? local?})
|
||||
|
||||
(dnd/set-data! event "penpot/component" {:file-id file-id
|
||||
:component component})
|
||||
(dnd/set-allowed-effect! event "move")))
|
||||
(let [file-data
|
||||
(d/nilv (dm/get-in @refs/workspace-libraries [file-id :data]) @refs/workspace-data)
|
||||
|
||||
shape-main
|
||||
(ctf/get-component-root file-data component)]
|
||||
|
||||
;; dnd api only allow to acces to the dataTransfer data on on-drop (https://html.spec.whatwg.org/dev/dnd.html#concept-dnd-p)
|
||||
;; We need to know if the dragged element is from the local library on on-drag-enter, so we need to keep the info elsewhere
|
||||
(set-drag-data! {:file-id file-id
|
||||
:component component
|
||||
:shape shape-main
|
||||
:local? local?})
|
||||
|
||||
(dnd/set-data! event "penpot/component" true)
|
||||
|
||||
;; Remove the ghost image for componentes because we're going to instantiate it on the viewport
|
||||
(dnd/set-drag-image! event (dnd/invisible-image))
|
||||
|
||||
(dnd/set-allowed-effect! event "move"))))
|
||||
|
||||
on-show-main
|
||||
(mf/use-fn
|
||||
@ -569,4 +582,3 @@
|
||||
{:option-name (tr "workspace.shape.menu.show-main")
|
||||
:id "assets-show-main-component"
|
||||
:option-handler on-show-main})]}]]]))
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
}
|
||||
.asset-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax($s-112, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax($s-96, 1fr));
|
||||
grid-auto-rows: $s-112;
|
||||
max-width: 100%;
|
||||
gap: $s-4;
|
||||
|
||||
@ -61,7 +61,8 @@
|
||||
:transform :matrix-render
|
||||
:transform-inverse :matrix-render
|
||||
:selrect :rect-render
|
||||
:points :points-render})
|
||||
:points :points-render
|
||||
:layout-grid-cells :cells-render})
|
||||
|
||||
(mf/defc shape-link
|
||||
[{:keys [id objects]}]
|
||||
@ -69,6 +70,25 @@
|
||||
:on-click #(st/emit! (dw/select-shape id))}
|
||||
(dm/str (dm/get-in objects [id :name]) " #" id)])
|
||||
|
||||
(mf/defc cells-render
|
||||
[{:keys [cells objects]}]
|
||||
[:div {:class (stl/css :cells-render)}
|
||||
(for [[id cell] cells]
|
||||
|
||||
[:div {:key (dm/str "cell-" id)
|
||||
:class (stl/css :cell-container)}
|
||||
[:div {:class (stl/css :cell-position)}
|
||||
(dm/fmt "(%, %) -> (%, %)"
|
||||
(:row cell)
|
||||
(:column cell)
|
||||
(+ (:row cell) (dec (:row-span cell)))
|
||||
(+ (:column cell) (dec (:column-span cell))))]
|
||||
|
||||
[:div {:class (stl/css :cell-shape)}
|
||||
(if (empty? (:shapes cell))
|
||||
[:div "<empty>"]
|
||||
[:& shape-link {:id (first (:shapes cell)) :objects objects}])]])])
|
||||
|
||||
(mf/defc debug-shape-attr
|
||||
[{:keys [attr value objects]}]
|
||||
|
||||
@ -79,7 +99,8 @@
|
||||
:shape-list
|
||||
[:div {:class (stl/css :shape-list)}
|
||||
(for [id value]
|
||||
[:& shape-link {:id id :objects objects}])]
|
||||
[:& shape-link {:key (dm/str "child-" id)
|
||||
:id id :objects objects}])]
|
||||
|
||||
:matrix-render
|
||||
[:div (dm/str (gmt/format-precision value 2))]
|
||||
@ -89,8 +110,11 @@
|
||||
|
||||
:points-render
|
||||
[:div {:class (stl/css :point-list)}
|
||||
(for [point value]
|
||||
[:div (dm/fmt "(%, %)" (:x point) (:y point))])]
|
||||
(for [[idx point] (d/enumerate value)]
|
||||
[:div {:key (dm/str "point-" idx)} (dm/fmt "(%, %)" (:x point) (:y point))])]
|
||||
|
||||
:cells-render
|
||||
[:& cells-render {:cells value :objects objects}]
|
||||
|
||||
[:div {:class (stl/css :attrs-container-value)} (str value)]))
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--panel-background-color);
|
||||
color: white;
|
||||
color: $df-primary;
|
||||
font-size: $fs-12;
|
||||
user-select: text;
|
||||
}
|
||||
@ -97,3 +97,8 @@
|
||||
display: flex;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.cell-container {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 1fr;
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@
|
||||
(dom/focus! textarea))))
|
||||
on-delete-annotation
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(mf/deps (:id shape))
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (modal/show
|
||||
@ -98,7 +98,7 @@
|
||||
(dw/update-component-annotation component-id nil)))}))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps shape)
|
||||
(mf/deps (:id shape))
|
||||
(fn []
|
||||
(initialize)
|
||||
(when (and (not creating?) (:id-for-create workspace-annotations)) ;; cleanup set-annotations-id-for-create if we aren't on the marked component
|
||||
|
||||
@ -176,6 +176,7 @@
|
||||
[:div {:class (stl/css :header)}
|
||||
[:& search-bar {:on-change on-filter-change
|
||||
:value (:term @state)
|
||||
:auto-focus true
|
||||
:placeholder (tr "workspace.options.search-font")}]
|
||||
(when (and recent-fonts show-recent)
|
||||
[:section {:class (stl/css :show-recent)}
|
||||
|
||||
@ -247,14 +247,13 @@
|
||||
|
||||
.text-options {
|
||||
@include flexColumn;
|
||||
margin-bottom: $s-8;
|
||||
&:not(.text-options-full-size) {
|
||||
position: relative;
|
||||
}
|
||||
.font-option {
|
||||
@include titleTipography;
|
||||
@extend .asset-element;
|
||||
padding-right: 0;
|
||||
padding: $s-8 0 $s-8 $s-8;
|
||||
cursor: pointer;
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
@ -292,6 +291,7 @@
|
||||
padding: 0;
|
||||
.numeric-input {
|
||||
@extend .input-base;
|
||||
padding-inline-start: $s-8;
|
||||
}
|
||||
}
|
||||
|
||||
@ -375,39 +375,40 @@
|
||||
padding: $s-12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.font-wrapper {
|
||||
padding-bottom: $s-4;
|
||||
cursor: pointer;
|
||||
.font-item {
|
||||
@extend .asset-element;
|
||||
margin-bottom: $s-4;
|
||||
border-radius: $br-8;
|
||||
display: flex;
|
||||
.icon {
|
||||
@include flexCenter;
|
||||
height: $s-28;
|
||||
width: $s-28;
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
color: var(--assets-item-name-foreground-color-hover);
|
||||
.icon {
|
||||
svg {
|
||||
stroke: var(--assets-item-name-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
.font-wrapper {
|
||||
padding-bottom: $s-4;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label {
|
||||
@include titleTipography;
|
||||
flex-grow: 1;
|
||||
.font-item {
|
||||
@extend .asset-element;
|
||||
margin-bottom: $s-4;
|
||||
border-radius: $br-8;
|
||||
display: flex;
|
||||
.icon {
|
||||
@include flexCenter;
|
||||
height: $s-28;
|
||||
width: $s-28;
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
color: var(--assets-item-name-foreground-color-hover);
|
||||
.icon {
|
||||
svg {
|
||||
stroke: var(--assets-item-name-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
@include titleTipography;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.font-selector-dropdown-full-size {
|
||||
@ -418,7 +419,6 @@
|
||||
}
|
||||
|
||||
.fonts-list {
|
||||
@include menuShadow;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -439,4 +439,10 @@
|
||||
border-start-start-radius: 0;
|
||||
border-start-end-radius: 0;
|
||||
border: $s-1 solid var(--color-background-quaternary);
|
||||
|
||||
// TODO: this should belong to typography-entry , but atm we don't have a clear
|
||||
// way of accessing whether we are in fullsize mode or not
|
||||
.selected {
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,10 +56,6 @@
|
||||
:values measure-values
|
||||
:shape shape}]
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& layout-container-menu
|
||||
{:type type
|
||||
:ids [(:id shape)]
|
||||
@ -81,6 +77,10 @@
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& fill-menu {:ids ids
|
||||
:type type
|
||||
:values (select-keys shape fill-attrs)}]
|
||||
|
||||
@ -58,10 +58,6 @@
|
||||
:values measure-values
|
||||
:shape shape}]
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& layout-container-menu
|
||||
{:type type
|
||||
:ids [(:id shape)]
|
||||
@ -83,6 +79,10 @@
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& fill-menu {:ids ids
|
||||
:type type
|
||||
:values (select-keys shape fill-attrs)}]
|
||||
|
||||
@ -65,10 +65,6 @@
|
||||
|
||||
[:& component-menu {:shapes [shape]}]
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& layout-container-menu
|
||||
{:type type
|
||||
:ids [(:id shape)]
|
||||
@ -91,6 +87,9 @@
|
||||
:is-layout-container? is-layout-container?
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& fill-menu {:ids ids
|
||||
:type type
|
||||
|
||||
@ -72,9 +72,6 @@
|
||||
[:& measures-menu {:type type :ids measure-ids :values measure-values :shape shape}]
|
||||
[:& component-menu {:shapes [shape]}] ;;remove this in components-v2
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids constraint-ids :values constraint-values}])
|
||||
|
||||
[:& layout-container-menu
|
||||
{:type type
|
||||
:ids [(:id shape)]
|
||||
@ -96,6 +93,9 @@
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:values layout-item-values}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids constraint-ids :values constraint-values}])
|
||||
|
||||
(when-not (empty? fill-ids)
|
||||
[:& fill-menu {:type type :ids fill-ids :values fill-values}])
|
||||
|
||||
|
||||
@ -58,10 +58,6 @@
|
||||
:values measure-values
|
||||
:shape shape}]
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& layout-container-menu
|
||||
{:type type
|
||||
:ids [(:id shape)]
|
||||
@ -83,6 +79,10 @@
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& fill-menu {:ids ids
|
||||
:type type
|
||||
:values fill-values}]
|
||||
|
||||
@ -364,9 +364,6 @@
|
||||
(when-not (empty? components)
|
||||
[:& component-menu {:shapes components}])
|
||||
|
||||
(when-not (or (empty? constraint-ids) ^boolean is-layout-child?)
|
||||
[:& constraints-menu {:ids constraint-ids :values constraint-values}])
|
||||
|
||||
[:& layout-container-menu
|
||||
{:type type
|
||||
:ids layout-container-ids
|
||||
@ -383,6 +380,9 @@
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:values layout-item-values}])
|
||||
|
||||
(when-not (or (empty? constraint-ids) ^boolean is-layout-child?)
|
||||
[:& constraints-menu {:ids constraint-ids :values constraint-values}])
|
||||
|
||||
(when-not (empty? text-ids)
|
||||
[:& ot/text-menu {:type type :ids text-ids :values text-values}])
|
||||
|
||||
|
||||
@ -57,10 +57,6 @@
|
||||
:values measure-values
|
||||
:shape shape}]
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& layout-container-menu
|
||||
{:type type
|
||||
:ids [(:id shape)]
|
||||
@ -82,6 +78,10 @@
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& fill-menu {:ids ids
|
||||
:type type
|
||||
:values (select-keys shape fill-attrs)}]
|
||||
|
||||
@ -60,10 +60,6 @@
|
||||
:values measure-values
|
||||
:shape shape}]
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& layout-container-menu
|
||||
{:type type
|
||||
:ids ids
|
||||
@ -85,6 +81,10 @@
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& fill-menu {:ids ids
|
||||
:type type
|
||||
:values fill-values}]
|
||||
|
||||
@ -129,10 +129,6 @@
|
||||
:values measure-values
|
||||
:shape shape}]
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& layout-container-menu
|
||||
{:type type
|
||||
:ids [(:id shape)]
|
||||
@ -154,6 +150,10 @@
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}])
|
||||
|
||||
[:& fill-menu {:ids ids
|
||||
:type type
|
||||
:values fill-values}]
|
||||
|
||||
@ -93,11 +93,6 @@
|
||||
:values (select-keys shape measure-attrs)
|
||||
:shape shape}]
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu
|
||||
{:ids ids
|
||||
:values (select-keys shape constraint-attrs)}])
|
||||
|
||||
[:& layout-container-menu
|
||||
{:type type
|
||||
:ids [(:id shape)]
|
||||
@ -119,6 +114,11 @@
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
[:& constraints-menu
|
||||
{:ids ids
|
||||
:values (select-keys shape constraint-attrs)}])
|
||||
|
||||
[:& text-menu
|
||||
{:ids ids
|
||||
:type type
|
||||
|
||||
@ -174,9 +174,12 @@
|
||||
on-click (actions/on-click hover selected edition drawing-path? drawing-tool space? selrect z?)
|
||||
on-context-menu (actions/on-context-menu hover hover-ids workspace-read-only?)
|
||||
on-double-click (actions/on-double-click hover hover-ids hover-top-frame-id drawing-path? base-objects edition drawing-tool z? workspace-read-only?)
|
||||
on-drag-enter (actions/on-drag-enter)
|
||||
|
||||
comp-inst-ref (mf/use-ref false)
|
||||
on-drag-enter (actions/on-drag-enter comp-inst-ref)
|
||||
on-drag-over (actions/on-drag-over move-stream)
|
||||
on-drop (actions/on-drop file)
|
||||
on-drag-end (actions/on-drag-over comp-inst-ref)
|
||||
on-drop (actions/on-drop file comp-inst-ref)
|
||||
on-pointer-down (actions/on-pointer-down @hover selected edition drawing-tool text-editing? node-editing? grid-editing?
|
||||
drawing-path? create-comment? space? panning z? workspace-read-only?)
|
||||
|
||||
@ -365,6 +368,7 @@
|
||||
:on-double-click on-double-click
|
||||
:on-drag-enter on-drag-enter
|
||||
:on-drag-over on-drag-over
|
||||
:on-drag-end on-drag-end
|
||||
:on-drop on-drop
|
||||
:on-pointer-down on-pointer-down
|
||||
:on-pointer-enter on-pointer-enter
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
[app.main.data.workspace.specialized-panel :as-alias dwsp]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.workspace.sidebar.assets.components :as wsac]
|
||||
[app.main.ui.workspace.viewport.viewport-ref :as uwvv]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.dom.dnd :as dnd]
|
||||
@ -28,7 +29,8 @@
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.mouse :as mse]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as timers]
|
||||
[app.util.rxops :refer [throttle-fn]]
|
||||
[app.util.timers :as ts]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
@ -216,7 +218,7 @@
|
||||
(st/emit! (mse/->MouseEvent :double-click ctrl? shift? alt? meta?))
|
||||
|
||||
;; Emit asynchronously so the double click to exit shapes won't break
|
||||
(timers/schedule
|
||||
(ts/schedule
|
||||
(fn []
|
||||
(when (and (not drawing-path?) shape)
|
||||
(cond
|
||||
@ -244,7 +246,7 @@
|
||||
workspace-read-only?)
|
||||
(let [position (dom/get-client-position event)]
|
||||
;; Delayed callback because we need to wait to the previous context menu to be closed
|
||||
(timers/schedule
|
||||
(ts/schedule
|
||||
#(st/emit!
|
||||
(if (some? @hover)
|
||||
(dw/show-shape-context-menu {:position position
|
||||
@ -290,7 +292,7 @@
|
||||
|
||||
;; We store this so in Firefox the middle button won't do a paste of the content
|
||||
(reset! disable-paste true)
|
||||
(timers/schedule #(reset! disable-paste false)))
|
||||
(ts/schedule #(reset! disable-paste false)))
|
||||
|
||||
(st/emit! (dw/finish-panning)
|
||||
(dw/finish-zooming))))))
|
||||
@ -400,9 +402,28 @@
|
||||
(st/emit! (dw/update-viewport-position {:x #(+ % (/ delta-x zoom))
|
||||
:y #(+ % (/ delta-y zoom))}))))))))))
|
||||
|
||||
(defn on-drag-enter []
|
||||
(defn on-drag-enter
|
||||
[comp-inst-ref]
|
||||
(mf/use-callback
|
||||
(fn [e]
|
||||
(let [component-inst? (mf/ref-val comp-inst-ref)]
|
||||
(when (and (dnd/has-type? e "penpot/component")
|
||||
(dom/class? (dom/get-target e) "viewport-controls")
|
||||
(not component-inst?))
|
||||
(let [point (gpt/point (.-clientX e) (.-clientY e))
|
||||
viewport-coord (uwvv/point->viewport point)
|
||||
{:keys [component file-id shape]} @wsac/drag-data*
|
||||
|
||||
;; shape (get-in component [:objects (:id component)])
|
||||
final-x (- (:x viewport-coord) (/ (:width shape) 2))
|
||||
final-y (- (:y viewport-coord) (/ (:height shape) 2))]
|
||||
|
||||
(mf/set-ref-val! comp-inst-ref true)
|
||||
(st/emit! (dwl/instantiate-component
|
||||
file-id
|
||||
(:id component)
|
||||
(gpt/point final-x final-y)
|
||||
{:start-move? true :initial-point viewport-coord})))))
|
||||
(when (or (dnd/has-type? e "penpot/shape")
|
||||
(dnd/has-type? e "penpot/component")
|
||||
(dnd/has-type? e "Files")
|
||||
@ -410,8 +431,19 @@
|
||||
(dnd/has-type? e "text/asset-id"))
|
||||
(dom/prevent-default e)))))
|
||||
|
||||
(defn on-drag-end
|
||||
[comp-inst-ref]
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(mf/set-ref-val! comp-inst-ref false))))
|
||||
|
||||
(defn on-drag-over [move-stream]
|
||||
(let [on-pointer-move (on-pointer-move move-stream)]
|
||||
(let [on-pointer-move (on-pointer-move move-stream)
|
||||
|
||||
;; Drag-over is not the same as pointer-move. Drag over is fired less frequently so we need
|
||||
;; to create a throttle so the events that cannot be processed at a certain path are
|
||||
;; discarded.
|
||||
on-pointer-move (throttle-fn 50 (fn [e] (ts/raf #(on-pointer-move e))))]
|
||||
(mf/use-callback
|
||||
(fn [e]
|
||||
(when (or (dnd/has-type? e "penpot/shape")
|
||||
@ -423,7 +455,7 @@
|
||||
(dom/prevent-default e))))))
|
||||
|
||||
(defn on-drop
|
||||
[file]
|
||||
[file comp-inst-ref]
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
@ -443,13 +475,13 @@
|
||||
(assoc :y final-y)))))
|
||||
|
||||
(dnd/has-type? event "penpot/component")
|
||||
(let [{:keys [component file-id]} (dnd/get-data event "penpot/component")
|
||||
shape (get-in component [:objects (:id component)])
|
||||
final-x (- (:x viewport-coord) (/ (:width shape) 2))
|
||||
final-y (- (:y viewport-coord) (/ (:height shape) 2))]
|
||||
(st/emit! (dwl/instantiate-component file-id
|
||||
(:id component)
|
||||
(gpt/point final-x final-y))))
|
||||
(let [event (.-nativeEvent event)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)
|
||||
meta? (kbd/meta? event)]
|
||||
(st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?))
|
||||
(mf/set-ref-val! comp-inst-ref false))
|
||||
|
||||
;; Will trigger when the user drags an image from a browser
|
||||
;; to the viewport (firefox and chrome do it a bit different
|
||||
@ -517,4 +549,3 @@
|
||||
(not @disable-paste)
|
||||
(not workspace-read-only?))
|
||||
(st/emit! (dw/paste-from-event event @in-viewport?)))))))
|
||||
|
||||
|
||||
@ -118,7 +118,7 @@
|
||||
:on-pointer-up on-pointer-up}]])
|
||||
|
||||
(mf/defc gradient-handler-transformed
|
||||
[{:keys [from-p to-p width-p from-color to-color zoom editing transform
|
||||
[{:keys [from-p to-p width-p from-color to-color zoom editing
|
||||
on-change-start on-change-finish on-change-width]}]
|
||||
(let [moving-point (mf/use-var nil)
|
||||
angle (+ 90 (gpt/angle from-p to-p))
|
||||
@ -151,7 +151,7 @@
|
||||
(reset! moving-point nil))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps @moving-point from-p to-p width-p transform)
|
||||
(mf/deps @moving-point from-p to-p width-p)
|
||||
(fn []
|
||||
(let [subs (->> st/stream
|
||||
(rx/filter mse/pointer-event?)
|
||||
@ -159,18 +159,17 @@
|
||||
(rx/map mse/get-pointer-position)
|
||||
(rx/subs!
|
||||
(fn [pt]
|
||||
(let [pt (gpt/transform pt transform)]
|
||||
(case @moving-point
|
||||
:from-p (when on-change-start (on-change-start pt))
|
||||
:to-p (when on-change-finish (on-change-finish pt))
|
||||
:width-p (when on-change-width
|
||||
(let [width-v (gpt/unit (gpt/to-vec from-p width-p))
|
||||
distance (gpt/point-line-distance pt from-p to-p)
|
||||
new-width-p (gpt/add
|
||||
from-p
|
||||
(gpt/multiply width-v (gpt/point distance)))]
|
||||
(on-change-width new-width-p)))
|
||||
nil)))))]
|
||||
(case @moving-point
|
||||
:from-p (when on-change-start (on-change-start pt))
|
||||
:to-p (when on-change-finish (on-change-finish pt))
|
||||
:width-p (when on-change-width
|
||||
(let [width-v (gpt/unit (gpt/to-vec from-p width-p))
|
||||
distance (gpt/point-line-distance pt from-p to-p)
|
||||
new-width-p (gpt/add
|
||||
from-p
|
||||
(gpt/multiply width-v (gpt/point distance)))]
|
||||
(on-change-width new-width-p)))
|
||||
nil))))]
|
||||
(fn [] (rx/dispose! subs)))))
|
||||
[:g.gradient-handlers
|
||||
[:defs
|
||||
@ -296,7 +295,6 @@
|
||||
:width-p (when (= :radial (:type gradient)) width-p)
|
||||
:from-color {:value start-color :opacity start-opacity}
|
||||
:to-color {:value end-color :opacity end-opacity}
|
||||
:transform transform
|
||||
:zoom zoom
|
||||
:on-change-start on-change-start
|
||||
:on-change-finish on-change-finish
|
||||
|
||||
@ -62,6 +62,13 @@
|
||||
(.setData dt data-type data))
|
||||
e)))
|
||||
|
||||
(defn invisible-image
|
||||
[]
|
||||
(let [img (js/Image.)
|
||||
imd "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="]
|
||||
(set! (.-src img) imd)
|
||||
img))
|
||||
|
||||
(defn set-drag-image!
|
||||
([e image]
|
||||
(set-drag-image! e image 0 0))
|
||||
@ -108,11 +115,13 @@
|
||||
([e]
|
||||
(get-data e "penpot/data"))
|
||||
([e data-type]
|
||||
(let [dt (.-dataTransfer e)]
|
||||
(if (or (str/starts-with? data-type "penpot")
|
||||
(= data-type "application/json"))
|
||||
(t/decode-str (.getData dt data-type))
|
||||
(.getData dt data-type)))))
|
||||
(let [dt (.-dataTransfer e)
|
||||
data (.getData dt data-type)]
|
||||
(cond-> data
|
||||
(and (some? data) (not= data "")
|
||||
(or (str/starts-with? data-type "penpot")
|
||||
(= data-type "application/json")))
|
||||
(t/decode-str)))))
|
||||
|
||||
(defn get-files
|
||||
[e]
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
(:require
|
||||
[beicon.v2.core :as rx]))
|
||||
|
||||
(defn- throttle-fn
|
||||
(defn throttle-fn
|
||||
[delay f]
|
||||
(let [state
|
||||
#js {:lastExecTime 0
|
||||
|
||||
@ -3442,6 +3442,12 @@ msgstr "Show fonts palette"
|
||||
msgid "workspace.header.menu.undo"
|
||||
msgstr "Undo"
|
||||
|
||||
msgid "workspace.header.menu.toggle-light-theme"
|
||||
msgstr "Switch to light theme"
|
||||
|
||||
msgid "workspace.header.menu.toggle-dark-theme"
|
||||
msgstr "Switch to dark theme"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs
|
||||
msgid "workspace.header.reset-zoom"
|
||||
msgstr "Reset"
|
||||
@ -3593,6 +3599,10 @@ msgstr "Search shared libraries"
|
||||
msgid "workspace.libraries.shared-libraries"
|
||||
msgstr "SHARED LIBRARIES"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs
|
||||
msgid "workspace.libraries.loading"
|
||||
msgstr "Loading…"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.libraries.text.multiple-typography"
|
||||
msgstr "Multiple typographies"
|
||||
|
||||
@ -3504,6 +3504,12 @@ msgstr "Mostrar paleta de textos"
|
||||
msgid "workspace.header.menu.undo"
|
||||
msgstr "Deshacer"
|
||||
|
||||
msgid "workspace.header.menu.toggle-light-theme"
|
||||
msgstr "Cambiar a tema claro"
|
||||
|
||||
msgid "workspace.header.menu.toggle-dark-theme"
|
||||
msgstr "Cambiar a tema oscuro"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs
|
||||
msgid "workspace.header.reset-zoom"
|
||||
msgstr "Restablecer"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user