mirror of
https://github.com/penpot/penpot.git
synced 2026-02-23 10:17:35 -05:00
Compare commits
60 Commits
1.0.0-alph
...
1.1.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4703f6d5c7 | ||
|
|
8d2797f8a1 | ||
|
|
6cdde84445 | ||
|
|
afa35379b2 | ||
|
|
1099e08b7d | ||
|
|
89cb20ada7 | ||
|
|
32b0fd7b36 | ||
|
|
04670bb5f2 | ||
|
|
8566fe4ac1 | ||
|
|
e607e8315c | ||
|
|
a9b7cf61a5 | ||
|
|
7c7bda669c | ||
|
|
0c82c6f2f5 | ||
|
|
b7cbe49cb2 | ||
|
|
7378089f4a | ||
|
|
62b6b12066 | ||
|
|
39fdff9052 | ||
|
|
32c0913f00 | ||
|
|
7eb90d62b0 | ||
|
|
ec2683417f | ||
|
|
cb23c8b093 | ||
|
|
687f7ddf64 | ||
|
|
992a8e9aef | ||
|
|
6e08c6bc35 | ||
|
|
b71d05935a | ||
|
|
c14dbc19f8 | ||
|
|
1eff1c94c4 | ||
|
|
53be7feee1 | ||
|
|
e182cc4028 | ||
|
|
80309cbff3 | ||
|
|
816db29f9c | ||
|
|
526e0afc70 | ||
|
|
77973af49f | ||
|
|
dc5cff645a | ||
|
|
0ea8e9e750 | ||
|
|
69b4968578 | ||
|
|
b7e266e350 | ||
|
|
b056cc35e4 | ||
|
|
d66452423f | ||
|
|
d85537fa7b | ||
|
|
fc11fb6e3d | ||
|
|
cbdfb4349b | ||
|
|
19ed0b70c2 | ||
|
|
3092747b5f | ||
|
|
0adfc2ddab | ||
|
|
8fd8bc4537 | ||
|
|
e7d6a54907 | ||
|
|
e3c273c84b | ||
|
|
8aedbd1418 | ||
|
|
e713c30785 | ||
|
|
74a168d87e | ||
|
|
ca63ff621a | ||
|
|
d120af2c81 | ||
|
|
95ab5b57b7 | ||
|
|
2e7f90f3cc | ||
|
|
8403352af8 | ||
|
|
526b6e1f03 | ||
|
|
f2fd976934 | ||
|
|
8b9371d7e1 | ||
|
|
948a4038c6 |
@@ -67,39 +67,61 @@
|
||||
<div class="table-val">{{profile-id}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user-agent %}
|
||||
<div class="table-row">
|
||||
<div class="table-key">VERS: </div>
|
||||
<div class="table-key">UAGENT: </div>
|
||||
<div class="table-val">{{user-agent}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if frontend-version %}
|
||||
<div class="table-row">
|
||||
<div class="table-key">FVERS: </div>
|
||||
<div class="table-val">{{frontend-version}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="table-row">
|
||||
<div class="table-key">BVERS: </div>
|
||||
<div class="table-val">{{version}}</div>
|
||||
</div>
|
||||
|
||||
<div class="table-row">
|
||||
<div class="table-key">HOST: </div>
|
||||
<div class="table-val">{{host}}</div>
|
||||
</div>
|
||||
|
||||
{% if type %}
|
||||
<div class="table-row">
|
||||
<div class="table-key">TYPE: </div>
|
||||
<div class="table-val">{{type}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if code %}
|
||||
<div class="table-row">
|
||||
<div class="table-key">CODE: </div>
|
||||
<div class="table-val">{{code}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="table-row">
|
||||
<div class="table-key">CLASS: </div>
|
||||
<div class="table-val">{{class}}</div>
|
||||
</div>
|
||||
|
||||
<div class="table-row">
|
||||
<div class="table-key">HINT: </div>
|
||||
<div class="table-val">{{hint}}</div>
|
||||
</div>
|
||||
|
||||
{% if method %}
|
||||
<div class="table-row">
|
||||
<div class="table-key">PATH: </div>
|
||||
<div class="table-val">{{method|upper}} {{path}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if params %}
|
||||
<div class="table-row multiline">
|
||||
@@ -128,7 +150,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="table-row multiline">
|
||||
<div class="table-key">TRACE:</div>
|
||||
<div class="table-val">
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
(let [rng (java.util.Random. 1)]
|
||||
(letfn [(create-profile [conn index]
|
||||
(let [id (mk-uuid "profile" index)
|
||||
_ (log/info "create profile" id)
|
||||
_ (log/info "create profile" index id)
|
||||
|
||||
prof (register-profile conn
|
||||
{:id id
|
||||
@@ -98,10 +98,9 @@
|
||||
(create-team [conn index]
|
||||
(let [id (mk-uuid "team" index)
|
||||
name (str "Team" index)]
|
||||
(log/info "create team" id)
|
||||
(log/info "create team" index id)
|
||||
(db/insert! conn :team {:id id
|
||||
:name name
|
||||
:photo ""})
|
||||
:name name})
|
||||
id))
|
||||
|
||||
(create-teams [conn]
|
||||
@@ -113,7 +112,7 @@
|
||||
(let [id (mk-uuid "file" project-id index)
|
||||
name (str "file" index)
|
||||
data (cp/make-file-data id)]
|
||||
(log/info "create file" id)
|
||||
(log/info "create file" index id)
|
||||
(db/insert! conn :file
|
||||
{:id id
|
||||
:data (blob/encode data)
|
||||
@@ -135,7 +134,7 @@
|
||||
(create-project [conn team-id owner-id index]
|
||||
(let [id (mk-uuid "project" team-id index)
|
||||
name (str "project " index)]
|
||||
(log/info "create project" id)
|
||||
(log/info "create project" index id)
|
||||
(db/insert! conn :project
|
||||
{:id id
|
||||
:team-id team-id
|
||||
@@ -188,7 +187,7 @@
|
||||
project-id (:default-project-id owner)
|
||||
data (cp/make-file-data id)]
|
||||
|
||||
(log/info "create draft file" id)
|
||||
(log/info "create draft file" index id)
|
||||
(db/insert! conn :file
|
||||
{:id id
|
||||
:data (blob/encode data)
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
:rlimits-image 2
|
||||
|
||||
:smtp-enabled false
|
||||
:smtp-default-reply-to "no-reply@example.com"
|
||||
:smtp-default-from "no-reply@example.com"
|
||||
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
||||
:smtp-default-from "Penpot <no-reply@example.com>"
|
||||
|
||||
:allow-demo-users true
|
||||
:registration-enabled true
|
||||
@@ -92,8 +92,8 @@
|
||||
|
||||
(s/def ::error-report-webhook ::us/string)
|
||||
(s/def ::smtp-enabled ::us/boolean)
|
||||
(s/def ::smtp-default-reply-to ::us/email)
|
||||
(s/def ::smtp-default-from ::us/email)
|
||||
(s/def ::smtp-default-reply-to ::us/string)
|
||||
(s/def ::smtp-default-from ::us/string)
|
||||
(s/def ::smtp-host ::us/string)
|
||||
(s/def ::smtp-port ::us/integer)
|
||||
(s/def ::smtp-username (s/nilable ::us/string))
|
||||
|
||||
@@ -76,38 +76,42 @@
|
||||
(tr/decode-stream input))))
|
||||
|
||||
(defn create-profile-initial-data
|
||||
[conn profile]
|
||||
(when-let [initial-data-path (:initial-data-file cfg/config)]
|
||||
(when-let [{:keys [file file-library-rel file-media-object]} (read-initial-data initial-data-path)]
|
||||
(let [sample-project-name (:initial-data-project-name cfg/config "Penpot Onboarding")
|
||||
proj (projects/create-project conn {:profile-id (:id profile)
|
||||
:team-id (:default-team-id profile)
|
||||
:name sample-project-name})
|
||||
([conn profile]
|
||||
(when-let [initial-data-path (:initial-data-file cfg/config)]
|
||||
(create-profile-initial-data conn initial-data-path profile)))
|
||||
|
||||
map-ids {}
|
||||
([conn file profile]
|
||||
(when-let [{:keys [file file-library-rel file-media-object]} (read-initial-data file)]
|
||||
(let [sample-project-name (:initial-data-project-name cfg/config "Penpot Onboarding")
|
||||
|
||||
;; Create new ID's and change the references
|
||||
[map-ids file] (change-ids map-ids file #{:id})
|
||||
[map-ids file-library-rel] (change-ids map-ids file-library-rel #{:file-id :library-file-id})
|
||||
[_ file-media-object] (change-ids map-ids file-media-object #{:id :file-id :media-id :thumbnail-id})
|
||||
proj (projects/create-project conn {:profile-id (:id profile)
|
||||
:team-id (:default-team-id profile)
|
||||
:name sample-project-name})
|
||||
|
||||
file (map #(assoc % :project-id (:id proj)) file)
|
||||
file-profile-rel (map #(array-map :file-id (:id %)
|
||||
:profile-id (:id profile)
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true)
|
||||
file)]
|
||||
map-ids {}
|
||||
|
||||
(projects/create-project-profile conn {:project-id (:id proj)
|
||||
:profile-id (:id profile)})
|
||||
;; Create new ID's and change the references
|
||||
[map-ids file] (change-ids map-ids file #{:id})
|
||||
[map-ids file-library-rel] (change-ids map-ids file-library-rel #{:file-id :library-file-id})
|
||||
[_ file-media-object] (change-ids map-ids file-media-object #{:id :file-id :media-id :thumbnail-id})
|
||||
|
||||
(projects/create-team-project-profile conn {:team-id (:default-team-id profile)
|
||||
:project-id (:id proj)
|
||||
:profile-id (:id profile)})
|
||||
file (map #(assoc % :project-id (:id proj)) file)
|
||||
file-profile-rel (map #(array-map :file-id (:id %)
|
||||
:profile-id (:id profile)
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true)
|
||||
file)]
|
||||
|
||||
;; Re-insert into the database
|
||||
(db/insert-multi! conn :file file)
|
||||
(db/insert-multi! conn :file-profile-rel file-profile-rel)
|
||||
(db/insert-multi! conn :file-library-rel file-library-rel)
|
||||
(db/insert-multi! conn :file-media-object file-media-object)))))
|
||||
(projects/create-project-profile conn {:project-id (:id proj)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
(projects/create-team-project-profile conn {:team-id (:default-team-id profile)
|
||||
:project-id (:id proj)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
;; Re-insert into the database
|
||||
(db/insert-multi! conn :file file)
|
||||
(db/insert-multi! conn :file-profile-rel file-profile-rel)
|
||||
(db/insert-multi! conn :file-library-rel file-library-rel)
|
||||
(db/insert-multi! conn :file-media-object file-media-object)))))
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
(declare handle-event)
|
||||
|
||||
(defonce enabled-mattermost (atom true))
|
||||
(defonce queue (a/chan (a/sliding-buffer 64)))
|
||||
(defonce queue-fn (fn [event] (a/>!! queue event)))
|
||||
|
||||
@@ -117,7 +118,7 @@
|
||||
[cfg event]
|
||||
(try
|
||||
(let [cdata (get-context-data event)]
|
||||
(when (:uri cfg)
|
||||
(when (and (:uri cfg) @enabled-mattermost)
|
||||
(send-mattermost-notification! cfg cdata))
|
||||
(persist-on-database! cfg cdata))
|
||||
(catch Exception e
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.http.errors :as errors]
|
||||
[app.http.middleware :as middleware]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.log4j :refer [update-thread-context!]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.tools.logging :as log]
|
||||
[integrant.core :as ig]
|
||||
@@ -103,7 +104,7 @@
|
||||
(catch Throwable e
|
||||
(try
|
||||
(let [cdata (errors/get-error-context request e)]
|
||||
(errors/update-thread-context! cdata)
|
||||
(update-thread-context! cdata)
|
||||
(log/errorf e "Unhandled exception: %s (id: %s)" (ex-message e) (str (:id cdata)))
|
||||
{:status 500
|
||||
:body "internal server error"})
|
||||
|
||||
@@ -12,25 +12,10 @@
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[app.util.log4j :refer [update-thread-context!]]
|
||||
[clojure.tools.logging :as log]
|
||||
[cuerdas.core :as str]
|
||||
[expound.alpha :as expound])
|
||||
(:import
|
||||
org.apache.logging.log4j.ThreadContext))
|
||||
|
||||
(defn update-thread-context!
|
||||
[data]
|
||||
(run! (fn [[key val]]
|
||||
(ThreadContext/put
|
||||
(name key)
|
||||
(cond
|
||||
(coll? val)
|
||||
(binding [clojure.pprint/*print-right-margin* 120]
|
||||
(with-out-str (pprint val)))
|
||||
(instance? clojure.lang.Named val) (name val)
|
||||
:else (str val))))
|
||||
data))
|
||||
[expound.alpha :as expound]))
|
||||
|
||||
(defn- explain-error
|
||||
[error]
|
||||
@@ -48,10 +33,12 @@
|
||||
:version (:full cfg/version)
|
||||
:host (:public-uri cfg/config)
|
||||
:class (.getCanonicalName ^java.lang.Class (class error))
|
||||
:hint (ex-message error)}
|
||||
:hint (ex-message error)
|
||||
:data edata}
|
||||
|
||||
(when (map? edata)
|
||||
edata)
|
||||
(let [headers (:headers request)]
|
||||
{:user-agent (get headers "user-agent")
|
||||
:frontend-version (get headers "x-frontend-version" "unknown")})
|
||||
|
||||
(when (and (map? edata) (:data edata))
|
||||
{:explain (explain-error edata)}))))
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
(ns app.http.session
|
||||
(:require
|
||||
[app.db :as db]
|
||||
[app.http.errors :refer [update-thread-context!]]
|
||||
[app.util.log4j :refer [update-thread-context!]]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]
|
||||
[clojure.spec.alpha :as s]
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
:app.http.assets/handlers
|
||||
{:metrics (ig/ref :app.metrics/metrics)
|
||||
:assets-path (:assets-path cfg/config)
|
||||
:assets-path (:assets-path config)
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:cache-max-age (dt/duration {:hours 24})
|
||||
:signature-max-age (dt/duration {:hours 24 :minutes 5})}
|
||||
@@ -148,11 +148,11 @@
|
||||
|
||||
;; RLimit definition for password hashing
|
||||
:app.rlimits/password
|
||||
(:rlimits-password cfg/config)
|
||||
(:rlimits-password config)
|
||||
|
||||
;; RLimit definition for image processing
|
||||
:app.rlimits/image
|
||||
(:rlimits-image cfg/config)
|
||||
(:rlimits-image config)
|
||||
|
||||
;; A collection of rlimits as hash-map.
|
||||
:app.rlimits/all
|
||||
@@ -211,10 +211,10 @@
|
||||
:cron #app/cron "0 0 0 */1 * ?" ;; daily
|
||||
:fn (ig/ref :app.tasks.tasks-gc/handler)}
|
||||
|
||||
(when (:telemetry-enabled cfg/config)
|
||||
(when (:telemetry-enabled config)
|
||||
{:id "telemetry"
|
||||
:cron #app/cron "0 0 */6 * * ?" ;; every 6h
|
||||
:uri (:telemetry-uri cfg/config)
|
||||
:uri (:telemetry-uri config)
|
||||
:fn (ig/ref :app.tasks.telemetry/handler)})]}
|
||||
|
||||
:app.tasks/all
|
||||
@@ -265,18 +265,18 @@
|
||||
:app.tasks.telemetry/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:version (:full cfg/version)
|
||||
:uri (:telemetry-uri cfg/config)
|
||||
:uri (:telemetry-uri config)
|
||||
:sprops (ig/ref :app.sprops/props)}
|
||||
|
||||
:app.srepl/server
|
||||
{:port (:srepl-port cfg/config)
|
||||
:host (:srepl-host cfg/config)}
|
||||
{:port (:srepl-port config)
|
||||
:host (:srepl-host config)}
|
||||
|
||||
:app.sprops/props
|
||||
{:pool (ig/ref :app.db/pool)}
|
||||
|
||||
:app.error-reporter/reporter
|
||||
{:uri (:error-report-webhook cfg/config)
|
||||
{:uri (:error-report-webhook config)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)}
|
||||
|
||||
@@ -286,18 +286,18 @@
|
||||
:app.storage/storage
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)
|
||||
:backend (:storage-backend cfg/config :fs)
|
||||
:backend (:storage-backend config :fs)
|
||||
:backends {:s3 (ig/ref [::main :app.storage.s3/backend])
|
||||
:db (ig/ref [::main :app.storage.db/backend])
|
||||
:fs (ig/ref [::main :app.storage.fs/backend])
|
||||
:tmp (ig/ref [::tmp :app.storage.fs/backend])}}
|
||||
|
||||
[::main :app.storage.s3/backend]
|
||||
{:region (:storage-s3-region cfg/config)
|
||||
:bucket (:storage-s3-bucket cfg/config)}
|
||||
{:region (:storage-s3-region config)
|
||||
:bucket (:storage-s3-bucket config)}
|
||||
|
||||
[::main :app.storage.fs/backend]
|
||||
{:directory (:storage-fs-directory cfg/config)}
|
||||
{:directory (:storage-fs-directory config)}
|
||||
|
||||
[::tmp :app.storage.fs/backend]
|
||||
{:directory "/tmp/penpot"}
|
||||
@@ -305,7 +305,7 @@
|
||||
[::main :app.storage.db/backend]
|
||||
{:pool (ig/ref :app.db/pool)}}
|
||||
|
||||
(when (:telemetry-server-enabled cfg/config)
|
||||
(when (:telemetry-server-enabled config)
|
||||
{:app.telemetry/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)}
|
||||
|
||||
@@ -78,8 +78,10 @@
|
||||
;; profile data.
|
||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
||||
claims (assoc claims :member-id (:id profile))
|
||||
params (assoc params :profile-id (:id profile))]
|
||||
(process-token conn params claims)
|
||||
params (assoc params :profile-id (:id profile))
|
||||
cfg (assoc cfg :conn conn)]
|
||||
|
||||
(process-token cfg params claims)
|
||||
|
||||
;; Automatically mark the created profile as active because
|
||||
;; we already have the verification of email with the
|
||||
@@ -168,15 +170,24 @@
|
||||
active? (if demo? true is-active)
|
||||
props (db/tjson (or props {}))
|
||||
password (derive-password password)]
|
||||
(-> (db/insert! conn :profile
|
||||
{:id id
|
||||
:fullname fullname
|
||||
:email (str/lower email)
|
||||
:password password
|
||||
:props props
|
||||
:is-active active?
|
||||
:is-demo demo?})
|
||||
(update :props db/decode-transit-pgobject))))
|
||||
(try
|
||||
(-> (db/insert! conn :profile
|
||||
{:id id
|
||||
:fullname fullname
|
||||
:email (str/lower email)
|
||||
:password password
|
||||
:props props
|
||||
:is-active active?
|
||||
:is-demo demo?})
|
||||
(update :props db/decode-transit-pgobject))
|
||||
(catch org.postgresql.util.PSQLException e
|
||||
(let [state (.getSQLState e)]
|
||||
(if (not= state "23505")
|
||||
(throw e)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-exists
|
||||
:cause e)))))))
|
||||
|
||||
|
||||
(defn- create-profile-relations
|
||||
[conn profile]
|
||||
@@ -392,11 +403,14 @@
|
||||
:name (:fullname profile)}))]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(some->> email
|
||||
(profile/retrieve-profile-data-by-email conn)
|
||||
(create-recovery-token)
|
||||
(send-email-notification conn))
|
||||
nil)))
|
||||
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
|
||||
(when-not (:is-active profile)
|
||||
(ex/raise :type :validation
|
||||
:code :profile-not-verified
|
||||
:hint "the user need to validate profile before recover password"))
|
||||
(->> profile
|
||||
(create-recovery-token)
|
||||
(send-email-notification conn))))))
|
||||
|
||||
|
||||
;; --- Mutation: Recover Profile
|
||||
|
||||
@@ -44,20 +44,29 @@
|
||||
claims)
|
||||
|
||||
(defmethod process-token :verify-email
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (db/get-by-id conn :profile profile-id {:for-update true})]
|
||||
(when (:is-active profile)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-validated))
|
||||
(when (not= (:email profile)
|
||||
(:email claims))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
[{:keys [conn session] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/retrieve-profile conn profile-id)
|
||||
claims (assoc claims :profile profile)]
|
||||
|
||||
(when-not (:is-active profile)
|
||||
(when (not= (:email profile)
|
||||
(:email claims))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id profile)}))
|
||||
|
||||
(with-meta claims
|
||||
{:transform-response
|
||||
(fn [request response]
|
||||
(let [uagent (get-in request [:headers "user-agent"])
|
||||
id (session/create! session {:profile-id profile-id
|
||||
:user-agent uagent})]
|
||||
(assoc response
|
||||
:cookies (session/cookies session {:value id}))))})))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id profile)})
|
||||
claims))
|
||||
|
||||
(defmethod process-token :auth
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
(:require
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.db.profile-initial-data :as pid]
|
||||
[app.main :refer [system]]
|
||||
[app.rpc.queries.profile :as prof]
|
||||
[app.srepl.dev :as dev]
|
||||
[app.util.blob :as blob]
|
||||
[clojure.pprint :refer [pprint]]))
|
||||
@@ -58,3 +60,15 @@
|
||||
([system project-id path]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(pid/create-initial-data-dump conn project-id path))))
|
||||
|
||||
(defn load-data-into-user
|
||||
([system user-email]
|
||||
(if-let [file (:initial-data-file cfg/config)]
|
||||
(load-data-into-user system file user-email)
|
||||
(prn "Data file not found in configuration")))
|
||||
|
||||
([system file user-email]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [profile (prof/retrieve-profile-data-by-email conn user-email)
|
||||
profile (merge profile (prof/retrieve-additional-data conn (:id profile)))]
|
||||
(pid/create-profile-initial-data conn file profile)))))
|
||||
|
||||
@@ -29,24 +29,11 @@
|
||||
;; Email Building
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn build-address
|
||||
[v charset]
|
||||
(try
|
||||
(cond
|
||||
(string? v)
|
||||
(InternetAddress. v nil charset)
|
||||
(defn- parse-address
|
||||
[v]
|
||||
(InternetAddress/parse ^String v))
|
||||
|
||||
(map? v)
|
||||
(InternetAddress. (:addr v)
|
||||
(:name v)
|
||||
(:charset v charset))
|
||||
|
||||
:else
|
||||
(throw (ex-info "Invalid address" {:data v})))
|
||||
(catch Exception e
|
||||
(throw (ex-info "Invalid address" {:data v} e)))))
|
||||
|
||||
(defn- resolve-recipient-type
|
||||
(defn- ^Message$RecipientType resolve-recipient-type
|
||||
[type]
|
||||
(case type
|
||||
:to Message$RecipientType/TO
|
||||
@@ -54,33 +41,33 @@
|
||||
:bcc Message$RecipientType/BCC))
|
||||
|
||||
(defn- assign-recipient
|
||||
[^MimeMessage mmsg type address charset]
|
||||
[^MimeMessage mmsg type address]
|
||||
(if (sequential? address)
|
||||
(reduce #(assign-recipient %1 type %2 charset) mmsg address)
|
||||
(let [address (build-address address charset)
|
||||
(reduce #(assign-recipient %1 type %2) mmsg address)
|
||||
(let [address (parse-address address)
|
||||
type (resolve-recipient-type type)]
|
||||
(.addRecipient mmsg type address)
|
||||
(.addRecipients mmsg type address)
|
||||
mmsg)))
|
||||
|
||||
(defn- assign-recipients
|
||||
[mmsg {:keys [to cc bcc charset] :or {charset "utf-8"} :as params}]
|
||||
[mmsg {:keys [to cc bcc] :as params}]
|
||||
(cond-> mmsg
|
||||
(some? to) (assign-recipient :to to charset)
|
||||
(some? cc) (assign-recipient :cc cc charset)
|
||||
(some? bcc) (assign-recipient :bcc bcc charset)))
|
||||
(some? to) (assign-recipient :to to)
|
||||
(some? cc) (assign-recipient :cc cc)
|
||||
(some? bcc) (assign-recipient :bcc bcc)))
|
||||
|
||||
(defn- assign-from
|
||||
[mmsg {:keys [from charset] :or {charset "utf-8"}}]
|
||||
(when from
|
||||
(let [from (build-address from charset)]
|
||||
(.setFrom ^MimeMessage mmsg ^InternetAddress from))))
|
||||
[mmsg {:keys [default-from]} {:keys [from] :as props}]
|
||||
(let [from (or from default-from)]
|
||||
(when from
|
||||
(let [from (parse-address from)]
|
||||
(.addFrom ^MimeMessage mmsg from)))))
|
||||
|
||||
(defn- assign-reply-to
|
||||
[mmsg {:keys [defaut-reply-to]} {:keys [reply-to charset] :or {charset "utf-8"}}]
|
||||
(let [reply-to (or reply-to defaut-reply-to)]
|
||||
[mmsg {:keys [default-reply-to] :as cfg} {:keys [reply-to] :as params}]
|
||||
(let [reply-to (or reply-to default-reply-to)]
|
||||
(when reply-to
|
||||
(let [reply-to (build-address reply-to charset)
|
||||
reply-to (into-array InternetAddress [reply-to])]
|
||||
(let [reply-to (parse-address reply-to)]
|
||||
(.setReplyTo ^MimeMessage mmsg reply-to)))))
|
||||
|
||||
(defn- assign-subject
|
||||
@@ -136,7 +123,7 @@
|
||||
[cfg session params]
|
||||
(let [mmsg (MimeMessage. ^Session session)]
|
||||
(assign-recipients mmsg params)
|
||||
(assign-from mmsg params)
|
||||
(assign-from mmsg cfg params)
|
||||
(assign-reply-to mmsg cfg params)
|
||||
(assign-subject mmsg params)
|
||||
(assign-extra-headers mmsg params)
|
||||
@@ -156,12 +143,12 @@
|
||||
(Properties.)
|
||||
{"mail.user" username
|
||||
"mail.host" host
|
||||
"mail.from" default-from
|
||||
"mail.smtp.auth" (boolean username)
|
||||
"mail.smtp.starttls.enable" tls
|
||||
"mail.smtp.starttls.required" tls
|
||||
"mail.smtp.host" host
|
||||
"mail.smtp.port" port
|
||||
"mail.smtp.from" default-from
|
||||
"mail.smtp.user" username
|
||||
"mail.smtp.timeout" timeout
|
||||
"mail.smtp.connectiontimeout" timeout}))
|
||||
@@ -183,7 +170,9 @@
|
||||
(defn send!
|
||||
[cfg message]
|
||||
(let [^MimeMessage message (smtp-message cfg message)]
|
||||
(Transport/send message (:username cfg) (:password cfg))
|
||||
(Transport/send message
|
||||
(:username cfg)
|
||||
(:password cfg))
|
||||
nil))
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
27
backend/src/app/util/log4j.clj
Normal file
27
backend/src/app/util/log4j.clj
Normal file
@@ -0,0 +1,27 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2021 UXBOX Labs SL
|
||||
|
||||
(ns app.util.log4j
|
||||
(:require
|
||||
[clojure.pprint :refer [pprint]])
|
||||
(:import
|
||||
org.apache.logging.log4j.ThreadContext))
|
||||
|
||||
(defn update-thread-context!
|
||||
[data]
|
||||
(run! (fn [[key val]]
|
||||
(ThreadContext/put
|
||||
(name key)
|
||||
(cond
|
||||
(coll? val)
|
||||
(binding [clojure.pprint/*print-right-margin* 120]
|
||||
(with-out-str (pprint val)))
|
||||
(instance? clojure.lang.Named val) (name val)
|
||||
:else (str val))))
|
||||
data))
|
||||
@@ -12,11 +12,12 @@
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.util.async :as aa]
|
||||
[app.util.log4j :refer [update-thread-context!]]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.tools.logging :as log]
|
||||
[integrant.core :as ig]
|
||||
@@ -205,6 +206,17 @@
|
||||
(log/warn "no task handler found for" (pr-str name)))
|
||||
{:status :completed :task item}))
|
||||
|
||||
(defn get-error-context
|
||||
[error item]
|
||||
(let [edata (ex-data error)]
|
||||
{:id (uuid/next)
|
||||
:version (:full cfg/version)
|
||||
:host (:public-uri cfg/config)
|
||||
:class (.getCanonicalName ^java.lang.Class (class error))
|
||||
:hint (ex-message error)
|
||||
:data edata
|
||||
:params item}))
|
||||
|
||||
(defn- handle-exception
|
||||
[error item]
|
||||
(let [edata (ex-data error)]
|
||||
@@ -218,14 +230,9 @@
|
||||
(= ::noop (:strategy edata))
|
||||
(assoc :inc-by 0))
|
||||
|
||||
(do
|
||||
(log/errorf error
|
||||
(str "Unhandled exception.\n"
|
||||
"=> task: " (:name item) "\n"
|
||||
"=> retry: " (:retry-num item) "\n"
|
||||
"=> props: \n"
|
||||
(with-out-str
|
||||
(pprint (:props item)))))
|
||||
(let [cdata (get-error-context error item)]
|
||||
(update-thread-context! cdata)
|
||||
(log/errorf error "Unhandled exception on task (id: %s)" (:id cdata))
|
||||
(if (>= (:retry-num item) (:max-retries item))
|
||||
{:status :failed :task item :error error}
|
||||
{:status :retry :task item :error error})))))
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
(let [new-val (get curr attr ::undefined)
|
||||
value (cond
|
||||
(= new-val ::undefined) value
|
||||
(= new-val :multiple) :multiple
|
||||
(= value ::undefined) (sel new-val)
|
||||
(eqfn new-val value) value
|
||||
:else :multiple)]
|
||||
|
||||
@@ -328,6 +328,11 @@
|
||||
nil
|
||||
(apply f args))))
|
||||
|
||||
(defn nilv
|
||||
"Returns a default value if the given value is nil"
|
||||
[v default]
|
||||
(if (some? v) v default))
|
||||
|
||||
(defn check-num
|
||||
"Function that checks if a number is nil or nan. Will return 0 when not
|
||||
valid and the number otherwise."
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
(d/export helpers/set-touched-group)
|
||||
(d/export helpers/touched-group?)
|
||||
(d/export helpers/get-base-shape)
|
||||
(d/export helpers/is-parent?)
|
||||
|
||||
;; Process changes
|
||||
(d/export changes/process-changes)
|
||||
|
||||
@@ -376,3 +376,25 @@
|
||||
|
||||
;; The first id will be the top-most
|
||||
(get objects (first sorted-ids))))
|
||||
|
||||
(defn is-parent?
|
||||
"Check if `parent-candidate` is parent of `shape-id`"
|
||||
[objects shape-id parent-candidate]
|
||||
|
||||
(loop [current (get objects parent-candidate)
|
||||
done #{}
|
||||
pending (:shapes current)]
|
||||
|
||||
(cond
|
||||
(contains? done (:id current))
|
||||
(recur (get objects (first pending))
|
||||
done
|
||||
(rest pending))
|
||||
|
||||
(empty? pending) false
|
||||
(and current (contains? (set (:shapes current)) shape-id)) true
|
||||
|
||||
:else
|
||||
(recur (get objects (first pending))
|
||||
(conj done (:id current))
|
||||
(concat (rest pending) (:shapes current))))))
|
||||
|
||||
@@ -10,7 +10,7 @@ volumes:
|
||||
|
||||
services:
|
||||
penpot-frontend:
|
||||
image: "penpotapp/frontend:develop"
|
||||
image: "penpotapp/frontend:latest"
|
||||
ports:
|
||||
- 9001:80
|
||||
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
- penpot
|
||||
|
||||
penpot-backend:
|
||||
image: "penpotapp/backend:develop"
|
||||
image: "penpotapp/backend:latest"
|
||||
volumes:
|
||||
- penpot_assets_data:/opt/data
|
||||
|
||||
@@ -79,7 +79,7 @@ services:
|
||||
- penpot
|
||||
|
||||
penpot-exporter:
|
||||
image: "penpotapp/exporter:develop"
|
||||
image: "penpotapp/exporter:latest"
|
||||
environment:
|
||||
# Don't touch it; this uses internal docker network to
|
||||
# communicate with the frontend.
|
||||
|
||||
@@ -107,7 +107,7 @@ http {
|
||||
}
|
||||
|
||||
location /assets {
|
||||
proxy_pass http://127.0.0.1:6060/assets;
|
||||
proxy_pass http://penpot-backend:6060/assets;
|
||||
recursive_error_pages on;
|
||||
proxy_intercept_errors on;
|
||||
error_page 301 302 307 = @handle_redirect;
|
||||
@@ -115,7 +115,7 @@ http {
|
||||
|
||||
location /internal/assets {
|
||||
internal;
|
||||
alias /var/www/assets;
|
||||
alias /opt/data/assets;
|
||||
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,4 @@ docker-compose -p penpot -f docker-compose.yaml up
|
||||
The docker compose file contains the essential configuration for
|
||||
getting the application running, and many essential configurations
|
||||
already explained in comments. All other configuration options are
|
||||
explained in [management guide](./05-Management-Guide.md).
|
||||
explained in [configuration guide](./05-Configuration-Guide.md).
|
||||
|
||||
@@ -200,10 +200,10 @@ If any of the following variables are defined, they will enable the
|
||||
corresponding auth button in the login page
|
||||
|
||||
```js
|
||||
var appGoogleClientID = "<google-client-id-here>";
|
||||
var appGitlabClientID = "<gitlab-client-id-here>";
|
||||
var appGithubClientID = "<github-client-id-here>";
|
||||
var appLoginWithLDAP = <true|false>;
|
||||
var penpotGoogleClientID = "<google-client-id-here>";
|
||||
var penpotGitlabClientID = "<gitlab-client-id-here>";
|
||||
var penpotGithubClientID = "<github-client-id-here>";
|
||||
var penpotLoginWithLDAP = <true|false>;
|
||||
```
|
||||
|
||||
**NOTE:** The configuration should match the backend configuration for
|
||||
@@ -216,8 +216,8 @@ It is possible to display a warning message on a demo environment and
|
||||
disable/enable demo users:
|
||||
|
||||
```js
|
||||
var appDemoWarning = <true|false>;
|
||||
var appAllowDemoUsers = <true|false>;
|
||||
var penpotDemoWarning = <true|false>;
|
||||
var penpotAllowDemoUsers = <true|false>;
|
||||
```
|
||||
|
||||
**NOTE:** The configuration for demo users should match the backend
|
||||
|
||||
3
frontend/resources/images/icons/icon-verify.svg
Normal file
3
frontend/resources/images/icons/icon-verify.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="279.383" height="369.004" viewBox="0 0 261.922 345.941">
|
||||
<path d="M68.319 0L31.94 51.236v28.962L.286 95.255 0 95.12v188.953l123.419 58.308 7.542 3.56 7.542-3.56 123.419-58.308V95.12l-.233.11-31.666-15.062V51.236l-1.106-1.558L193.645 0l-36.38 51.236v.052l-26.47-37.283L104.527 51l-.94-1.322L68.318 0zm6.436 29.762l14.07 19.815H47.81l13.908-19.583 13.036-.232zm125.325 0l14.071 19.815H173.14l13.904-19.583 13.037-.232zm-62.85 14.007l14.07 19.814h-41.008L124.195 44l13.035-.23zM43.923 59.564h19.452v65.497l-19.452-9.19V59.564zm29.438 0h19.355l-.002 79.356-19.355-9.142.002-70.214zm95.887 0h19.453l-.001 70.146-19.452 9.188V59.564zm29.438 0h19.353v56.285l-19.353 9.142V59.564zM106.4 73.57h19.451v81.004l-19.45-9.19V73.57zm29.44 0h19.35l-.001 71.971-19.353 9.145.004-81.116zm94.18 21.526l17.126 7.002-17.126 8.09V95.095zm-198.08.025v15.09l-17.12-8.09 17.12-7zm-16.857 23.81l108.337 51.178v155.588L15.082 274.52V118.93zm231.756 0v155.588l-108.335 51.178V170.11l108.335-51.179zm-19.521 44.302l-45.187 82.05-26.366-21.373-7.364 12.228 37.954 30.627 51.45-94.185-10.487-9.347z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because it is too large
Load Diff
@@ -61,6 +61,35 @@ textarea {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
svg {
|
||||
fill: $color-gray-60;
|
||||
height: 40%;
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
font-size: $fs18;
|
||||
color: $color-gray-60;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.notification-text-email {
|
||||
background: $color-gray-10;
|
||||
border-radius: $br-small;
|
||||
color: $color-gray-60;
|
||||
font-size: $fs18;
|
||||
font-weight: 500;
|
||||
margin: 1.5rem 0 2.5rem 0;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $fs24;
|
||||
color: $color-gray-60;
|
||||
|
||||
@@ -211,8 +211,10 @@
|
||||
|
||||
width: calc(100% - 1rem);
|
||||
min-height: 5rem;
|
||||
|
||||
img {
|
||||
max-height: 8rem;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +99,14 @@
|
||||
position: fixed;
|
||||
right: calc(#{$width-settings-bar} + 10px);
|
||||
text-align: center;
|
||||
width: 110px;
|
||||
width: 125px;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 2px;
|
||||
transition: bottom 0.5s;
|
||||
|
||||
&.color-palette-open {
|
||||
bottom: 5rem;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $color-white;
|
||||
|
||||
@@ -3,7 +3,18 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>PENPOT - The Open-Source prototyping tool</title>
|
||||
<title>Penpot - Design Freedom for Teams</title>
|
||||
<meta name="description" content="The open-source solution for design and prototyping.">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:title" content="Penpot | Design Freedom for Teams">
|
||||
<meta property="og:description" content="The open-source solution for design and prototyping">
|
||||
<meta property="og:image" content="https://penpot.app/images/workspace-ui.jpg">
|
||||
<meta name="twitter:title" content="Penpot | Design Freedom for Teams">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:description" content="The open-source solution for design and prototyping">
|
||||
<meta name="twitter:image" content="https://penpot.app/images/workspace-ui.jpg">
|
||||
<meta name="twitter:site" content="@penpotapp">
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<link id="theme" href="/css/main-{{& th}}.css?ts={{& ts}}"
|
||||
rel="stylesheet" type="text/css" />
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{:deps {:aliases [:dev]}
|
||||
:http {:port 3448}
|
||||
:nrepl {:port 3447}
|
||||
:jvm-opts ["-Xmx1g" "-Xms512m"]
|
||||
:jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC"]
|
||||
|
||||
:builds
|
||||
{:main
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
|
||||
(ns app.main
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.spec :as us]
|
||||
[app.main.repo :as rp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.auth :refer [logout]]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.users :as udu]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui :as ui]
|
||||
[app.main.ui.confirm]
|
||||
@@ -22,12 +23,12 @@
|
||||
[app.main.worker]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.logging :as log]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.theme :as theme]
|
||||
[app.util.timers :as ts]
|
||||
[app.util.logging :as log]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
@@ -71,7 +72,7 @@
|
||||
(st/emit! (rt/nav :auth-login))
|
||||
|
||||
(nil? match)
|
||||
(st/emit! (rt/nav :not-found))
|
||||
(st/emit! (dm/assign-exception {:type :not-found}))
|
||||
|
||||
:else
|
||||
(st/emit! #(assoc % :route match)))))
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
(let [team-id (:default-team-id profile)]
|
||||
(rx/merge
|
||||
(rx/of (du/profile-fetched profile)
|
||||
(rt/nav :dashboard-projects {:team-id team-id}))
|
||||
(rt/nav' :dashboard-projects {:team-id team-id}))
|
||||
(when-not (get-in profile [:props :onboarding-viewed])
|
||||
(->> (rx/of (modal/show {:type :onboarding}))
|
||||
(rx/delay 1000))))))))
|
||||
@@ -77,9 +77,7 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(let [team-id (:default-team-id profile)]
|
||||
(rx/of (du/profile-fetched profile)
|
||||
(rt/nav' :dashboard-projects {:team-id team-id}))))))
|
||||
(rx/of (logged-in profile)))))
|
||||
|
||||
(defn login-with-ldap
|
||||
[{:keys [email password] :as data}]
|
||||
@@ -184,10 +182,7 @@
|
||||
|
||||
(->> (rp/mutation :request-profile-recovery data)
|
||||
(rx/tap on-success)
|
||||
(rx/catch (fn [err]
|
||||
(on-error err)
|
||||
(rx/empty))))))))
|
||||
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- Recovery (Password)
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
(-> state
|
||||
(update-in [:workspace-file :colors] #(d/replace-by-id % color))))))
|
||||
|
||||
(defn change-palette-size [size]
|
||||
(defn change-palette-size
|
||||
[size]
|
||||
(s/assert #{:big :small} size)
|
||||
(ptk/reify ::change-palette-size
|
||||
ptk/UpdateEvent
|
||||
@@ -58,14 +59,27 @@
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :selected-palette-size] size)))))
|
||||
|
||||
(defn change-palette-selected [selected]
|
||||
(defn change-palette-selected
|
||||
"Change the library used by the general palette tool"
|
||||
[selected]
|
||||
(ptk/reify ::change-palette-selected
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :selected-palette] selected)))))
|
||||
|
||||
(defn show-palette [selected]
|
||||
(defn change-palette-selected-colorpicker
|
||||
"Change the library used by the color picker"
|
||||
[selected]
|
||||
(ptk/reify ::change-palette-selected-colorpicker
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :selected-palette-colorpicker] selected)))))
|
||||
|
||||
(defn show-palette
|
||||
"Show the palette tool and change the library it uses"
|
||||
[selected]
|
||||
(ptk/reify ::change-palette-selected
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -73,14 +87,16 @@
|
||||
(update :workspace-layout conj :colorpalette)
|
||||
(assoc-in [:workspace-local :selected-palette] selected)))))
|
||||
|
||||
(defn start-picker []
|
||||
(defn start-picker
|
||||
[]
|
||||
(ptk/reify ::start-picker
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :picking-color?] true)))))
|
||||
|
||||
(defn stop-picker []
|
||||
(defn stop-picker
|
||||
[]
|
||||
(ptk/reify ::stop-picker
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -89,14 +105,16 @@
|
||||
(update :workspace-local dissoc :picked-shift?)
|
||||
(assoc-in [:workspace-local :picking-color?] false)))))
|
||||
|
||||
(defn pick-color [rgba]
|
||||
(defn pick-color
|
||||
[rgba]
|
||||
(ptk/reify ::pick-color
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :picked-color] rgba)))))
|
||||
|
||||
(defn pick-color-select [value shift?]
|
||||
(defn pick-color-select
|
||||
[value shift?]
|
||||
(ptk/reify ::pick-color-select
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -116,11 +134,21 @@
|
||||
text-ids (filter is-text? ids)
|
||||
shape-ids (filter (comp not is-text?) ids)
|
||||
|
||||
attrs {:fill-color (:color color)
|
||||
:fill-color-ref-id (:id color)
|
||||
:fill-color-ref-file (:file-id color)
|
||||
:fill-color-gradient (:gradient color)
|
||||
:fill-opacity (:opacity color)}
|
||||
attrs (cond-> {}
|
||||
(contains? color :color)
|
||||
(assoc :fill-color (:color color))
|
||||
|
||||
(contains? color :id)
|
||||
(assoc :fill-color-ref-id (:id color))
|
||||
|
||||
(contains? color :file-id)
|
||||
(assoc :fill-color-ref-file (:file-id color))
|
||||
|
||||
(contains? color :gradient)
|
||||
(assoc :fill-color-gradient (:gradient color))
|
||||
|
||||
(contains? color :opacity)
|
||||
(assoc :fill-opacity (:opacity color)))
|
||||
|
||||
update-fn (fn [shape] (merge shape attrs))
|
||||
editors (get-in state [:workspace-local :editors])
|
||||
@@ -131,29 +159,42 @@
|
||||
(map #(dwt/update-text-attrs {:id % :editor (get editors %) :attrs attrs}) text-ids)
|
||||
(dwc/update-shapes shape-ids update-fn))))))))
|
||||
|
||||
(defn change-stroke [ids color]
|
||||
(defn change-stroke
|
||||
[ids color]
|
||||
(ptk/reify ::change-stroke
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [pid (:current-page-id state)
|
||||
objects (get-in state [:workspace-data :pages-index pid :objects])
|
||||
not-frame (fn [shape-id] (not= (get-in objects [shape-id :type]) :frame))
|
||||
update-fn (fn [s]
|
||||
(cond-> s
|
||||
true
|
||||
(assoc :stroke-color (:color color)
|
||||
:stroke-opacity (:opacity color)
|
||||
:stroke-color-gradient (:gradient color)
|
||||
:stroke-color-ref-id (:id color)
|
||||
:stroke-color-ref-file (:file-id color))
|
||||
|
||||
(= (:stroke-style s) :none)
|
||||
(assoc :stroke-style :solid
|
||||
:stroke-width 1
|
||||
:stroke-opacity 1)))]
|
||||
color-attrs (cond-> {}
|
||||
(contains? color :color)
|
||||
(assoc :stroke-color (:color color))
|
||||
|
||||
(contains? color :id)
|
||||
(assoc :stroke-color-ref-id (:id color))
|
||||
|
||||
(contains? color :file-id)
|
||||
(assoc :stroke-color-ref-file (:file-id color))
|
||||
|
||||
(contains? color :gradient)
|
||||
(assoc :stroke-color-gradient (:gradient color))
|
||||
|
||||
(contains? color :opacity)
|
||||
(assoc :stroke-opacity (:opacity color)))
|
||||
|
||||
update-fn (fn [shape]
|
||||
(-> shape
|
||||
(merge color-attrs)
|
||||
(cond-> (= (:stroke-style s) :none)
|
||||
(assoc :stroke-style :solid
|
||||
:stroke-width 1
|
||||
:stroke-opacity 1))))]
|
||||
(rx/of (dwc/update-shapes ids update-fn))))))
|
||||
|
||||
(defn picker-for-selected-shape []
|
||||
(defn picker-for-selected-shape
|
||||
[]
|
||||
(let [sub (rx/subject)]
|
||||
(ptk/reify ::picker-for-selected-shape
|
||||
ptk/WatchEvent
|
||||
@@ -189,7 +230,8 @@
|
||||
:props {:on-change handle-change-color}
|
||||
:allow-click-outside true})))))))
|
||||
|
||||
(defn start-gradient [gradient]
|
||||
(defn start-gradient
|
||||
[gradient]
|
||||
(ptk/reify ::start-gradient
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -198,21 +240,24 @@
|
||||
(assoc-in [:workspace-local :current-gradient] gradient)
|
||||
(assoc-in [:workspace-local :current-gradient :shape-id] id))))))
|
||||
|
||||
(defn stop-gradient []
|
||||
(defn stop-gradient
|
||||
[]
|
||||
(ptk/reify ::stop-gradient
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-local dissoc :current-gradient)))))
|
||||
|
||||
(defn update-gradient [changes]
|
||||
(defn update-gradient
|
||||
[changes]
|
||||
(ptk/reify ::update-gradient
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update-in [:workspace-local :current-gradient] merge changes)))))
|
||||
|
||||
(defn select-gradient-stop [spot]
|
||||
(defn select-gradient-stop
|
||||
[spot]
|
||||
(ptk/reify ::select-gradient-stop
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
@@ -50,9 +51,13 @@
|
||||
;; Check that a file obtained with the file javascript API is valid.
|
||||
[file]
|
||||
(when (> (.-size file) cm/max-file-size)
|
||||
(throw (ex-info (tr "errors.media-too-large") {})))
|
||||
(ex/raise :type :validation
|
||||
:code :media-too-large
|
||||
:hint (str/fmt "media size is large than 5mb (size: %s)" (.-size file))))
|
||||
(when-not (contains? cm/valid-media-types (.-type file))
|
||||
(throw (ex-info (tr "errors.media-format-unsupported") {})))
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-not-allowed
|
||||
:hint (str/fmt "media type %s is not supported" (.-type file))))
|
||||
file)
|
||||
|
||||
(defn notify-start-loading
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
(declare show)
|
||||
|
||||
(def default-animation-timeout 600)
|
||||
(def default-timeout 2000)
|
||||
(def default-timeout 5000)
|
||||
|
||||
(s/def ::type #{:success :error :info :warning})
|
||||
(s/def ::position #{:fixed :floating :inline})
|
||||
|
||||
75
frontend/src/app/main/data/shortcuts.cljs
Normal file
75
frontend/src/app/main/data/shortcuts.cljs
Normal file
@@ -0,0 +1,75 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.shortcuts
|
||||
(:require
|
||||
[app.main.data.colors :as mdc]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[potok.core :as ptk]
|
||||
[beicon.core :as rx]
|
||||
[app.config :as cfg])
|
||||
(:refer-clojure :exclude [meta]))
|
||||
|
||||
(def mac-command "\u2318")
|
||||
(def mac-option "\u2325")
|
||||
(def mac-delete "\u232B")
|
||||
(def mac-shift "\u21E7")
|
||||
(def mac-control "\u2303")
|
||||
(def mac-esc "\u238B")
|
||||
|
||||
(def left-arrow "\u2190")
|
||||
(def up-arrow "\u2191")
|
||||
(def right-arrow "\u2192")
|
||||
(def down-arrow "\u2193")
|
||||
|
||||
(defn c-mod
|
||||
"Adds the control/command modifier to a shortcuts depending on the
|
||||
operating system for the user"
|
||||
[shortcut]
|
||||
(if (cfg/check-platform? :macos)
|
||||
(str "command+" shortcut)
|
||||
(str "ctrl+" shortcut)))
|
||||
|
||||
(defn bind-shortcuts [shortcuts bind-fn cb-fn]
|
||||
(doseq [[key {:keys [command disabled fn]}] shortcuts]
|
||||
(when-not disabled
|
||||
(if (vector? command)
|
||||
(doseq [cmd (seq command)]
|
||||
(bind-fn cmd (cb-fn key fn)))
|
||||
(bind-fn command (cb-fn key fn))))))
|
||||
|
||||
(defn meta [key]
|
||||
(str
|
||||
(if (cfg/check-platform? :macos)
|
||||
mac-command
|
||||
"Ctrl+")
|
||||
key))
|
||||
|
||||
(defn shift [key]
|
||||
(str
|
||||
(if (cfg/check-platform? :macos)
|
||||
mac-shift
|
||||
"Shift+")
|
||||
key))
|
||||
|
||||
(defn meta-shift [key]
|
||||
(-> key meta shift))
|
||||
|
||||
(defn supr []
|
||||
(if (cfg/check-platform? :macos)
|
||||
mac-delete
|
||||
"Supr"))
|
||||
|
||||
(defn esc []
|
||||
(if (cfg/check-platform? :macos)
|
||||
mac-esc
|
||||
"Escape"))
|
||||
|
||||
@@ -112,21 +112,22 @@
|
||||
(defn bundle-fetched
|
||||
[{:keys [project file page share-token token libraries users] :as bundle}]
|
||||
(us/verify ::bundle bundle)
|
||||
(ptk/reify ::file-fetched
|
||||
(ptk/reify ::bundle-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (:objects page)
|
||||
frames (extract-frames objects)]
|
||||
(assoc state
|
||||
:viewer-libraries (d/index-by :id libraries)
|
||||
:viewer-data {:project project
|
||||
:objects objects
|
||||
:users (d/index-by :id users)
|
||||
:file file
|
||||
:page page
|
||||
:frames frames
|
||||
:token token
|
||||
:share-token share-token})))))
|
||||
(-> state
|
||||
(assoc :viewer-libraries (d/index-by :id libraries))
|
||||
(update :viewer-data assoc
|
||||
:project project
|
||||
:objects objects
|
||||
:users (d/index-by :id users)
|
||||
:file file
|
||||
:page page
|
||||
:frames frames
|
||||
:token token
|
||||
:share-token share-token))))))
|
||||
|
||||
(defn fetch-comment-threads
|
||||
[{:keys [file-id page-id] :as params}]
|
||||
@@ -346,7 +347,7 @@
|
||||
|
||||
|
||||
(defn set-current-frame [frame-id]
|
||||
(ptk/reify ::current-frame
|
||||
(ptk/reify ::set-current-frame
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-data :current-frame-id] frame-id))))
|
||||
@@ -415,15 +416,3 @@
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :hover] (when hover? id)))))
|
||||
|
||||
;; --- Shortcuts
|
||||
|
||||
(def shortcuts
|
||||
{"+" (st/emitf increase-zoom)
|
||||
"-" (st/emitf decrease-zoom)
|
||||
"ctrl+a" (st/emitf (select-all))
|
||||
"shift+0" (st/emitf zoom-to-50)
|
||||
"shift+1" (st/emitf reset-zoom)
|
||||
"shift+2" (st/emitf zoom-to-200)
|
||||
"left" (st/emitf select-prev-frame)
|
||||
"right" (st/emitf select-next-frame)})
|
||||
|
||||
|
||||
57
frontend/src/app/main/data/viewer/shortcuts.cljs
Normal file
57
frontend/src/app/main/data/viewer/shortcuts.cljs
Normal file
@@ -0,0 +1,57 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.viewer.shortcuts
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.main.data.colors :as mdc]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.shortcuts :refer [c-mod]]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(def shortcuts
|
||||
{:increase-zoom {:tooltip "+"
|
||||
:command "+"
|
||||
:fn (st/emitf dv/increase-zoom)}
|
||||
|
||||
:decrease-zoom {:tooltip "-"
|
||||
:command "-"
|
||||
:fn (st/emitf dv/decrease-zoom)}
|
||||
|
||||
:select-all {:tooltip (ds/meta "A")
|
||||
:command (ds/c-mod "a")
|
||||
:fn (st/emitf (dv/select-all))}
|
||||
|
||||
:zoom-50 {:tooltip (ds/shift "0")
|
||||
:command "shift+0"
|
||||
:fn (st/emitf dv/zoom-to-50)}
|
||||
|
||||
:reset-zoom {:tooltip (ds/shift "1")
|
||||
:command "shift+1"
|
||||
:fn (st/emitf dv/reset-zoom)}
|
||||
|
||||
:zoom-200 {:tooltip (ds/shift "2")
|
||||
:command "shift+2"
|
||||
:fn (st/emitf dv/zoom-to-200)}
|
||||
|
||||
:next-frame {:tooltip ds/left-arrow
|
||||
:command "left"
|
||||
:fn (st/emitf dv/select-prev-frame)}
|
||||
|
||||
:prev-frame {:tooltip ds/right-arrow
|
||||
:command "right"
|
||||
:fn (st/emitf dv/select-next-frame)}})
|
||||
|
||||
(defn get-tooltip [shortcut]
|
||||
(assert (contains? shortcuts shortcut) (str shortcut))
|
||||
(get-in shortcuts [shortcut :tooltip]))
|
||||
@@ -9,14 +9,13 @@
|
||||
|
||||
(ns app.main.data.workspace
|
||||
(:require
|
||||
[goog.string.path :as path]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.align :as gal]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.proportions :as gpr]
|
||||
[app.common.geom.align :as gal]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.helpers :as cph]
|
||||
@@ -27,33 +26,33 @@
|
||||
[app.main.data.colors :as mdc]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.drawing.path :as dwdp]
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.notifications :as dwn]
|
||||
[app.main.data.workspace.persistence :as dwp]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.texts :as dwtxt]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.drawing.path :as dwdp]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :refer [tr] :as i18n]
|
||||
[app.util.logging :as log]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.timers :as ts]
|
||||
[app.util.transit :as t]
|
||||
[app.util.webapi :as wapi]
|
||||
[app.util.i18n :refer [tr] :as i18n]
|
||||
[app.util.object :as obj]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
;; [cljs.pprint :refer [pprint]]
|
||||
[goog.string.path :as path]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
;; (log/set-level! :trace)
|
||||
@@ -121,8 +120,10 @@
|
||||
:left-sidebar? true
|
||||
:right-sidebar? true
|
||||
:color-for-rename nil
|
||||
:selected-palette-colorpicker :recent
|
||||
:selected-palette :recent
|
||||
:selected-palette-size :big
|
||||
:assets-files-open {}
|
||||
:picking-color? false
|
||||
:picked-color nil
|
||||
:picked-color-select false})
|
||||
@@ -822,6 +823,9 @@
|
||||
;; Ignore any shape whose parent is also intented to be moved
|
||||
ids (cp/clean-loops objects ids)
|
||||
|
||||
;; If we try to move a parent into a child we remove it
|
||||
ids (filter #(not (cp/is-parent? objects parent-id %)) ids)
|
||||
|
||||
parents (loop [res #{parent-id}
|
||||
ids (seq ids)]
|
||||
(if (nil? ids)
|
||||
@@ -1220,20 +1224,26 @@
|
||||
|
||||
|
||||
(defn go-to-viewer
|
||||
[{:keys [file-id page-id] :as params}]
|
||||
(ptk/reify ::go-to-viewer
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav :viewer params {:index 0})))))
|
||||
([] (go-to-viewer {}))
|
||||
([{:keys [file-id page-id]}]
|
||||
(ptk/reify ::go-to-viewer
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [{:keys [current-file-id current-page-id]} state
|
||||
params {:file-id (or file-id current-file-id)
|
||||
:page-id (or page-id current-page-id)}]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav :viewer params {:index 0})))))))
|
||||
|
||||
(defn go-to-dashboard
|
||||
[{:keys [team-id] :as project}]
|
||||
(ptk/reify ::go-to-viewer
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav :dashboard-projects {:team-id team-id})))))
|
||||
([] (go-to-dashboard nil))
|
||||
([{:keys [team-id]}]
|
||||
(ptk/reify ::go-to-dashboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [team-id (or team-id (get-in state [:workspace-project :team-id]))]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav :dashboard-projects {:team-id team-id})))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Context Menu
|
||||
@@ -1279,7 +1289,6 @@
|
||||
;; Clipboard
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
|
||||
(defn copy-selected
|
||||
[]
|
||||
(letfn [;; Retrieve all ids of selected shapes with corresponding
|
||||
@@ -1398,12 +1407,22 @@
|
||||
(let [paste-data (wapi/read-from-paste-event event)
|
||||
image-data (wapi/extract-images paste-data)
|
||||
text-data (wapi/extract-text paste-data)
|
||||
decoded-data (and (t/transit? text-data) (t/decode text-data))]
|
||||
decoded-data (and (t/transit? text-data)
|
||||
(t/decode text-data))]
|
||||
(cond
|
||||
(seq image-data) (rx/from (map paste-image image-data))
|
||||
decoded-data (rx/of (paste-shape decoded-data in-viewport?))
|
||||
(string? text-data) (rx/of (paste-text text-data))
|
||||
:else (rx/empty)))
|
||||
(seq image-data)
|
||||
(rx/from (map paste-image image-data))
|
||||
|
||||
(coll? decoded-data)
|
||||
(->> (rx/of decoded-data)
|
||||
(rx/filter #(= :copied-shapes (:type %)))
|
||||
(rx/map #(paste-shape % in-viewport?)))
|
||||
|
||||
(string? text-data)
|
||||
(rx/of (paste-text text-data))
|
||||
|
||||
:else
|
||||
(rx/empty)))
|
||||
(catch :default err
|
||||
(js/console.error "Clipboard error:" err))))))
|
||||
|
||||
@@ -1564,7 +1583,7 @@
|
||||
(watch [_ state stream]
|
||||
(let [id (uuid/next)
|
||||
{:keys [x y]} @ms/mouse-position
|
||||
width (min (* 7 (count text)) 700)
|
||||
width (max 8 (min (* 7 (count text)) 700))
|
||||
height 16
|
||||
page-id (:current-page-id state)
|
||||
frame-id (-> (dwc/lookup-page-objects state page-id)
|
||||
@@ -1738,80 +1757,3 @@
|
||||
(d/export dwg/unmask-group)
|
||||
(d/export dwg/group-selected)
|
||||
(d/export dwg/ungroup-selected)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Shortcuts
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Shortcuts impl https://github.com/ccampbell/mousetrap
|
||||
|
||||
(defn esc-pressed []
|
||||
(ptk/reify :esc-pressed
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
;; Not interrupt when we're editing a path
|
||||
(let [edition-id (or (get-in state [:workspace-drawing :object :id])
|
||||
(get-in state [:workspace-local :edition]))
|
||||
path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])]
|
||||
(if-not (= :draw path-edit-mode)
|
||||
(rx/of :interrupt (deselect-all true))
|
||||
(rx/empty))))))
|
||||
|
||||
(defn c-mod
|
||||
"Adds the control/command modifier to a shortcuts depending on the
|
||||
operating system for the user"
|
||||
[shortcut]
|
||||
(if (cfg/check-platform? :macos)
|
||||
(str "command+" shortcut)
|
||||
(str "ctrl+" shortcut)))
|
||||
|
||||
(def shortcuts
|
||||
{(c-mod "i") #(st/emit! (toggle-layout-flags :assets))
|
||||
(c-mod "l") #(st/emit! (toggle-layout-flags :sitemap :layers))
|
||||
(c-mod "shift+r") #(st/emit! (toggle-layout-flags :rules))
|
||||
(c-mod "a") #(st/emit! (select-all))
|
||||
(c-mod "p") #(st/emit! (toggle-layout-flags :colorpalette))
|
||||
(c-mod "'") #(st/emit! (toggle-layout-flags :display-grid))
|
||||
(c-mod "shift+'") #(st/emit! (toggle-layout-flags :snap-grid))
|
||||
"+" #(st/emit! (increase-zoom nil))
|
||||
"-" #(st/emit! (decrease-zoom nil))
|
||||
(c-mod "g") #(st/emit! group-selected)
|
||||
"shift+g" #(st/emit! ungroup-selected)
|
||||
(c-mod "m") #(st/emit! mask-group)
|
||||
"shift+m" #(st/emit! unmask-group)
|
||||
(c-mod "k") #(st/emit! dwl/add-component)
|
||||
"shift+0" #(st/emit! reset-zoom)
|
||||
"shift+1" #(st/emit! zoom-to-fit-all)
|
||||
"shift+2" #(st/emit! zoom-to-selected-shape)
|
||||
(c-mod "d") #(st/emit! duplicate-selected)
|
||||
(c-mod "z") #(st/emit! dwc/undo)
|
||||
(c-mod "shift+z") #(st/emit! dwc/redo)
|
||||
(c-mod "y") #(st/emit! dwc/redo)
|
||||
(c-mod "q") #(st/emit! dwc/reinitialize-undo)
|
||||
"a" #(st/emit! (dwd/select-for-drawing :frame))
|
||||
"r" #(st/emit! (dwd/select-for-drawing :rect))
|
||||
"e" #(st/emit! (dwd/select-for-drawing :circle))
|
||||
"t" #(st/emit! dwtxt/start-edit-if-selected
|
||||
(dwd/select-for-drawing :text))
|
||||
"p" #(st/emit! (dwd/select-for-drawing :path))
|
||||
"k" (fn [event]
|
||||
(let [image-upload (dom/get-element "image-upload")]
|
||||
(dom/click image-upload)))
|
||||
(c-mod "c") #(st/emit! (copy-selected))
|
||||
(c-mod "x") #(st/emit! (copy-selected) delete-selected)
|
||||
"escape" #(st/emit! (esc-pressed))
|
||||
"del" #(st/emit! delete-selected)
|
||||
"backspace" #(st/emit! delete-selected)
|
||||
(c-mod "up") #(st/emit! (vertical-order-selected :up))
|
||||
(c-mod "down") #(st/emit! (vertical-order-selected :down))
|
||||
(c-mod "shift+up") #(st/emit! (vertical-order-selected :top))
|
||||
(c-mod "shift+down") #(st/emit! (vertical-order-selected :bottom))
|
||||
"shift+up" #(st/emit! (dwt/move-selected :up true))
|
||||
"shift+down" #(st/emit! (dwt/move-selected :down true))
|
||||
"shift+right" #(st/emit! (dwt/move-selected :right true))
|
||||
"shift+left" #(st/emit! (dwt/move-selected :left true))
|
||||
"up" #(st/emit! (dwt/move-selected :up false))
|
||||
"down" #(st/emit! (dwt/move-selected :down false))
|
||||
"right" #(st/emit! (dwt/move-selected :right false))
|
||||
"left" #(st/emit! (dwt/move-selected :left false))
|
||||
"i" #(st/emit! (mdc/picker-for-selected-shape ))})
|
||||
|
||||
@@ -536,20 +536,26 @@
|
||||
|
||||
(defn get-shape-layer-position
|
||||
[objects selected attrs]
|
||||
(cond
|
||||
(= :frame (:type attrs))
|
||||
|
||||
(if (= :frame (:type attrs))
|
||||
;; Frames are alwasy positioned on the root frame
|
||||
[uuid/zero uuid/zero nil]
|
||||
|
||||
(empty? selected)
|
||||
;; Calculate the frame over which we're drawing
|
||||
(let [position @ms/mouse-position
|
||||
frame-id (:frame-id attrs (cp/frame-id-by-position objects position))]
|
||||
[frame-id frame-id nil])
|
||||
frame-id (:frame-id attrs (cp/frame-id-by-position objects position))
|
||||
shape (when-not (empty? selected)
|
||||
(cp/get-base-shape objects selected))]
|
||||
|
||||
:else
|
||||
(let [shape (cp/get-base-shape objects selected)
|
||||
index (cp/position-on-parent (:id shape) objects)
|
||||
{:keys [frame-id parent-id]} shape]
|
||||
[frame-id parent-id (inc index)])))
|
||||
;; When no shapes has been selected or we're over a different frame
|
||||
;; we add it as the latest shape of that frame
|
||||
(if (or (not shape) (not= (:frame-id shape) frame-id))
|
||||
[frame-id frame-id nil]
|
||||
|
||||
;; Otherwise, we add it to next to the selected shape
|
||||
(let [index (cp/position-on-parent (:id shape) objects)
|
||||
{:keys [frame-id parent-id]} shape]
|
||||
[frame-id parent-id (inc index)])))))
|
||||
|
||||
(defn add-shape-changes
|
||||
[page-id objects selected attrs]
|
||||
@@ -562,6 +568,9 @@
|
||||
|
||||
shape (merge default-attrs shape)
|
||||
|
||||
not-frame? #(not (= :frame (get-in objects [% :type])))
|
||||
selected (into #{} (filter not-frame?) selected)
|
||||
|
||||
[frame-id parent-id index] (get-shape-layer-position objects selected attrs)
|
||||
|
||||
redo-changes [{:type :add-obj
|
||||
|
||||
@@ -21,12 +21,17 @@
|
||||
[app.main.data.workspace.drawing.common :as common]
|
||||
[app.common.math :as mth]))
|
||||
|
||||
(defn truncate-zero [num default]
|
||||
(if (mth/almost-zero? num) default num))
|
||||
|
||||
(defn resize-shape [{:keys [x y width height transform transform-inverse] :as shape} point lock?]
|
||||
(let [;; The new shape behaves like a resize on the bottom-right corner
|
||||
initial (gpt/point (+ x width) (+ y height))
|
||||
shapev (gpt/point width height)
|
||||
deltav (gpt/to-vec initial point)
|
||||
scalev (gpt/divide (gpt/add shapev deltav) shapev)
|
||||
scalev (-> (gpt/divide (gpt/add shapev deltav) shapev)
|
||||
(update :x truncate-zero 1)
|
||||
(update :y truncate-zero 1))
|
||||
scalev (if lock?
|
||||
(let [v (max (:x scalev) (:y scalev))]
|
||||
(gpt/point v v))
|
||||
|
||||
@@ -692,8 +692,8 @@
|
||||
point (-> content (get (if (= prefix :c1) (dec index) index)) (ugp/command->point))
|
||||
handler (-> content (get index) (ugp/get-handler prefix))
|
||||
|
||||
current-distance (gpt/distance (ugp/opposite-handler point handler) opposite-handler)
|
||||
match-opposite? (mth/almost-zero? current-distance)]
|
||||
current-distance (when opposite-handler (gpt/distance (ugp/opposite-handler point handler) opposite-handler))
|
||||
match-opposite? (and opposite-handler (mth/almost-zero? current-distance))]
|
||||
|
||||
(drag-stream
|
||||
(rx/concat
|
||||
|
||||
@@ -65,6 +65,13 @@
|
||||
|
||||
(declare sync-file)
|
||||
|
||||
(defn set-assets-box-open
|
||||
[file-id box open?]
|
||||
(ptk/reify ::set-assets-box-open
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :assets-files-open file-id box] open?))))
|
||||
|
||||
(defn default-color-name [color]
|
||||
(or (:color color)
|
||||
(case (get-in color [:gradient :type])
|
||||
@@ -230,7 +237,8 @@
|
||||
(let [file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)
|
||||
objects (dwc/lookup-page-objects state page-id)
|
||||
selected (get-in state [:workspace-local :selected])]
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
selected (cp/clean-loops objects selected)]
|
||||
(let [[group rchanges uchanges]
|
||||
(dwlh/generate-add-component selected objects page-id file-id)]
|
||||
(when-not (empty? rchanges)
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
|
||||
(ns app.main.data.workspace.persistence
|
||||
(:require
|
||||
[cuerdas.core :as str]
|
||||
[app.util.http :as http]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.media :as cm]
|
||||
[app.common.pages :as cp]
|
||||
@@ -21,21 +20,22 @@
|
||||
[app.main.data.media :as di]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.svg-upload :as svg]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.svg-upload :as svg]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.util.avatars :as avatars]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[app.util.avatars :as avatars]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.core :as ptk]
|
||||
[app.main.store :as st]))
|
||||
[cuerdas.core :as str]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(declare persist-changes)
|
||||
(declare persist-sychronous-changes)
|
||||
@@ -417,24 +417,27 @@
|
||||
(defn- handle-upload-error [on-error stream]
|
||||
(->> stream
|
||||
(rx/catch
|
||||
(fn [error]
|
||||
(cond
|
||||
(= (:code error) :media-type-not-allowed)
|
||||
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
|
||||
(fn on-error [error]
|
||||
(if (ex/ex-info? error)
|
||||
(on-error (ex-data error))
|
||||
(cond
|
||||
(= (:code error) :media-type-not-allowed)
|
||||
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
|
||||
|
||||
(= (:code error) :media-type-mismatch)
|
||||
(rx/of (dm/error (tr "errors.media-type-mismatch")))
|
||||
(= (:code error) :media-too-large)
|
||||
(rx/of (dm/error (tr "errors.media-too-large")))
|
||||
|
||||
(= (:code error) :unable-to-optimize)
|
||||
(rx/of (dm/error (:hint error)))
|
||||
(= (:code error) :media-type-mismatch)
|
||||
(rx/of (dm/error (tr "errors.media-type-mismatch")))
|
||||
|
||||
(fn? on-error)
|
||||
(do
|
||||
(= (:code error) :unable-to-optimize)
|
||||
(rx/of (dm/error (:hint error)))
|
||||
|
||||
(fn? on-error)
|
||||
(on-error error)
|
||||
(rx/empty))
|
||||
|
||||
:else
|
||||
(rx/throw error))))))
|
||||
:else
|
||||
(rx/throw error)))))))
|
||||
|
||||
(defn- upload-uris [file-id local? name uris mtype on-image on-svg]
|
||||
(letfn [(svg-url? [url]
|
||||
@@ -490,7 +493,7 @@
|
||||
(rx/map #(assoc (first %) :name (.-name (second %))))
|
||||
(rx/do on-svg)))))
|
||||
|
||||
(defn upload-media-objects
|
||||
(defn- upload-media-objects
|
||||
[{:keys [file-id local? data name uris mtype svg-as-images] :as params}]
|
||||
(us/assert ::upload-media-objects params)
|
||||
(ptk/reify ::upload-media-objects
|
||||
@@ -499,7 +502,6 @@
|
||||
(let [{:keys [on-image on-svg on-error]
|
||||
:or {on-image identity
|
||||
on-svg identity}} (meta params)]
|
||||
|
||||
(rx/concat
|
||||
(rx/of (dm/show {:content (tr "media.loading")
|
||||
:type :info
|
||||
@@ -515,7 +517,8 @@
|
||||
(handle-upload-error on-error)
|
||||
(rx/finalize (st/emitf (dm/hide-tag :media-loading)))))))))
|
||||
|
||||
(defn upload-media-asset [params]
|
||||
(defn upload-media-asset
|
||||
[params]
|
||||
(let [params (-> params
|
||||
(assoc :svg-as-images true)
|
||||
(assoc :local? false)
|
||||
@@ -525,13 +528,12 @@
|
||||
(defn upload-media-workspace
|
||||
[params position]
|
||||
(let [{:keys [x y]} position
|
||||
params (-> params
|
||||
(assoc :local? true)
|
||||
(with-meta
|
||||
{:on-image
|
||||
#(st/emit! (dwc/image-uploaded % x y))
|
||||
:on-svg
|
||||
#(st/emit! (svg/svg-uploaded % x y))}))]
|
||||
mdata {:on-image #(st/emit! (dwc/image-uploaded % x y))
|
||||
:on-svg #(st/emit! (svg/svg-uploaded % x y))}
|
||||
|
||||
params (-> (assoc params :local? true)
|
||||
(with-meta mdata))]
|
||||
|
||||
(upload-media-objects params)))
|
||||
|
||||
|
||||
|
||||
@@ -105,6 +105,14 @@
|
||||
objects (dwc/lookup-page-objects state page-id)]
|
||||
(rx/of (dwc/expand-all-parents [id] objects)))))))
|
||||
|
||||
(defn deselect-shape
|
||||
[id]
|
||||
(us/verify ::us/uuid id)
|
||||
(ptk/reify ::select-shape
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :selected] disj id))))
|
||||
|
||||
(defn shift-select-shapes
|
||||
([id]
|
||||
(ptk/reify ::shift-select-shapes
|
||||
|
||||
254
frontend/src/app/main/data/workspace/shortcuts.cljs
Normal file
254
frontend/src/app/main/data/workspace/shortcuts.cljs
Normal file
@@ -0,0 +1,254 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.workspace.shortcuts
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.main.data.colors :as mdc]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.texts :as dwtxt]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
;; \u2318P
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Shortcuts
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Shortcuts impl https://github.com/ccampbell/mousetrap
|
||||
|
||||
(defn esc-pressed []
|
||||
(ptk/reify :esc-pressed
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
;; Not interrupt when we're editing a path
|
||||
(let [edition-id (or (get-in state [:workspace-drawing :object :id])
|
||||
(get-in state [:workspace-local :edition]))
|
||||
path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])]
|
||||
(if-not (= :draw path-edit-mode)
|
||||
(rx/of :interrupt (dw/deselect-all true))
|
||||
(rx/empty))))))
|
||||
|
||||
(def shortcuts
|
||||
{:toggle-layers {:tooltip (ds/meta "L")
|
||||
:command (ds/c-mod "l")
|
||||
:fn #(st/emit! (dw/go-to-layout :layers))}
|
||||
|
||||
:toggle-assets {:tooltip (ds/meta "I")
|
||||
:command (ds/c-mod "i")
|
||||
:fn #(st/emit! (dw/go-to-layout :assets))}
|
||||
|
||||
:toggle-history {:tooltip (ds/meta "H")
|
||||
:command (ds/c-mod "h")
|
||||
:fn #(st/emit! (dw/go-to-layout :document-history))}
|
||||
|
||||
:toggle-palette {:tooltip (ds/meta "P")
|
||||
:command (ds/c-mod "p")
|
||||
:fn #(st/emit! (dw/toggle-layout-flags :colorpalette))}
|
||||
|
||||
:toggle-rules {:tooltip (ds/meta-shift "R")
|
||||
:command (ds/c-mod "shift+r")
|
||||
:fn #(st/emit! (dw/toggle-layout-flags :rules))}
|
||||
|
||||
:select-all {:tooltip (ds/meta "A")
|
||||
:command (ds/c-mod "a")
|
||||
:fn #(st/emit! (dw/select-all))}
|
||||
|
||||
:toggle-grid {:tooltip (ds/meta "'")
|
||||
:command (ds/c-mod "'")
|
||||
:fn #(st/emit! (dw/toggle-layout-flags :display-grid))}
|
||||
|
||||
:toggle-snap-grid {:tooltip (ds/meta-shift "'")
|
||||
:command (ds/c-mod "shift+'")
|
||||
:fn #(st/emit! (dw/toggle-layout-flags :snap-grid))}
|
||||
|
||||
:toggle-alignment {:tooltip (ds/meta "\\")
|
||||
:command (ds/c-mod "\\")
|
||||
:fn #(st/emit! (dw/toggle-layout-flags :dynamic-alignment))}
|
||||
|
||||
:increase-zoom {:tooltip "+"
|
||||
:command "+"
|
||||
:fn #(st/emit! (dw/increase-zoom nil))}
|
||||
|
||||
:decrease-zoom {:tooltip "-"
|
||||
:command "-"
|
||||
:fn #(st/emit! (dw/decrease-zoom nil))}
|
||||
|
||||
:group {:tooltip (ds/meta "G")
|
||||
:command (ds/c-mod "g")
|
||||
:fn #(st/emit! dw/group-selected)}
|
||||
|
||||
:ungroup {:tooltip (ds/shift "G")
|
||||
:command "shift+g"
|
||||
:fn #(st/emit! dw/ungroup-selected)}
|
||||
|
||||
:mask {:tooltip (ds/meta "M")
|
||||
:command (ds/c-mod "m")
|
||||
:fn #(st/emit! dw/mask-group)}
|
||||
|
||||
:unmask {:tooltip (ds/meta-shift "M")
|
||||
:command (ds/c-mod "shift+m")
|
||||
:fn #(st/emit! dw/unmask-group)}
|
||||
|
||||
:create-component {:tooltip (ds/meta "K")
|
||||
:command (ds/c-mod "k")
|
||||
:fn #(st/emit! dwl/add-component)}
|
||||
|
||||
:reset-zoom {:tooltip (ds/shift "0")
|
||||
:command "shift+0"
|
||||
:fn #(st/emit! dw/reset-zoom)}
|
||||
|
||||
:fit-all {:tooltip (ds/shift "1")
|
||||
:command "shift+1"
|
||||
:fn #(st/emit! dw/zoom-to-fit-all)}
|
||||
|
||||
:zoom-selected {:tooltip (ds/shift "2")
|
||||
:command "shift+2"
|
||||
:fn #(st/emit! dw/zoom-to-selected-shape)}
|
||||
|
||||
:duplicate {:tooltip (ds/meta "D")
|
||||
:command (ds/c-mod "d")
|
||||
:fn #(st/emit! dw/duplicate-selected)}
|
||||
|
||||
:undo {:tooltip (ds/meta "Z")
|
||||
:command (ds/c-mod "z")
|
||||
:fn #(st/emit! dwc/undo)}
|
||||
|
||||
:redo {:tooltip (ds/meta "Y")
|
||||
:command [(ds/c-mod "shift+z") (ds/c-mod "y")]
|
||||
:fn #(st/emit! dwc/redo)}
|
||||
|
||||
:clear-undo {:tooltip (ds/meta "Q")
|
||||
:command (ds/c-mod "q")
|
||||
:fn #(st/emit! dwc/reinitialize-undo)}
|
||||
|
||||
:draw-frame {:tooltip "A"
|
||||
:command "a"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :frame))}
|
||||
|
||||
:draw-rect {:tooltip "R"
|
||||
:command "r"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :rect))}
|
||||
|
||||
:draw-ellipse {:tooltip "E"
|
||||
:command "e"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :circle))}
|
||||
|
||||
:draw-text {:tooltip "T"
|
||||
:command "t"
|
||||
:fn #(st/emit! dwtxt/start-edit-if-selected
|
||||
(dwd/select-for-drawing :text))}
|
||||
|
||||
:draw-path {:tooltip "P"
|
||||
:command "p"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :path))}
|
||||
|
||||
:draw-curve {:tooltip (ds/shift "C")
|
||||
:command "shift+c"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :curve))}
|
||||
|
||||
:add-comment {:tooltip "C"
|
||||
:command "c"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :comments))}
|
||||
|
||||
:insert-image {:tooltip "K"
|
||||
:command "k"
|
||||
:fn #(-> "image-upload" dom/get-element dom/click)}
|
||||
|
||||
:copy {:tooltip (ds/meta "C")
|
||||
:command (ds/c-mod "c")
|
||||
:fn #(st/emit! (dw/copy-selected))}
|
||||
|
||||
:cut {:tooltip (ds/meta "X")
|
||||
:command (ds/c-mod "x")
|
||||
:fn #(st/emit! (dw/copy-selected) dw/delete-selected)}
|
||||
|
||||
:paste {:tooltip (ds/meta "V")
|
||||
:disabled true
|
||||
:command (ds/c-mod "v")}
|
||||
|
||||
:delete {:tooltip (ds/supr)
|
||||
:command ["del" "backspace"]
|
||||
:fn #(st/emit! dw/delete-selected)}
|
||||
|
||||
:bring-forward {:tooltip (ds/meta ds/up-arrow)
|
||||
:command (ds/c-mod "up")
|
||||
:fn #(st/emit! (dw/vertical-order-selected :up))}
|
||||
|
||||
:bring-backward {:tooltip (ds/meta ds/down-arrow)
|
||||
:command (ds/c-mod "down")
|
||||
:fn #(st/emit! (dw/vertical-order-selected :down))}
|
||||
|
||||
:bring-front {:tooltip (ds/meta-shift ds/up-arrow)
|
||||
:command (ds/c-mod "shift+up")
|
||||
:fn #(st/emit! (dw/vertical-order-selected :top))}
|
||||
|
||||
:bring-back {:tooltip (ds/meta-shift ds/down-arrow)
|
||||
:command (ds/c-mod "shift+down")
|
||||
:fn #(st/emit! (dw/vertical-order-selected :bottom))}
|
||||
|
||||
:move-fast-up {:tooltip (ds/shift ds/up-arrow)
|
||||
:command "shift+up"
|
||||
:fn #(st/emit! (dwt/move-selected :up true))}
|
||||
|
||||
:move-fast-down {:tooltip (ds/shift ds/down-arrow)
|
||||
:command "shift+down"
|
||||
:fn #(st/emit! (dwt/move-selected :down true))}
|
||||
|
||||
:move-fast-right {:tooltip (ds/shift ds/right-arrow)
|
||||
:command "shift+right"
|
||||
:fn #(st/emit! (dwt/move-selected :right true))}
|
||||
|
||||
:move-fast-left {:tooltip (ds/shift ds/left-arrow)
|
||||
:command "shift+left"
|
||||
:fn #(st/emit! (dwt/move-selected :left true))}
|
||||
|
||||
:move-unit-up {:tooltip ds/up-arrow
|
||||
:command "up"
|
||||
:fn #(st/emit! (dwt/move-selected :up false))}
|
||||
|
||||
:move-unit-down {:tooltip ds/down-arrow
|
||||
:command "down"
|
||||
:fn #(st/emit! (dwt/move-selected :down false))}
|
||||
|
||||
:move-unit-left {:tooltip ds/right-arrow
|
||||
:command "right"
|
||||
:fn #(st/emit! (dwt/move-selected :right false))}
|
||||
|
||||
:move-unit-right {:tooltip ds/left-arrow
|
||||
:command "left"
|
||||
:fn #(st/emit! (dwt/move-selected :left false))}
|
||||
|
||||
:open-color-picker {:tooltip "I"
|
||||
:command "i"
|
||||
:fn #(st/emit! (mdc/picker-for-selected-shape ))}
|
||||
|
||||
:open-viewer {:tooltip "G V"
|
||||
:command "g v"
|
||||
:fn #(st/emit! (dw/go-to-viewer))}
|
||||
|
||||
:open-dashboard {:tooltip "G D"
|
||||
:command "g d"
|
||||
:fn #(st/emit! (dw/go-to-dashboard))}
|
||||
|
||||
:escape {:tooltip (ds/esc)
|
||||
:command "escape"
|
||||
:fn #(st/emit! (esc-pressed))}})
|
||||
|
||||
(defn get-tooltip [shortcut]
|
||||
(assert (contains? shortcuts shortcut) (str shortcut))
|
||||
(get-in shortcuts [shortcut :tooltip]))
|
||||
@@ -95,7 +95,14 @@
|
||||
;; Resize vector
|
||||
scalev (gpt/divide (gpt/add shapev deltav) shapev)
|
||||
|
||||
scalev (if lock? (let [v (max (:x scalev) (:y scalev))] (gpt/point v v)) scalev)
|
||||
scalev (if lock?
|
||||
(let [v (cond
|
||||
(#{:right :left} handler) (:x scalev)
|
||||
(#{:top :bottom} handler) (:y scalev)
|
||||
:else (max (:x scalev) (:y scalev)))]
|
||||
(gpt/point v v))
|
||||
|
||||
scalev)
|
||||
|
||||
shape-transform (:transform shape (gmt/matrix))
|
||||
shape-transform-inverse (:transform-inverse shape (gmt/matrix))
|
||||
@@ -294,18 +301,20 @@
|
||||
|
||||
snap-delta (->> position
|
||||
(rx/switch-map #(snap/closest-snap-move page-id shapes objects layout zoom %)))]
|
||||
(rx/concat
|
||||
(->> snap-delta
|
||||
(rx/with-latest vector position)
|
||||
(rx/map (fn [[delta pos]] (-> (gpt/add pos delta) (gpt/round 0))))
|
||||
(rx/map gmt/translate-matrix)
|
||||
(rx/map #(fn [state] (assoc-in state [:workspace-local :modifiers] {:displacement %}))))
|
||||
(if (empty? shapes)
|
||||
(rx/empty)
|
||||
(rx/concat
|
||||
(->> snap-delta
|
||||
(rx/with-latest vector position)
|
||||
(rx/map (fn [[delta pos]] (-> (gpt/add pos delta) (gpt/round 0))))
|
||||
(rx/map gmt/translate-matrix)
|
||||
(rx/map #(fn [state] (assoc-in state [:workspace-local :modifiers] {:displacement %}))))
|
||||
|
||||
(rx/of (set-modifiers ids)
|
||||
(apply-modifiers ids)
|
||||
(calculate-frame-for-move ids)
|
||||
(fn [state] (update state :workspace-local dissoc :modifiers))
|
||||
finish-transform)))))))
|
||||
(rx/of (set-modifiers ids)
|
||||
(apply-modifiers ids)
|
||||
(calculate-frame-for-move ids)
|
||||
(fn [state] (update state :workspace-local dissoc :modifiers))
|
||||
finish-transform))))))))
|
||||
|
||||
(defn- get-displacement-with-grid
|
||||
"Retrieve the correct displacement delta point for the
|
||||
@@ -369,15 +378,17 @@
|
||||
(->> move-events
|
||||
(rx/take-until stopper)
|
||||
(rx/scan #(gpt/add %1 mov-vec) (gpt/point 0 0))
|
||||
(rx/map #(set-modifiers selected {:displacement (gmt/translate-matrix %)})))
|
||||
(rx/map gmt/translate-matrix)
|
||||
(rx/map #(fn [state] (assoc-in state [:workspace-local :modifiers] {:displacement %}))))
|
||||
(rx/of (move-selected direction shift?)))
|
||||
|
||||
(rx/of (apply-modifiers selected)
|
||||
(rx/of (set-modifiers selected)
|
||||
(apply-modifiers selected)
|
||||
(calculate-frame-for-move selected)
|
||||
(fn [state] (-> state
|
||||
(update :workspace-local dissoc :current-move-selected))))
|
||||
(->>
|
||||
(rx/timer 100)
|
||||
(rx/map (fn [] finish-transform)))))
|
||||
(update :workspace-local dissoc :modifiers)
|
||||
(update :workspace-local dissoc :current-move-selected)))
|
||||
finish-transform)))
|
||||
(rx/empty))))))
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
(:require
|
||||
[beicon.core :as rx]
|
||||
[okulary.core :as l]
|
||||
[app.common.data :as d]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.constants :as c]
|
||||
@@ -109,7 +110,8 @@
|
||||
(def workspace-file-colors
|
||||
(l/derived (fn [state]
|
||||
(when-let [file (:workspace-file state)]
|
||||
(get-in file [:data :colors])))
|
||||
(->> (get-in file [:data :colors])
|
||||
(d/mapm #(assoc %2 :file-id (:id file))))))
|
||||
st/state))
|
||||
|
||||
(def workspace-recent-colors
|
||||
|
||||
@@ -61,10 +61,10 @@
|
||||
[["/auth"
|
||||
["/login" :auth-login]
|
||||
["/register" :auth-register]
|
||||
["/register/success" :auth-register-success]
|
||||
["/recovery/request" :auth-recovery-request]
|
||||
["/recovery" :auth-recovery]
|
||||
["/verify-token" :auth-verify-token]
|
||||
["/goodbye" :auth-goodbye]]
|
||||
["/verify-token" :auth-verify-token]]
|
||||
|
||||
["/settings"
|
||||
["/profile" :settings-profile]
|
||||
@@ -111,7 +111,7 @@
|
||||
(case (get-in route [:data :name])
|
||||
(:auth-login
|
||||
:auth-register
|
||||
:auth-goodbye
|
||||
:auth-register-success
|
||||
:auth-recovery-request
|
||||
:auth-recovery)
|
||||
[:& auth {:route route}]
|
||||
@@ -225,17 +225,17 @@
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(dm/show {:content "Unexpected validation error (server side)."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
:type :error})))
|
||||
|
||||
;; Print to the console some debug info.
|
||||
(js/console.group "Server Error")
|
||||
(js/console.info
|
||||
(with-out-str
|
||||
(pprint (dissoc error :explain))))
|
||||
(when-let [explain (:explain error)]
|
||||
(js/console.error explain))
|
||||
(js/console.endGroup "Server Error"))
|
||||
(js/console.group "Validation Error")
|
||||
(ex/ignoring
|
||||
(js/console.info
|
||||
(with-out-str
|
||||
(pprint (dissoc error :explain))))
|
||||
(when-let [explain (:explain error)]
|
||||
(js/console.error explain)))
|
||||
(js/console.groupEnd "Validation Error"))
|
||||
|
||||
;; This is a pure frontend error that can be caused by an active
|
||||
;; assertion (assertion that is preserved on production builds). From
|
||||
@@ -244,8 +244,7 @@
|
||||
[{:keys [data stack message context] :as error}]
|
||||
(ts/schedule
|
||||
(st/emitf (dm/show {:content "Internal error: assertion."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
:type :error})))
|
||||
|
||||
;; Print to the console some debugging info
|
||||
(js/console.group message)
|
||||
@@ -268,8 +267,8 @@
|
||||
(ts/schedule
|
||||
(st/emitf (dm/show
|
||||
{:content "Something wrong has happened (on backend)."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
:type :error})))
|
||||
|
||||
(js/console.group "Internal Server Error:")
|
||||
(js/console.error "hint:" (or (:hint data) (:message data)))
|
||||
(js/console.info
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
[app.main.ui.auth.login :refer [login-page]]
|
||||
[app.main.ui.auth.recovery :refer [recovery-page]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
|
||||
[app.main.ui.auth.register :refer [register-page]]
|
||||
[app.main.ui.auth.register :refer [register-page register-success-page]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.storage :refer [cache]]
|
||||
@@ -29,11 +29,6 @@
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc goodbye-page
|
||||
[{:keys [locale] :as props}]
|
||||
[:div.goodbay
|
||||
[:h1 (t locale "auth.goodbye-title")]])
|
||||
|
||||
(mf/defc auth
|
||||
[{:keys [route] :as props}]
|
||||
(let [section (get-in route [:data :name])
|
||||
@@ -47,9 +42,18 @@
|
||||
|
||||
[:section.auth-content
|
||||
(case section
|
||||
:auth-register [:& register-page {:locale locale :params params}]
|
||||
:auth-login [:& login-page {:locale locale :params params}]
|
||||
:auth-goodbye [:& goodbye-page {:locale locale}]
|
||||
:auth-recovery-request [:& recovery-request-page {:locale locale}]
|
||||
:auth-recovery [:& recovery-page {:locale locale
|
||||
:params (:query-params route)}])]]))
|
||||
:auth-register
|
||||
[:& register-page {:locale locale :params params}]
|
||||
|
||||
:auth-register-success
|
||||
[:& register-success-page {:params params}]
|
||||
|
||||
:auth-login
|
||||
[:& login-page {:locale locale :params params}]
|
||||
|
||||
:auth-recovery-request
|
||||
[:& recovery-request-page {:locale locale}]
|
||||
|
||||
:auth-recovery
|
||||
[:& recovery-page {:locale locale
|
||||
:params (:query-params route)}])]]))
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
[:& fm/submit-button
|
||||
{:label (tr "auth.login-submit")
|
||||
:on-click on-submit}]
|
||||
|
||||
(when cfg/login-with-ldap
|
||||
[:& fm/submit-button
|
||||
{:label (tr "auth.login-with-ldap-submit")
|
||||
|
||||
@@ -20,49 +20,69 @@
|
||||
[app.util.router :as rt]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::recovery-request-form (s/keys :req-un [::email]))
|
||||
|
||||
(defn- on-success
|
||||
[]
|
||||
(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
|
||||
(rt/nav :auth-login)))
|
||||
|
||||
(defn- on-submit
|
||||
[form event]
|
||||
(let [params (with-meta (:clean-data @form)
|
||||
{:on-success on-success})]
|
||||
(st/emit! (uda/request-profile-recovery params))))
|
||||
|
||||
(mf/defc recovery-form
|
||||
[{:keys [locale] :as props}]
|
||||
[]
|
||||
(let [form (fm/use-form :spec ::recovery-request-form
|
||||
:initial {})]
|
||||
:initial {})
|
||||
|
||||
submitted (mf/use-state false)
|
||||
|
||||
on-error
|
||||
(mf/use-callback
|
||||
(fn [{:keys [code] :as error}]
|
||||
(reset! submitted false)
|
||||
(if (= code :profile-not-verified)
|
||||
(rx/of (dm/error (tr "auth.notifications.profile-not-verified")
|
||||
{:timeout nil}))
|
||||
|
||||
(rx/throw error))))
|
||||
|
||||
on-success
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(reset! submitted false)
|
||||
(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
|
||||
(rt/nav :auth-login))))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(reset! submitted true)
|
||||
(->> (with-meta (:clean-data @form)
|
||||
{:on-success on-success
|
||||
:on-error on-error})
|
||||
(uda/request-profile-recovery)
|
||||
(st/emit!))))]
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:form form}
|
||||
[:div.fields-row
|
||||
[:& fm/input {:name :email
|
||||
:label (t locale "auth.email")
|
||||
:label (tr "auth.email")
|
||||
:help-icon i/at
|
||||
:type "text"}]]
|
||||
|
||||
[:& fm/submit-button
|
||||
{:label (t locale "auth.recovery-request-submit")}]]))
|
||||
{:label (tr "auth.recovery-request-submit")}]]))
|
||||
|
||||
|
||||
;; --- Recovery Request Page
|
||||
|
||||
(mf/defc recovery-request-page
|
||||
[{:keys [locale] :as props}]
|
||||
[]
|
||||
[:section.generic-form
|
||||
[:div.form-container
|
||||
[:h1 (t locale "auth.recovery-request-title")]
|
||||
[:div.subtitle (t locale "auth.recovery-request-subtitle")]
|
||||
[:& recovery-form {:locale locale}]
|
||||
[:h1 (tr "auth.recovery-request-title")]
|
||||
[:div.subtitle (tr "auth.recovery-request-subtitle")]
|
||||
[:& recovery-form]
|
||||
|
||||
[:div.links
|
||||
[:div.link-entry
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
|
||||
(t locale "auth.go-back-to-login")]]]]])
|
||||
(tr "auth.go-back-to-login")]]]]])
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.messages :as msgs]
|
||||
[app.main.ui.auth.login :as login]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr t]]
|
||||
[app.util.router :as rt]
|
||||
@@ -32,42 +33,12 @@
|
||||
{:type :warning
|
||||
:content (tr "auth.demo-warning")}])
|
||||
|
||||
(defn- on-error
|
||||
[form error]
|
||||
(case (:code error)
|
||||
:registration-disabled
|
||||
(st/emit! (dm/error (tr "errors.registration-disabled")))
|
||||
|
||||
:email-already-exists
|
||||
(swap! form assoc-in [:errors :email]
|
||||
{:message "errors.email-already-exists"})
|
||||
|
||||
(st/emit! (dm/error (tr "errors.unexpected-error")))))
|
||||
|
||||
(defn- on-success
|
||||
[form data]
|
||||
(if (and (:is-active data) (:claims data))
|
||||
(let [message (tr "auth.notifications.team-invitation-accepted")]
|
||||
(st/emit! (rt/nav :dashboard-projects {:team-id (get-in data [:claims :team-id])})
|
||||
du/fetch-profile
|
||||
(dm/success message)))
|
||||
(let [message (tr "notifications.validation-email-sent" (:email data))]
|
||||
(st/emit! (rt/nav :auth-login)
|
||||
(dm/success message)))))
|
||||
|
||||
(defn- validate
|
||||
[data]
|
||||
(let [password (:password data)]
|
||||
(when (> 8 (count password))
|
||||
{:password {:message "errors.password-too-short"}})))
|
||||
|
||||
(defn- on-submit
|
||||
[form event]
|
||||
(let [data (with-meta (:clean-data @form)
|
||||
{:on-error (partial on-error form)
|
||||
:on-success (partial on-success form)})]
|
||||
(st/emit! (da/register data))))
|
||||
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
(s/def ::email ::us/email)
|
||||
@@ -80,59 +51,125 @@
|
||||
:opt-un [::token]))
|
||||
|
||||
(mf/defc register-form
|
||||
[{:keys [locale params] :as props}]
|
||||
[{:keys [params] :as props}]
|
||||
(let [initial (mf/use-memo (mf/deps params) (constantly params))
|
||||
form (fm/use-form :spec ::register-form
|
||||
:validators [validate]
|
||||
:initial initial)]
|
||||
:initial initial)
|
||||
submitted? (mf/use-state false)
|
||||
|
||||
on-error
|
||||
(mf/use-callback
|
||||
(fn [form error]
|
||||
(reset! submitted? false)
|
||||
(case (:code error)
|
||||
:registration-disabled
|
||||
(st/emit! (dm/error (tr "errors.registration-disabled")))
|
||||
|
||||
:email-already-exists
|
||||
(swap! form assoc-in [:errors :email]
|
||||
{:message "errors.email-already-exists"})
|
||||
|
||||
(st/emit! (dm/error (tr "errors.unexpected-error"))))))
|
||||
|
||||
on-success
|
||||
(mf/use-callback
|
||||
(fn [form data]
|
||||
(reset! submitted? false)
|
||||
(if (and (:is-active data) (:claims data))
|
||||
(let [message (tr "auth.notifications.team-invitation-accepted")]
|
||||
(st/emit! (rt/nav :dashboard-projects {:team-id (get-in data [:claims :team-id])})
|
||||
du/fetch-profile
|
||||
(dm/success message)))
|
||||
(st/emit! (rt/nav :auth-register-success {} {:email (:email data)})))))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(fn [form event]
|
||||
(reset! submitted? true)
|
||||
(let [data (with-meta (:clean-data @form)
|
||||
{:on-error (partial on-error form)
|
||||
:on-success (partial on-success form)})]
|
||||
(st/emit! (da/register data)))))]
|
||||
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:form form}
|
||||
[:div.fields-row
|
||||
[:& fm/input {:name :fullname
|
||||
:tab-index "1"
|
||||
:label (t locale "auth.fullname")
|
||||
:label (tr "auth.fullname")
|
||||
:type "text"}]]
|
||||
[:div.fields-row
|
||||
[:& fm/input {:type "email"
|
||||
:name :email
|
||||
:tab-index "2"
|
||||
:help-icon i/at
|
||||
:label (t locale "auth.email")}]]
|
||||
:label (tr "auth.email")}]]
|
||||
[:div.fields-row
|
||||
[:& fm/input {:name :password
|
||||
:tab-index "3"
|
||||
:hint (t locale "auth.password-length-hint")
|
||||
:label (t locale "auth.password")
|
||||
:hint (tr "auth.password-length-hint")
|
||||
:label (tr "auth.password")
|
||||
:type "password"}]]
|
||||
|
||||
[:& fm/submit-button
|
||||
{:label (t locale "auth.register-submit")}]]))
|
||||
{:label (tr "auth.register-submit")
|
||||
:disabled @submitted?
|
||||
}]]))
|
||||
|
||||
;; --- Register Page
|
||||
|
||||
(mf/defc register-page
|
||||
[{:keys [locale params] :as props}]
|
||||
(mf/defc register-success-page
|
||||
[{:keys [params] :as props}]
|
||||
[:div.form-container
|
||||
[:h1 (t locale "auth.register-title")]
|
||||
[:div.subtitle (t locale "auth.register-subtitle")]
|
||||
[:div.notification-icon i/icon-verify]
|
||||
[:div.notification-text (tr "auth.verification-email-sent")]
|
||||
[:div.notification-text-email (:email params "")]
|
||||
[:div.notification-text (tr "auth.check-your-email")]])
|
||||
|
||||
|
||||
(mf/defc register-page
|
||||
[{:keys [params] :as props}]
|
||||
[:div.form-container
|
||||
[:h1 (tr "auth.register-title")]
|
||||
[:div.subtitle (tr "auth.register-subtitle")]
|
||||
|
||||
(when cfg/demo-warning
|
||||
[:& demo-warning])
|
||||
|
||||
[:& register-form {:locale locale
|
||||
:params params}]
|
||||
[:& register-form {:params params}]
|
||||
|
||||
[:div.links
|
||||
[:div.link-entry
|
||||
[:span (t locale "auth.already-have-account") " "]
|
||||
[:span (tr "auth.already-have-account") " "]
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth-login))
|
||||
:tab-index "4"}
|
||||
(t locale "auth.login-here")]]
|
||||
(tr "auth.login-here")]]
|
||||
|
||||
(when cfg/allow-demo-users
|
||||
[:div.link-entry
|
||||
[:span (t locale "auth.create-demo-profile") " "]
|
||||
[:span (tr "auth.create-demo-profile") " "]
|
||||
[:a {:on-click #(st/emit! da/create-demo-profile)
|
||||
:tab-index "5"}
|
||||
(t locale "auth.create-demo-account")]])]])
|
||||
(tr "auth.create-demo-account")]])]
|
||||
|
||||
(when cfg/google-client-id
|
||||
[:a.btn-ocean.btn-large.btn-google-auth
|
||||
{:on-click login/login-with-google}
|
||||
"Login with Google"])
|
||||
|
||||
(when cfg/gitlab-client-id
|
||||
[:a.btn-ocean.btn-large.btn-gitlab-auth
|
||||
{:on-click login/login-with-gitlab}
|
||||
[:img.logo
|
||||
{:src "/images/icons/brand-gitlab.svg"}]
|
||||
(tr "auth.login-with-gitlab-submit")])
|
||||
|
||||
(when cfg/github-client-id
|
||||
[:a.btn-ocean.btn-large.btn-github-auth
|
||||
{:on-click login/login-with-github}
|
||||
[:img.logo
|
||||
{:src "/images/icons/brand-github.svg"}]
|
||||
(tr "auth.login-with-github-submit")])])
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) 2020-2021 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.auth.verify-token
|
||||
(:require
|
||||
@@ -21,9 +21,9 @@
|
||||
[app.main.ui.auth.register :refer [register-page]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.storage :refer [cache]]
|
||||
[app.util.i18n :as i18n :refer [tr t]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [cache]]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
@@ -35,7 +35,7 @@
|
||||
[data]
|
||||
(let [msg (tr "dashboard.notifications.email-verified-successfully")]
|
||||
(ts/schedule 100 #(st/emit! (dm/success msg)))
|
||||
(st/emit! (rt/nav :auth-login))))
|
||||
(st/emit! (da/login-from-token data))))
|
||||
|
||||
(defmethod handle-token :change-email
|
||||
[data]
|
||||
|
||||
@@ -122,12 +122,12 @@
|
||||
i/arrow-slide]]]))
|
||||
|
||||
(mf/defc submit-button
|
||||
[{:keys [label form on-click] :as props}]
|
||||
[{:keys [label form on-click disabled] :as props}]
|
||||
(let [form (or form (mf/use-ctx form-ctx))]
|
||||
[:input.btn-primary.btn-large
|
||||
{:name "submit"
|
||||
:class (when-not (:valid @form) "btn-disabled")
|
||||
:disabled (not (:valid @form))
|
||||
:disabled (or (not (:valid @form)) (true? disabled))
|
||||
:on-click on-click
|
||||
:value label
|
||||
:type "submit"}]))
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.data.viewer.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.fullscreen :as fs]
|
||||
@@ -75,7 +76,7 @@
|
||||
(let [on-mouse-wheel
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(when (kbd/ctrl? event)
|
||||
(when (or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(dom/prevent-default event)
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
delta (+ (.-deltaY ^js event)
|
||||
@@ -94,7 +95,7 @@
|
||||
(events/unlistenByKey key1))))]
|
||||
|
||||
(mf/use-effect on-mount)
|
||||
(hooks/use-shortcuts dv/shortcuts)
|
||||
(hooks/use-shortcuts sc/shortcuts)
|
||||
|
||||
[:& fs/fullscreen-wrapper {}
|
||||
[:div.handoff-layout
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.main.ui.workspace.sidebar.layers :refer [element-icon layer-name frame-wrapper]]
|
||||
[app.util.dom :as dom]
|
||||
[okulary.core :as l]
|
||||
@@ -52,10 +53,10 @@
|
||||
(dom/prevent-default event)
|
||||
(let [id (:id item)]
|
||||
(cond
|
||||
(.-ctrlKey event)
|
||||
(or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(st/emit! (dv/toggle-selection id))
|
||||
|
||||
(.-shiftKey event)
|
||||
(kbd/shift? event)
|
||||
(st/emit! (dv/shift-select-to id))
|
||||
|
||||
:else
|
||||
|
||||
@@ -10,19 +10,23 @@
|
||||
(ns app.main.ui.hooks
|
||||
"A collection of general purpose react hooks."
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
["mousetrap" :as mousetrap]
|
||||
[app.common.spec :as us]
|
||||
[beicon.core :as rx]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf]
|
||||
[app.util.transit :as t]
|
||||
[app.main.data.shortcuts :refer [bind-shortcuts]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.dom.dnd :as dnd]
|
||||
[app.util.webapi :as wapi]
|
||||
[app.util.logging :as log]
|
||||
[app.util.timers :as ts]
|
||||
["mousetrap" :as mousetrap])
|
||||
[app.util.transit :as t]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(log/set-level! :warn)
|
||||
|
||||
(defn use-rxsub
|
||||
[ob]
|
||||
(let [[state reset-state!] (mf/useState @ob)]
|
||||
@@ -33,20 +37,18 @@
|
||||
#js [ob])
|
||||
state))
|
||||
|
||||
(s/def ::shortcuts
|
||||
(s/map-of ::us/string fn?))
|
||||
|
||||
(defn use-shortcuts
|
||||
[shortcuts]
|
||||
(us/assert ::shortcuts shortcuts)
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(->> (seq shortcuts)
|
||||
(run! (fn [[key f]]
|
||||
(mousetrap/bind key (fn [event]
|
||||
(js/console.log "[debug]: shortcut:" key)
|
||||
(.preventDefault event)
|
||||
(f event))))))
|
||||
(bind-shortcuts
|
||||
shortcuts
|
||||
mousetrap/bind
|
||||
(fn [key cb]
|
||||
(fn [event]
|
||||
(log/debug :msg (str "Shortcut" key))
|
||||
(.preventDefault event)
|
||||
(cb event))))
|
||||
(fn [] (mousetrap/reset))))
|
||||
nil)
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
(def icon-empty (icon-xref :icon-empty))
|
||||
(def icon-list (icon-xref :icon-list))
|
||||
(def icon-lock (icon-xref :icon-lock))
|
||||
(def icon-verify (icon-xref :icon-verify))
|
||||
(def icon-set (icon-xref :icon-set))
|
||||
(def image (icon-xref :image))
|
||||
(def infocard (icon-xref :infocard))
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
[event]
|
||||
(.-ctrlKey event))
|
||||
|
||||
(defn ^boolean meta?
|
||||
[event]
|
||||
(.-metaKey event))
|
||||
|
||||
(defn ^boolean shift?
|
||||
[event]
|
||||
(.-shiftKey event))
|
||||
|
||||
@@ -58,9 +58,7 @@
|
||||
:position (or (:position message) :fixed)
|
||||
:controls (if (some? (:controls message))
|
||||
(:controls message)
|
||||
(if (some? (:timeout message))
|
||||
:none
|
||||
:close))
|
||||
:close)
|
||||
:on-close on-close)])))
|
||||
|
||||
(mf/defc inline-banner
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
(defn on-success
|
||||
[x]
|
||||
(st/emit! (rt/nav :auth-goodbye)))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
|
||||
(mf/defc delete-account-modal
|
||||
{::mf/register modal/components
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
(let [shape (unchecked-get props "shape")
|
||||
base-props (unchecked-get props "base-props")
|
||||
elem-name (unchecked-get props "elem-name")
|
||||
;; {:keys [x y width height]} (geom/shape->rect-shape shape)
|
||||
base-style (obj/get base-props "style")
|
||||
{:keys [x y width height]} (:selrect shape)
|
||||
stroke-id (mf/use-var (uuid/next))
|
||||
stroke-style (:stroke-style shape :none)
|
||||
@@ -37,19 +37,25 @@
|
||||
(= stroke-position :inner)
|
||||
(let [clip-id (str "clip-" @stroke-id)
|
||||
|
||||
clip-props (-> (obj/merge! #js {} base-props)
|
||||
(obj/merge! #js {:stroke nil
|
||||
clip-props (obj/merge
|
||||
base-props
|
||||
#js {:transform nil
|
||||
:style (obj/merge
|
||||
base-style
|
||||
#js {:stroke nil
|
||||
:strokeWidth nil
|
||||
:strokeOpacity nil
|
||||
:strokeDasharray nil
|
||||
:fill "white"
|
||||
:fillOpacity 1
|
||||
:transform nil}))
|
||||
:fillOpacity 1})})
|
||||
|
||||
stroke-width (obj/get base-props "strokeWidth")
|
||||
shape-props (-> (obj/merge! #js {} base-props)
|
||||
(obj/merge! #js {:strokeWidth (* stroke-width 2)
|
||||
:clipPath (str "url('#" clip-id "')")}))]
|
||||
stroke-width (obj/get base-style "strokeWidth" 0)
|
||||
shape-props (obj/merge
|
||||
base-props
|
||||
#js {:clipPath (str "url('#" clip-id "')")
|
||||
:style (obj/merge
|
||||
base-style
|
||||
#js {:strokeWidth (* stroke-width 2)})})]
|
||||
[:*
|
||||
[:> "clipPath" #js {:id clip-id}
|
||||
[:> elem-name clip-props]]
|
||||
@@ -63,34 +69,46 @@
|
||||
|
||||
(= stroke-position :outer)
|
||||
(let [stroke-mask-id (str "mask-" @stroke-id)
|
||||
stroke-width (or (.-strokeWidth ^js base-props) 0)
|
||||
mask-props1 (-> (obj/merge! #js {} base-props)
|
||||
(obj/merge! #js {:stroke "white"
|
||||
stroke-width (obj/get base-style "strokeWidth" 0)
|
||||
mask-props1 (obj/merge
|
||||
base-props
|
||||
#js {:transform nil
|
||||
:style (obj/merge
|
||||
base-style
|
||||
#js {:stroke "white"
|
||||
:strokeWidth (* stroke-width 2)
|
||||
:strokeOpacity 1
|
||||
:strokeDasharray nil
|
||||
:fill "white"
|
||||
:fillOpacity 1
|
||||
:transform nil}))
|
||||
mask-props2 (-> (obj/merge! #js {} base-props)
|
||||
(obj/merge! #js {:stroke nil
|
||||
:fillOpacity 1})})
|
||||
mask-props2 (obj/merge
|
||||
base-props
|
||||
#js {:transform nil
|
||||
:style (obj/merge
|
||||
base-style
|
||||
#js {:stroke nil
|
||||
:strokeWidth nil
|
||||
:strokeOpacity nil
|
||||
:strokeDasharray nil
|
||||
:fill "black"
|
||||
:fillOpacity 1
|
||||
:transform nil}))
|
||||
:fillOpacity 1})})
|
||||
|
||||
shape-props1 (-> (obj/merge! #js {} base-props)
|
||||
(obj/merge! #js {:stroke nil
|
||||
shape-props1 (obj/merge
|
||||
base-props
|
||||
#js {:style (obj/merge
|
||||
base-style
|
||||
#js {:stroke nil
|
||||
:strokeWidth nil
|
||||
:strokeOpacity nil
|
||||
:strokeDasharray nil}))
|
||||
shape-props2 (-> (obj/merge! #js {} base-props)
|
||||
(obj/merge! #js {:strokeWidth (* stroke-width 2)
|
||||
:strokeDasharray nil})})
|
||||
shape-props2 (obj/merge
|
||||
base-props
|
||||
#js {:mask (str "url('#" stroke-mask-id "')")
|
||||
:style (obj/merge
|
||||
base-style
|
||||
#js {:strokeWidth (* stroke-width 2)
|
||||
:fill "none"
|
||||
:fillOpacity 0
|
||||
:mask (str "url('#" stroke-mask-id "')")}))]
|
||||
:fillOpacity 0})})]
|
||||
[:*
|
||||
[:mask {:id stroke-mask-id}
|
||||
[:> elem-name mask-props1]
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.config :as cfg]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.main.ui.shapes.attrs :as attrs]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.data.fetch :as df]
|
||||
@@ -43,7 +44,11 @@
|
||||
:transform transform
|
||||
:width width
|
||||
:height height
|
||||
:preserveAspectRatio "none"}))]
|
||||
:preserveAspectRatio "none"}))
|
||||
on-drag-start (fn [event]
|
||||
;; Prevent browser dragging of the image
|
||||
(dom/prevent-default event))]
|
||||
|
||||
(if (nil? @data-uri)
|
||||
[:> "rect" (obj/merge!
|
||||
props
|
||||
@@ -51,4 +56,5 @@
|
||||
:stroke "#000000"})]
|
||||
[:> "image" (obj/merge!
|
||||
props
|
||||
#js {:xlinkHref @data-uri})]))))
|
||||
#js {:xlinkHref @data-uri
|
||||
:onDragStart on-drag-start})]))))
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.pages :as cp]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.data.viewer.shortcuts :as sc]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@@ -214,7 +215,7 @@
|
||||
|
||||
on-mouse-wheel
|
||||
(fn [event]
|
||||
(when (kbd/ctrl? event)
|
||||
(when (or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(dom/prevent-default event)
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
delta (+ (.-deltaY ^js event) (.-deltaX ^js event))]
|
||||
@@ -240,7 +241,7 @@
|
||||
(events/unlistenByKey key3))))]
|
||||
|
||||
(mf/use-effect on-mount)
|
||||
(hooks/use-shortcuts dv/shortcuts)
|
||||
(hooks/use-shortcuts sc/shortcuts)
|
||||
|
||||
[:& fs/fullscreen-wrapper {}
|
||||
[:div.viewer-layout
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.viewer.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
@@ -44,15 +45,15 @@
|
||||
:on-close #(reset! show-dropdown? false)}
|
||||
[:ul.dropdown.zoom-dropdown
|
||||
[:li {:on-click on-increase}
|
||||
"Zoom in" [:span "+"]]
|
||||
"Zoom in" [:span (sc/get-tooltip :increase-zoom)]]
|
||||
[:li {:on-click on-decrease}
|
||||
"Zoom out" [:span "-"]]
|
||||
"Zoom out" [:span (sc/get-tooltip :decrease-zoom)]]
|
||||
[:li {:on-click on-zoom-to-50}
|
||||
"Zoom to 50%" [:span "Shift + 0"]]
|
||||
"Zoom to 50%" [:span (sc/get-tooltip :zoom-50)]]
|
||||
[:li {:on-click on-zoom-to-100}
|
||||
"Zoom to 100%" [:span "Shift + 1"]]
|
||||
"Zoom to 100%" [:span (sc/get-tooltip :reset-zoom)]]
|
||||
[:li {:on-click on-zoom-to-200}
|
||||
"Zoom to 200%" [:span "Shift + 2"]]]]]))
|
||||
"Zoom to 200%" [:span (sc/get-tooltip :zoom-200)]]]]]))
|
||||
|
||||
(mf/defc share-link
|
||||
[{:keys [page token] :as props}]
|
||||
@@ -72,7 +73,7 @@
|
||||
(wapi/write-to-clipboard link)
|
||||
(st/emit! (dm/show {:type :info
|
||||
:content "Link copied successfuly!"
|
||||
:timeout 2000})))]
|
||||
:timeout 3000})))]
|
||||
[:*
|
||||
[:span.btn-primary.btn-small
|
||||
{:alt (t locale "viewer.header.share.title")
|
||||
@@ -272,7 +273,7 @@
|
||||
:on-zoom-to-100 (st/emitf dv/reset-zoom)
|
||||
:on-zoom-to-200 (st/emitf dv/zoom-to-200)}]
|
||||
|
||||
[:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom
|
||||
[:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left
|
||||
{:alt (t locale "viewer.header.fullscreen")
|
||||
:on-click #(if @fullscreen (fullscreen false) (fullscreen true))}
|
||||
(if @fullscreen
|
||||
|
||||
@@ -12,15 +12,16 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.main.constants :as c]
|
||||
[app.main.data.history :as udh]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.workspace.colorpalette :refer [colorpalette]]
|
||||
[app.main.ui.workspace.colorpicker]
|
||||
[app.main.ui.workspace.context-menu :refer [context-menu]]
|
||||
@@ -30,8 +31,8 @@
|
||||
[app.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]]
|
||||
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
|
||||
[app.main.ui.workspace.viewport :refer [viewport viewport-actions coordinates]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
@@ -45,7 +46,8 @@
|
||||
[props]
|
||||
(let [zoom (or (obj/get props "zoom") 1)
|
||||
vbox (obj/get props "vbox")
|
||||
vport (obj/get props "vport")]
|
||||
vport (obj/get props "vport")
|
||||
colorpalette? (obj/get props "colorpalette?")]
|
||||
|
||||
[:*
|
||||
[:div.empty-rule-square]
|
||||
@@ -55,7 +57,7 @@
|
||||
[:& vertical-rule {:zoom zoom
|
||||
:vbox vbox
|
||||
:vport vport}]
|
||||
[:& coordinates]]))
|
||||
[:& coordinates {:colorpalette? colorpalette?}]]))
|
||||
|
||||
(mf/defc workspace-content
|
||||
{::mf/wrap-props false}
|
||||
@@ -75,7 +77,8 @@
|
||||
(when (contains? layout :rules)
|
||||
[:& workspace-rules {:zoom zoom
|
||||
:vbox vbox
|
||||
:vport vport}])
|
||||
:vport vport
|
||||
:colorpalette? (contains? layout :colorpalette)}])
|
||||
|
||||
[:& viewport-actions]
|
||||
[:& viewport {:file file
|
||||
@@ -128,7 +131,7 @@
|
||||
;; Close any non-modal dialog that may be still open
|
||||
(st/emitf dm/hide)))
|
||||
|
||||
(hooks/use-shortcuts dw/shortcuts)
|
||||
(hooks/use-shortcuts sc/shortcuts)
|
||||
|
||||
(let [file (mf/deref refs/workspace-file)
|
||||
project (mf/deref refs/workspace-project)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.util.color :refer [hex->rgb]]
|
||||
[app.util.color :as uc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t]]
|
||||
[app.util.object :as obj]
|
||||
@@ -55,8 +55,8 @@
|
||||
(fn [event]
|
||||
(let [ids (get-in @st/state [:workspace-local :selected])]
|
||||
(if (kbd/shift? event)
|
||||
(st/emit! (mdc/change-stroke ids color))
|
||||
(st/emit! (mdc/change-fill ids color)))))]
|
||||
(st/emit! (mdc/change-stroke ids (merge uc/empty-color color)))
|
||||
(st/emit! (mdc/change-fill ids (merge uc/empty-color color))))))]
|
||||
|
||||
[:div.color-cell {:class (str "cell-"(name size))
|
||||
:on-click select-color}
|
||||
|
||||
@@ -32,9 +32,13 @@
|
||||
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]]
|
||||
[app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]]))
|
||||
|
||||
(def selected-palette-ref
|
||||
(-> (l/in [:workspace-local :selected-palette-colorpicker])
|
||||
(l/derived st/state)))
|
||||
|
||||
(mf/defc libraries [{:keys [current-color on-select-color on-add-library-color
|
||||
disable-gradient disable-opacity]}]
|
||||
(let [selected-library (mf/use-state "recent")
|
||||
(let [selected-library (or (mf/deref selected-palette-ref) :recent)
|
||||
current-library-colors (mf/use-state [])
|
||||
|
||||
shared-libs (mf/deref refs/workspace-libraries)
|
||||
@@ -43,10 +47,10 @@
|
||||
locale (mf/deref i18n/locale)
|
||||
|
||||
parse-selected
|
||||
(fn [selected]
|
||||
(if (#{"recent" "file"} selected)
|
||||
(keyword selected)
|
||||
(uuid selected)) )
|
||||
(fn [selected-str]
|
||||
(if (#{"recent" "file"} selected-str)
|
||||
(keyword selected-str)
|
||||
(uuid selected-str)))
|
||||
|
||||
check-valid-color? (fn [color]
|
||||
(and (or (not disable-gradient) (not (:gradient color)))
|
||||
@@ -54,37 +58,36 @@
|
||||
|
||||
;; Load library colors when the select is changed
|
||||
(mf/use-effect
|
||||
(mf/deps @selected-library)
|
||||
(mf/deps selected-library)
|
||||
(fn []
|
||||
(let [mapped-colors
|
||||
(cond
|
||||
(= @selected-library "recent")
|
||||
(= selected-library :recent)
|
||||
;; The `map?` check is to keep backwards compatibility. We transform from string to map
|
||||
(map #(if (map? %) % (hash-map :color %)) (reverse (or recent-colors [])))
|
||||
|
||||
(= @selected-library "file")
|
||||
(= selected-library :file)
|
||||
(vals file-colors)
|
||||
|
||||
:else ;; Library UUID
|
||||
(->> (get-in shared-libs [(uuid @selected-library) :data :colors])
|
||||
(->> (get-in shared-libs [selected-library :data :colors])
|
||||
(vals)
|
||||
(map #(merge % {:file-id (uuid @selected-library)}))))]
|
||||
(map #(merge % {:file-id selected-library}))))]
|
||||
|
||||
(reset! current-library-colors (into [] (filter check-valid-color?) mapped-colors)))))
|
||||
|
||||
;; If the file colors change and the file option is selected updates the state
|
||||
(mf/use-effect
|
||||
(mf/deps file-colors)
|
||||
(fn [] (when (= @selected-library "file")
|
||||
(fn [] (when (= selected-library :file)
|
||||
(let [colors (vals file-colors)]
|
||||
(reset! current-library-colors (into [] (filter check-valid-color?) colors))))))
|
||||
|
||||
|
||||
[:div.libraries
|
||||
[:select {:on-change (fn [e]
|
||||
(when-let [val (dom/get-target-val e)]
|
||||
(reset! selected-library val)))
|
||||
:value @selected-library}
|
||||
(when-let [val (parse-selected (dom/get-target-val e))]
|
||||
(st/emit! (dc/change-palette-selected-colorpicker val))))
|
||||
:value (name selected-library)}
|
||||
[:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")]
|
||||
[:option {:value "file"} (t locale "workspace.libraries.colors.file-library")]
|
||||
|
||||
@@ -93,13 +96,13 @@
|
||||
:value id} name])]
|
||||
|
||||
[:div.selected-colors
|
||||
(when (= "file" @selected-library)
|
||||
(when (= selected-library :file)
|
||||
[:div.color-bullet.button.plus-button {:style {:background-color "white"}
|
||||
:on-click on-add-library-color}
|
||||
i/plus])
|
||||
|
||||
[:div.color-bullet.button {:style {:background-color "white"}
|
||||
:on-click #(st/emit! (dc/show-palette (parse-selected @selected-library)))}
|
||||
:on-click #(st/emit! (dc/show-palette selected-library))}
|
||||
i/palette]
|
||||
|
||||
(for [[idx color] (map-indexed vector @current-library-colors)]
|
||||
|
||||
@@ -14,11 +14,12 @@
|
||||
[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.streams :as ms]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.hooks :refer [use-rxsub]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
@@ -99,52 +100,52 @@
|
||||
(:component-file shape)))]
|
||||
[:*
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.copy")
|
||||
:shortcut "Ctrl + c"
|
||||
:shortcut (sc/get-tooltip :copy)
|
||||
:on-click do-copy}]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.cut")
|
||||
:shortcut "Ctrl + x"
|
||||
:shortcut (sc/get-tooltip :cut)
|
||||
:on-click do-cut}]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.paste")
|
||||
:shortcut "Ctrl + v"
|
||||
:shortcut (sc/get-tooltip :paste)
|
||||
:on-click do-paste}]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.duplicate")
|
||||
:shortcut "Ctrl + d"
|
||||
:shortcut (sc/get-tooltip :duplicate)
|
||||
:on-click do-duplicate}]
|
||||
[:& menu-separator]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.forward")
|
||||
:shortcut "Ctrl + ↑"
|
||||
:shortcut (sc/get-tooltip :bring-forward)
|
||||
:on-click do-bring-forward}]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.front")
|
||||
:shortcut "Ctrl + Shift + ↑"
|
||||
:shortcut (sc/get-tooltip :bring-front)
|
||||
:on-click do-bring-to-front}]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.backward")
|
||||
:shortcut "Ctrl + ↓"
|
||||
:shortcut (sc/get-tooltip :bring-backward)
|
||||
:on-click do-send-backward}]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.back")
|
||||
:shortcut "Ctrl + Shift + ↓"
|
||||
:shortcut (sc/get-tooltip :bring-back)
|
||||
:on-click do-send-to-back}]
|
||||
[:& menu-separator]
|
||||
|
||||
(when (> (count selected) 1)
|
||||
[:*
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.group")
|
||||
:shortcut "Ctrl + g"
|
||||
:shortcut (sc/get-tooltip :group)
|
||||
:on-click do-create-group}]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.mask")
|
||||
:shortcut "Ctrl + M"
|
||||
:shortcut (sc/get-tooltip :mask)
|
||||
:on-click do-mask-group}]])
|
||||
|
||||
(when (and (= (count selected) 1) (= (:type shape) :group))
|
||||
[:*
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.ungroup")
|
||||
:shortcut "Shift + g"
|
||||
:shortcut (sc/get-tooltip :ungroup)
|
||||
:on-click do-remove-group}]
|
||||
(if (:masked-group? shape)
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.unmask")
|
||||
:shortcut "Shift + M"
|
||||
:shortcut (sc/get-tooltip :unmask)
|
||||
:on-click do-unmask-group}]
|
||||
[:& menu-entry {:title "Mask"
|
||||
:shortcut "Ctrl + M"
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.mask")
|
||||
:shortcut (sc/get-tooltip :group)
|
||||
:on-click do-mask-group}])])
|
||||
|
||||
(if (:hidden shape)
|
||||
@@ -165,7 +166,7 @@
|
||||
[:*
|
||||
[:& menu-separator]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.create-component")
|
||||
:shortcut "Ctrl + K"
|
||||
:shortcut (sc/get-tooltip :create-component)
|
||||
:on-click do-add-component}]])
|
||||
|
||||
(when (and (:component-id shape)
|
||||
@@ -197,7 +198,7 @@
|
||||
|
||||
[:& menu-separator]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.delete")
|
||||
:shortcut "Supr"
|
||||
:shortcut (sc/get-tooltip :delete)
|
||||
:on-click do-delete}]]))
|
||||
|
||||
(mf/defc viewport-context-menu
|
||||
@@ -206,7 +207,7 @@
|
||||
do-paste (st/emitf dw/paste)]
|
||||
[:*
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.paste")
|
||||
:shortcut "Ctrl + v"
|
||||
:shortcut (sc/get-tooltip :paste)
|
||||
:on-click do-paste}]]))
|
||||
|
||||
(mf/defc context-menu
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
drawing? @refs/selected-drawing-tool
|
||||
button (.-which (.-nativeEvent event))
|
||||
shift? (kbd/shift? event)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
ctrl? (or (kbd/ctrl? event) (kbd/meta? event))
|
||||
|
||||
allow-click? (and (not blocked)
|
||||
(not drawing?)
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
[app.config :as cfg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
@@ -73,15 +75,15 @@
|
||||
:on-close #(reset! show-dropdown? false)}
|
||||
[:ul.zoom-dropdown
|
||||
[:li {:on-click on-increase}
|
||||
"Zoom in" [:span "+"]]
|
||||
"Zoom in" [:span (sc/get-tooltip :increase-zoom)]]
|
||||
[:li {:on-click on-decrease}
|
||||
"Zoom out" [:span "-"]]
|
||||
"Zoom out" [:span (sc/get-tooltip :decrease-zoom)]]
|
||||
[:li {:on-click on-zoom-reset}
|
||||
"Zoom to 100%" [:span "Shift + 0"]]
|
||||
"Zoom to 100%" [:span (sc/get-tooltip :reset-zoom)]]
|
||||
[:li {:on-click on-zoom-fit}
|
||||
"Zoom to fit all" [:span "Shift + 1"]]
|
||||
"Zoom to fit all" [:span (sc/get-tooltip :fit-all)]]
|
||||
[:li {:on-click on-zoom-selected}
|
||||
"Zoom to selected" [:span "Shift + 2"]]]]]))
|
||||
"Zoom to selected" [:span (sc/get-tooltip :zoom-selected)]]]]]))
|
||||
|
||||
;; --- Header Users
|
||||
|
||||
@@ -146,8 +148,8 @@
|
||||
[:div.btn-icon-dark.btn-small {:on-click #(reset! show-menu? true)} i/actions]
|
||||
[:div.project-tree {:alt (tr "workspace.sitemap")}
|
||||
[:span.project-name
|
||||
{:on-click #(st/emit! (rt/navigate :dashboard-project {:team-id team-id
|
||||
:project-id (:project-id file)}))}
|
||||
{:on-click #(st/emit! (rt/navigate :dashboard-files {:team-id team-id
|
||||
:project-id (:project-id file)}))}
|
||||
(:name project) " /"]
|
||||
(if @editing?
|
||||
[:input.file-name
|
||||
@@ -171,52 +173,53 @@
|
||||
(if (contains? layout :rules)
|
||||
(tr "workspace.header.menu.hide-rules")
|
||||
(tr "workspace.header.menu.show-rules"))]
|
||||
[:span.shortcut "Ctrl+shift+R"]]
|
||||
[:span.shortcut (sc/get-tooltip :toggle-rules)]]
|
||||
|
||||
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :display-grid))}
|
||||
[:span
|
||||
(if (contains? layout :display-grid)
|
||||
(tr "workspace.header.menu.hide-grid")
|
||||
(tr "workspace.header.menu.show-grid"))]
|
||||
[:span.shortcut "Ctrl+'"]]
|
||||
[:span.shortcut (sc/get-tooltip :toggle-grid)]]
|
||||
|
||||
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :snap-grid))}
|
||||
[:span
|
||||
(if (contains? layout :snap-grid)
|
||||
(tr "workspace.header.menu.disable-snap-grid")
|
||||
(tr "workspace.header.menu.enable-snap-grid"))]
|
||||
[:span.shortcut "Ctrl+Shift+'"]]
|
||||
[:span.shortcut (sc/get-tooltip :toggle-snap-grid)]]
|
||||
|
||||
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :sitemap :layers))}
|
||||
[:span
|
||||
(if (or (contains? layout :sitemap) (contains? layout :layers))
|
||||
(tr "workspace.header.menu.hide-layers")
|
||||
(tr "workspace.header.menu.show-layers"))]
|
||||
[:span.shortcut "Ctrl+l"]]
|
||||
[:span.shortcut (sc/get-tooltip :toggle-layers)]]
|
||||
|
||||
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :colorpalette))}
|
||||
[:span
|
||||
(if (contains? layout :colorpalette)
|
||||
(tr "workspace.header.menu.hide-palette")
|
||||
(tr "workspace.header.menu.show-palette"))]
|
||||
[:span.shortcut "Ctrl+p"]]
|
||||
[:span.shortcut (sc/get-tooltip :toggle-palette)]]
|
||||
|
||||
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :assets))}
|
||||
[:span
|
||||
(if (contains? layout :assets)
|
||||
(tr "workspace.header.menu.hide-assets")
|
||||
(tr "workspace.header.menu.show-assets"))]
|
||||
[:span.shortcut "Ctrl+i"]]
|
||||
[:span.shortcut (sc/get-tooltip :toggle-assets)]]
|
||||
|
||||
[:li {:on-click #(st/emit! (dw/select-all))}
|
||||
[:span (tr "workspace.header.menu.select-all")]
|
||||
[:span.shortcut "Ctrl+a"]]
|
||||
[:span.shortcut (sc/get-tooltip :select-all)]]
|
||||
|
||||
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :dynamic-alignment))}
|
||||
[:span
|
||||
(if (contains? layout :dynamic-alignment)
|
||||
(tr "workspace.header.menu.disable-dynamic-alignment")
|
||||
(tr "workspace.header.menu.enable-dynamic-alignment"))]]
|
||||
(tr "workspace.header.menu.enable-dynamic-alignment"))]
|
||||
[:span.shortcut (sc/get-tooltip :toggle-alignment)]]
|
||||
|
||||
(if (:is-shared file)
|
||||
[:li {:on-click on-remove-shared}
|
||||
@@ -272,8 +275,8 @@
|
||||
:on-zoom-fit #(st/emit! dw/zoom-to-fit-all)
|
||||
:on-zoom-selected #(st/emit! dw/zoom-to-selected-shape)}]
|
||||
|
||||
[:a.btn-icon-dark.btn-small.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.header.viewer")
|
||||
[:a.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left
|
||||
{:alt (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer))
|
||||
:href (str "#" view-url)
|
||||
:on-click go-viewer}
|
||||
i/play]]]))
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
(ns app.main.ui.workspace.left-toolbar
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.math :as mth]
|
||||
[app.common.media :as cm]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||
@@ -35,8 +37,13 @@
|
||||
(mf/deps file)
|
||||
(fn [blobs]
|
||||
(let [params {:file-id (:id file)
|
||||
:data (seq blobs)}]
|
||||
(st/emit! (dw/upload-media-workspace params (gpt/point 0 0))))))]
|
||||
:data (seq blobs)}
|
||||
;; We don't want to add a ref because that redraws the component
|
||||
;; for everychange. Better direct access on the callback
|
||||
vbox (get-in @st/state [:workspace-local :vbox])
|
||||
x (mth/round (+ (:x vbox) (/ (:width vbox) 2)))
|
||||
y (mth/round (+ (:y vbox) (/ (:height vbox) 2)))]
|
||||
(st/emit! (dw/upload-media-workspace params (gpt/point x y))))))]
|
||||
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (tr "workspace.toolbar.image")
|
||||
@@ -67,22 +74,22 @@
|
||||
:on-click (st/emitf :interrupt)}
|
||||
i/pointer-inner]
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (tr "workspace.toolbar.frame")
|
||||
{:alt (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame))
|
||||
:class (when (= selected-drawtool :frame) "selected")
|
||||
:on-click (partial select-drawtool :frame)}
|
||||
i/artboard]
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (tr "workspace.toolbar.rect")
|
||||
{:alt (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect))
|
||||
:class (when (= selected-drawtool :rect) "selected")
|
||||
:on-click (partial select-drawtool :rect)}
|
||||
i/box]
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (tr "workspace.toolbar.ellipse")
|
||||
{:alt (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse))
|
||||
:class (when (= selected-drawtool :circle) "selected")
|
||||
:on-click (partial select-drawtool :circle)}
|
||||
i/circle]
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (tr "workspace.toolbar.text")
|
||||
{:alt (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text))
|
||||
:class (when (= selected-drawtool :text) "selected")
|
||||
:on-click (partial select-drawtool :text)}
|
||||
i/text]
|
||||
@@ -90,40 +97,40 @@
|
||||
[:& image-upload]
|
||||
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (tr "workspace.toolbar.curve")
|
||||
{:alt (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve))
|
||||
:class (when (= selected-drawtool :curve) "selected")
|
||||
:on-click (partial select-drawtool :curve)}
|
||||
i/pencil]
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (tr "workspace.toolbar.path")
|
||||
{:alt (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path))
|
||||
:class (when (= selected-drawtool :path) "selected")
|
||||
:on-click (partial select-drawtool :path)}
|
||||
i/pen]
|
||||
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (tr "workspace.toolbar.comments")
|
||||
{:alt (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment))
|
||||
:class (when (= selected-drawtool :comments) "selected")
|
||||
:on-click (partial select-drawtool :comments)}
|
||||
i/chat]]
|
||||
|
||||
[:ul.left-toolbar-options.panels
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt "Layers"
|
||||
{:alt (tr "workspace.sidebar.layers" (sc/get-tooltip :toggle-layers))
|
||||
:class (when (contains? layout :layers) "selected")
|
||||
:on-click (st/emitf (dw/go-to-layout :layers))}
|
||||
i/layers]
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (tr "workspace.toolbar.assets")
|
||||
{:alt (tr "workspace.toolbar.assets" (sc/get-tooltip :toggle-assets))
|
||||
:class (when (contains? layout :assets) "selected")
|
||||
:on-click (st/emitf (dw/go-to-layout :assets))}
|
||||
i/library]
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt "History"
|
||||
{:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history))
|
||||
:class (when (contains? layout :document-history) "selected")
|
||||
:on-click (st/emitf (dw/go-to-layout :document-history))}
|
||||
i/undo-history]
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (tr "workspace.toolbar.color-palette")
|
||||
{:alt (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-palette))
|
||||
:class (when (contains? layout :colorpalette) "selected")
|
||||
:on-click (st/emitf (dw/toggle-layout-flags :colorpalette))}
|
||||
i/palette]]]]))
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
:y cy'
|
||||
:width resize-point-circle-radius
|
||||
:height resize-point-circle-radius
|
||||
:transform (str/fmt "rotate(%s, %s, %s)" rotation cx' cy')
|
||||
:transform (when rotation (str/fmt "rotate(%s, %s, %s)" rotation cx' cy'))
|
||||
:style {:fill (if (debug? :resize-handler) "red" "transparent")
|
||||
:cursor cursor}
|
||||
:on-mouse-down #(on-resize {:x cx' :y cy'} %)}])
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.ui.cursors :as cur]
|
||||
[app.main.ui.shapes.text.styles :as sts])
|
||||
(:import
|
||||
@@ -163,7 +164,8 @@
|
||||
(fn []
|
||||
(st/emit! dw/clear-edition-mode)
|
||||
(when (= 0 (content-size @content-var))
|
||||
(st/emit! (dw/delete-shapes [id]))))
|
||||
(st/emit! (dws/deselect-shape id)
|
||||
(dw/delete-shapes [id]))))
|
||||
|
||||
on-click-outside
|
||||
(fn [event]
|
||||
@@ -202,7 +204,8 @@
|
||||
|
||||
on-mount
|
||||
(fn []
|
||||
(let [keys [(events/listen js/document EventType.CLICK on-click-outside)
|
||||
(let [keys [(events/listen js/document EventType.MOUSEDOWN on-click-outside)
|
||||
(events/listen js/document EventType.CLICK on-click-outside)
|
||||
(events/listen js/document EventType.KEYUP on-key-up)]]
|
||||
(st/emit! (dwt/assign-editor id editor)
|
||||
(dwc/start-undo-transaction))
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc components-box
|
||||
[{:keys [file-id local? components open? on-open on-close] :as props}]
|
||||
[{:keys [file-id local? components open?] :as props}]
|
||||
(let [state (mf/use-state {:menu-open false
|
||||
:renaming nil
|
||||
:top nil
|
||||
@@ -106,7 +106,8 @@
|
||||
|
||||
[:div.asset-group
|
||||
[:div.group-title {:class (when (not open?) "closed")}
|
||||
[:span {:on-click #(if open? (on-close) (on-open))} i/arrow-slide (tr "workspace.assets.components")]
|
||||
[:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :components (not open?)))}
|
||||
i/arrow-slide (tr "workspace.assets.components")]
|
||||
[:span (str "\u00A0(") (count components) ")"]] ;; Unicode 00A0 is non-breaking space
|
||||
(when open?
|
||||
[:div.group-grid.big
|
||||
@@ -140,7 +141,7 @@
|
||||
[(tr "workspace.assets.delete") on-delete]]}])]))
|
||||
|
||||
(mf/defc graphics-box
|
||||
[{:keys [file-id local? objects open? on-open on-close] :as props}]
|
||||
[{:keys [file-id local? objects open?] :as props}]
|
||||
(let [input-ref (mf/use-ref nil)
|
||||
state (mf/use-state {:menu-open false
|
||||
:renaming nil
|
||||
@@ -151,7 +152,7 @@
|
||||
add-graphic
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(on-open)
|
||||
(st/emitf (dwl/set-assets-box-open file-id :graphics true))
|
||||
(dom/click (mf/ref-val input-ref))))
|
||||
|
||||
on-media-uploaded
|
||||
@@ -218,7 +219,8 @@
|
||||
|
||||
[:div.asset-group
|
||||
[:div.group-title {:class (when (not open?) "closed")}
|
||||
[:span {:on-click #(if open? (on-close) (on-open))} i/arrow-slide (tr "workspace.assets.graphics")]
|
||||
[:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :graphics (not open?)))}
|
||||
i/arrow-slide (tr "workspace.assets.graphics")]
|
||||
[:span.num-assets (str "\u00A0(") (count objects) ")"] ;; Unicode 00A0 is non-breaking space
|
||||
(when local?
|
||||
[:div.group-button {:on-click add-graphic}
|
||||
@@ -375,7 +377,7 @@
|
||||
[(t locale "workspace.assets.delete") delete-color]]}])]))
|
||||
|
||||
(mf/defc colors-box
|
||||
[{:keys [file-id local? colors locale open? on-open on-close] :as props}]
|
||||
[{:keys [file-id local? colors locale open?] :as props}]
|
||||
(let [add-color
|
||||
(mf/use-callback
|
||||
(mf/deps file-id)
|
||||
@@ -386,7 +388,7 @@
|
||||
(mf/use-callback
|
||||
(mf/deps file-id)
|
||||
(fn [event]
|
||||
(on-open)
|
||||
(st/emitf (dwl/set-assets-box-open file-id :colors true))
|
||||
(modal/show! :colorpicker
|
||||
{:x (.-clientX event)
|
||||
:y (.-clientY event)
|
||||
@@ -396,7 +398,8 @@
|
||||
:position :right})))]
|
||||
[:div.asset-group
|
||||
[:div.group-title {:class (when (not open?) "closed")}
|
||||
[:span {:on-click #(if open? (on-close) (on-open))} i/arrow-slide (t locale "workspace.assets.colors")]
|
||||
[:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :colors (not open?)))}
|
||||
i/arrow-slide (t locale "workspace.assets.colors")]
|
||||
[:span.num-assets (str "\u00A0(") (count colors) ")"] ;; Unicode 00A0 is non-breaking space
|
||||
(when local?
|
||||
[:div.group-button {:on-click add-color-clicked} i/plus])]
|
||||
@@ -414,7 +417,7 @@
|
||||
:locale locale}]))])]))
|
||||
|
||||
(mf/defc typography-box
|
||||
[{:keys [file file-id local? typographies locale open? on-open on-close] :as props}]
|
||||
[{:keys [file file-id local? typographies locale open?] :as props}]
|
||||
|
||||
(let [state (mf/use-state {:detail-open? false
|
||||
:menu-open? false
|
||||
@@ -488,7 +491,8 @@
|
||||
|
||||
[:div.asset-group
|
||||
[:div.group-title {:class (when (not open?) "closed")}
|
||||
[:span {:on-click #(if open? (on-close) (on-open))} i/arrow-slide (t locale "workspace.assets.typography")]
|
||||
[:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :typography (not open?)))}
|
||||
i/arrow-slide (t locale "workspace.assets.typography")]
|
||||
[:span.num-assets (str "\u00A0(") (count typographies) ")"] ;; Unicode 00A0 is non-breaking space
|
||||
(when local?
|
||||
[:div.group-button {:on-click add-typography} i/plus])]
|
||||
@@ -553,6 +557,11 @@
|
||||
(vals (get-in state [:workspace-libraries id :data :typographies])))))
|
||||
st/state =))
|
||||
|
||||
(defn open-file-ref
|
||||
[id]
|
||||
(-> (l/in [:assets-files-open id])
|
||||
(l/derived refs/workspace-local)))
|
||||
|
||||
(defn apply-filters
|
||||
[coll filters]
|
||||
(->> coll
|
||||
@@ -562,16 +571,18 @@
|
||||
(sort-by #(str/lower (:name %)))))
|
||||
|
||||
(mf/defc file-library
|
||||
[{:keys [file local? open? filters locale] :as props}]
|
||||
(let [open? (mf/use-state open?)
|
||||
[{:keys [file local? default-open? filters locale] :as props}]
|
||||
(let [open-file (mf/deref (open-file-ref (:id file)))
|
||||
open? (-> open-file
|
||||
:library
|
||||
(d/nilv default-open?))
|
||||
open-box? (fn [box]
|
||||
(-> open-file
|
||||
box
|
||||
(d/nilv true)))
|
||||
shared? (:is-shared file)
|
||||
router (mf/deref refs/router)
|
||||
toggle-open #(swap! open? not)
|
||||
|
||||
toggles (mf/use-state #{:components
|
||||
:graphics
|
||||
:colors
|
||||
:typographies})
|
||||
toggle-open (st/emitf (dwl/set-assets-box-open (:id file) :library (not open?)))
|
||||
|
||||
url (rt/resolve router :workspace
|
||||
{:project-id (:project-id file)
|
||||
@@ -594,7 +605,7 @@
|
||||
[:div.tool-window-bar.library-bar
|
||||
{:on-click toggle-open}
|
||||
[:div.collapse-library
|
||||
{:class (dom/classnames :open @open?)}
|
||||
{:class (dom/classnames :open open?)}
|
||||
i/arrow-slide]
|
||||
|
||||
(if local?
|
||||
@@ -610,7 +621,7 @@
|
||||
:on-click dom/stop-propagation}
|
||||
i/chain]]])]
|
||||
|
||||
(when @open?
|
||||
(when open?
|
||||
(let [show-components? (and (or (= (:box filters) :all)
|
||||
(= (:box filters) :components))
|
||||
(or (> (count components) 0)
|
||||
@@ -632,24 +643,18 @@
|
||||
[:& components-box {:file-id (:id file)
|
||||
:local? local?
|
||||
:components components
|
||||
:open? (contains? @toggles :components)
|
||||
:on-open #(swap! toggles conj :components)
|
||||
:on-close #(swap! toggles disj :components)}])
|
||||
:open? (open-box? :components)}])
|
||||
(when show-graphics?
|
||||
[:& graphics-box {:file-id (:id file)
|
||||
:local? local?
|
||||
:objects media
|
||||
:open? (contains? @toggles :graphics)
|
||||
:on-open #(swap! toggles conj :graphics)
|
||||
:on-close #(swap! toggles disj :graphics)}])
|
||||
:open? (open-box? :graphics)}])
|
||||
(when show-colors?
|
||||
[:& colors-box {:file-id (:id file)
|
||||
:local? local?
|
||||
:locale locale
|
||||
:colors colors
|
||||
:open? (contains? @toggles :colors)
|
||||
:on-open #(swap! toggles conj :colors)
|
||||
:on-close #(swap! toggles disj :colors)}])
|
||||
:open? (open-box? :colors)}])
|
||||
|
||||
(when show-typography?
|
||||
[:& typography-box {:file file
|
||||
@@ -657,9 +662,7 @@
|
||||
:local? local?
|
||||
:locale locale
|
||||
:typographies typographies
|
||||
:open? (contains? @toggles :typographies)
|
||||
:on-open #(swap! toggles conj :typographies)
|
||||
:on-close #(swap! toggles disj :typographies)}])
|
||||
:open? (open-box? :typographies)}])
|
||||
|
||||
(when (and (not show-components?) (not show-graphics?) (not show-colors?))
|
||||
[:div.asset-group
|
||||
@@ -733,7 +736,7 @@
|
||||
{:file file
|
||||
:locale locale
|
||||
:local? true
|
||||
:open? true
|
||||
:default-open? true
|
||||
:filters @filters}]
|
||||
|
||||
(for [file (->> libraries
|
||||
@@ -743,6 +746,6 @@
|
||||
:file file
|
||||
:local? false
|
||||
:locale locale
|
||||
:open? false
|
||||
:default-open? false
|
||||
:filters @filters}])]]))
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t]]
|
||||
[app.util.object :as obj]
|
||||
@@ -140,10 +141,10 @@
|
||||
(:hidden item))
|
||||
nil
|
||||
|
||||
(.-shiftKey event)
|
||||
(kbd/shift? event)
|
||||
(st/emit! (dw/shift-select-shapes id))
|
||||
|
||||
(.-ctrlKey event)
|
||||
(or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(st/emit! (dw/select-shape id true))
|
||||
|
||||
(> (count selected) 1)
|
||||
|
||||
@@ -47,7 +47,12 @@
|
||||
|
||||
handle-change
|
||||
(fn [value]
|
||||
(change! #(assoc-in % [:blur :value] value)))
|
||||
(change! #(cond-> %
|
||||
(not (contains? % :blur))
|
||||
(assoc :blur (create-blur))
|
||||
|
||||
:always
|
||||
(assoc-in [:blur :value] value))))
|
||||
|
||||
handle-toggle-visibility
|
||||
(fn []
|
||||
|
||||
@@ -9,17 +9,18 @@
|
||||
|
||||
(ns app.main.ui.workspace.sidebar.options.fill
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[app.common.pages :as cp]
|
||||
[app.main.data.colors :as dc]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.colors :as dc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :as i18n :refer [tr t]]
|
||||
[app.util.object :as obj]))
|
||||
[app.util.object :as obj]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def fill-attrs
|
||||
[:fill-color
|
||||
@@ -57,19 +58,24 @@
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [event]
|
||||
(st/emit! (dc/change-fill ids nil))))
|
||||
(st/emit! (dc/change-fill ids (into {} uc/empty-color)))))
|
||||
|
||||
on-change
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [color]
|
||||
(st/emit! (dc/change-fill ids color))))
|
||||
(let [remove-multiple (fn [[key value]] (not= value :multiple))
|
||||
color (into {} (filter remove-multiple) color)]
|
||||
(st/emit! (dc/change-fill ids color)))))
|
||||
|
||||
on-detach
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn []
|
||||
(st/emit! (dc/change-fill ids (dissoc color :id :file-id)))))
|
||||
(let [remove-multiple (fn [[key value]] (not= value :multiple))
|
||||
color (-> (into {} (filter remove-multiple) color)
|
||||
(assoc :id nil :file-id nil))]
|
||||
(st/emit! (dc/change-fill ids color)))))
|
||||
|
||||
on-open-picker
|
||||
(mf/use-callback
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
[v]
|
||||
(when v (select-keys v blur-keys)))
|
||||
|
||||
(defn empty-map [keys]
|
||||
(into {} (map #(hash-map % nil)) keys))
|
||||
|
||||
(defn get-attrs
|
||||
"Given a `type` of options that we want to extract and the shapes to extract them from
|
||||
returns a list of tuples [id, values] with the extracted properties for the shapes that
|
||||
@@ -142,7 +145,9 @@
|
||||
result (case props
|
||||
:ignore [ids values]
|
||||
:shape [(conj ids id)
|
||||
(merge-attrs values (select-keys shape attrs))]
|
||||
(merge-attrs values (merge
|
||||
(empty-map attrs)
|
||||
(select-keys shape attrs)))]
|
||||
:text [(conj ids id)
|
||||
(-> values
|
||||
(merge-attrs (select-keys shape attrs))
|
||||
|
||||
@@ -82,10 +82,13 @@
|
||||
(dissoc :gradient)))))
|
||||
|
||||
change-opacity (fn [new-opacity]
|
||||
(when on-change (on-change (assoc color :opacity new-opacity))))
|
||||
(when on-change (on-change (assoc color
|
||||
:opacity new-opacity
|
||||
:id nil
|
||||
:file-id nil))))
|
||||
|
||||
handle-pick-color (fn [color]
|
||||
(when on-change (on-change color)))
|
||||
(when on-change (on-change (merge uc/empty-color color))))
|
||||
|
||||
handle-open (fn [] (when on-open (on-open)))
|
||||
|
||||
@@ -133,7 +136,7 @@
|
||||
|
||||
(cond
|
||||
;; Rendering a color with ID
|
||||
(:id color)
|
||||
(and (:id color) (not (uc/multiple? color)))
|
||||
[:*
|
||||
[:div.color-info
|
||||
[:div.color-name (str (get-color-name color))]]
|
||||
@@ -146,7 +149,8 @@
|
||||
|
||||
;; Rendering a gradient
|
||||
(and (not (uc/multiple? color))
|
||||
(:gradient color) (get-in color [:gradient :type]))
|
||||
(:gradient color)
|
||||
(get-in color [:gradient :type]))
|
||||
[:div.color-info
|
||||
[:div.color-name (cb/gradient-type->string (get-in color [:gradient :type]))]]
|
||||
|
||||
|
||||
@@ -78,11 +78,12 @@
|
||||
update-color
|
||||
(fn [index]
|
||||
(fn [color opacity]
|
||||
(st/emit! (dwc/update-shapes
|
||||
ids
|
||||
#(-> %
|
||||
(assoc-in [:shadow index :color] color)
|
||||
(assoc-in [:shadow index :opacity] opacity))))))
|
||||
(let [color (d/without-keys color [:id :file-id :gradient])]
|
||||
(st/emit! (dwc/update-shapes
|
||||
ids
|
||||
#(-> %
|
||||
(assoc-in [:shadow index :color] color)
|
||||
(assoc-in [:shadow index :opacity] opacity)))))))
|
||||
|
||||
detach-color
|
||||
(fn [index]
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [color]
|
||||
(st/emit! (dc/change-stroke ids color))))
|
||||
(let [remove-multiple (fn [[key value]] (not= value :multiple))
|
||||
color (into {} (filter remove-multiple) color)]
|
||||
(st/emit! (dc/change-stroke ids color)))))
|
||||
|
||||
handle-detach
|
||||
(mf/use-callback
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
[:div.row-flex
|
||||
[:> grow-options opts]
|
||||
[:> text-decoration-options opts]]
|
||||
|
||||
|
||||
]]))
|
||||
|
||||
(mf/defc options
|
||||
|
||||
@@ -81,9 +81,9 @@
|
||||
;; --- Coordinates Widget
|
||||
|
||||
(mf/defc coordinates
|
||||
[]
|
||||
[{:keys [colorpalette?]}]
|
||||
(let [coords (hooks/use-rxsub ms/mouse-position)]
|
||||
[:ul.coordinates
|
||||
[:ul.coordinates {:class (when colorpalette? "color-palette-open")}
|
||||
[:span {:alt "x"}
|
||||
(str "X: " (:x coords "-"))]
|
||||
[:span {:alt "y"}
|
||||
@@ -508,7 +508,7 @@
|
||||
(let [node (mf/ref-val viewport-ref)
|
||||
target (dom/get-target event)]
|
||||
(cond
|
||||
(kbd/ctrl? event)
|
||||
(or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
pos @ms/mouse-position]
|
||||
(dom/prevent-default event)
|
||||
|
||||
@@ -109,12 +109,16 @@
|
||||
|
||||
:else "transparent")))
|
||||
|
||||
(defn multiple? [{:keys [value color gradient]}]
|
||||
(defn multiple? [{:keys [id file-id value color gradient]}]
|
||||
(or (= value :multiple)
|
||||
(= color :multiple)
|
||||
(= gradient :multiple)
|
||||
(and gradient color)))
|
||||
(= id :multiple)
|
||||
(= file-id :multiple)))
|
||||
|
||||
(defn parse-color [^string color-str]
|
||||
(let [result (gcolor/parse color-str)]
|
||||
(str (.-hex ^js result))))
|
||||
|
||||
(def empty-color
|
||||
(into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity]))
|
||||
|
||||
@@ -289,7 +289,8 @@
|
||||
(:c2x params) (update-in [index :params :c2x] + (:c2x params))
|
||||
(:c2y params) (update-in [index :params :c2y] + (:c2y params)))
|
||||
content))]
|
||||
(reduce apply-to-index content modifiers)))
|
||||
(let [content (if (vector? content) content (into [] content))]
|
||||
(reduce apply-to-index content modifiers))))
|
||||
|
||||
(defn command->point [command]
|
||||
(when-not (nil? command)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"A http client with rx streams interface."
|
||||
(:refer-clojure :exclude [get])
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.util.object :as obj]
|
||||
[app.util.transit :as t]
|
||||
[beicon.core :as rx]
|
||||
@@ -68,11 +69,15 @@
|
||||
(.setQueryData uri dt)))
|
||||
(.toString uri)))
|
||||
|
||||
(def default-headers
|
||||
{"x-frontend-version" (:full @cfg/version)})
|
||||
|
||||
(defn- fetch
|
||||
[{:keys [method uri query-string query headers body] :as request}
|
||||
{:keys [timeout credentials? response-type]
|
||||
:or {timeout 0 credentials? false response-type :text}}]
|
||||
(let [uri (create-uri uri query-string query)
|
||||
headers (merge default-headers headers)
|
||||
headers (if headers (clj->js headers) #js {})
|
||||
method (translate-method method)
|
||||
xhr (doto (XhrIo.)
|
||||
|
||||
23
manage.sh
23
manage.sh
@@ -114,15 +114,13 @@ function build-bundle {
|
||||
sed -i -re "s/\%version\%/$version/g" ./bundle/backend/main/app/config.clj;
|
||||
|
||||
local generate_tar=${PENPOT_BUNDLE_GENERATE_TAR:-"true"};
|
||||
|
||||
if [ $generate_tar == "true" ]; then
|
||||
pushd bundle/
|
||||
tar -cvf ../$name.tar *;
|
||||
tar -I lz4 -cvf ../$name.tar.lz4 *;
|
||||
popd
|
||||
|
||||
xz -vez1f -T4 $name.tar
|
||||
|
||||
echo "##############################################################";
|
||||
echo "# Generated $name.tar.xz";
|
||||
echo "# Generated $name.tar.lz4";
|
||||
echo "# Version $version";
|
||||
echo "##############################################################";
|
||||
fi
|
||||
@@ -137,19 +135,18 @@ function build-image {
|
||||
set -x
|
||||
pushd ./docker/images;
|
||||
docker buildx build --platform linux/amd64 -t $docker_image:$tag -f Dockerfile.$image .;
|
||||
docker tag $docker_image:$tag $docker_image:$version;
|
||||
|
||||
# docker tag $docker_image:$tag $docker_image:$version;
|
||||
# docker buildx build --platform linux/arm64 -t $docker_image:$version-arm64 .;
|
||||
popd;
|
||||
}
|
||||
|
||||
function build-images {
|
||||
local version="$CURRENT_VERSION";
|
||||
local bundle_file="penpot-$CURRENT_BRANCH.tar.xz";
|
||||
local bundle_file="penpot-$CURRENT_BRANCH.tar.lz4";
|
||||
|
||||
if [ $CURRENT_BRANCH != "main" ]; then
|
||||
version="$CURRENT_BRANCH-$CURRENT_VERSION";
|
||||
bundle_file="penpot-$CURRENT_BRANCH.tar.xz";
|
||||
bundle_file="penpot-$CURRENT_BRANCH.tar.lz4";
|
||||
fi;
|
||||
|
||||
if [ ! -f $bundle_file ]; then
|
||||
@@ -164,7 +161,7 @@ function build-images {
|
||||
mkdir -p ./docker/images/bundle;
|
||||
|
||||
pushd ./docker/images/bundle;
|
||||
tar xvf $bundle_file_path;
|
||||
tar -I lz4 -xvf $bundle_file_path;
|
||||
popd
|
||||
|
||||
build-image "backend" $CURRENT_BRANCH $version;
|
||||
@@ -180,9 +177,9 @@ function publish-latest-images {
|
||||
|
||||
set -x
|
||||
|
||||
docker tag $ORGANIZATION/frontend:$CURRENT_VERSION $ORGANIZATION/frontend:latest;
|
||||
docker tag $ORGANIZATION/backend:$CURRENT_VERSION $ORGANIZATION/backend:latest;
|
||||
docker tag $ORGANIZATION/exporter:$CURRENT_VERSION $ORGANIZATION/exporter:latest;
|
||||
docker tag $ORGANIZATION/frontend:$CURRENT_BRANCH $ORGANIZATION/frontend:latest;
|
||||
docker tag $ORGANIZATION/backend:$CURRENT_BRANCH $ORGANIZATION/backend:latest;
|
||||
docker tag $ORGANIZATION/exporter:$CURRENT_BRANCH $ORGANIZATION/exporter:latest;
|
||||
|
||||
# docker push $ORGANIZATION/frontend:$CURRENT_VERSION;
|
||||
# docker push $ORGANIZATION/backend:$CURRENT_VERSION;
|
||||
|
||||
Reference in New Issue
Block a user