Merge remote-tracking branch 'origin/staging' into main

This commit is contained in:
Andrey Antukh 2021-09-13 12:54:44 +02:00
commit ee6350189f
198 changed files with 12712 additions and 5323 deletions

View file

@ -23,6 +23,9 @@
{:unsorted-required-namespaces {:unsorted-required-namespaces
{:level :warning} {:level :warning}
:potok/reify-type
{:level :error}
:unresolved-namespace :unresolved-namespace
{:level :warning {:level :warning
:exclude [data_readers]} :exclude [data_readers]}

View file

@ -10,15 +10,34 @@
sname])] sname])]
{:node result})) {:node result}))
(def registry (atom {}))
(defn potok-reify (defn potok-reify
[{:keys [:node]}] [{:keys [:node :filename] :as params}]
(let [[rnode rtype & other] (:children node) (let [[rnode rtype & other] (:children node)
result (api/list-node rsym (symbol (str "event-type-" (name (:k rtype))))
(into [(api/token-node (symbol "deftype")) reg (get @registry filename #{})]
(api/token-node (gensym (name (:k rtype)))) (when-not (:namespaced? rtype)
(api/vector-node [])] (let [{:keys [:row :col]} (meta rtype)]
other))] (api/reg-finding! {:message "ptk/reify type should be namespaced"
{:node result})) :type :potok/reify-type
:row row
:col col})))
(if (contains? reg rsym)
(let [{:keys [:row :col]} (meta rtype)]
(api/reg-finding! {:message (str "duplicate type: " (name (:k rtype)))
:type :potok/reify-type
:row row
:col col}))
(swap! registry update filename (fnil conj #{}) rsym))
(let [result (api/list-node
(into [(api/token-node (symbol "deftype"))
(api/token-node rsym)
(api/vector-node [])]
other))]
{:node result})))
(defn clojure-specify (defn clojure-specify
[{:keys [:node]}] [{:keys [:node]}]

View file

@ -1,14 +1,62 @@
# CHANGELOG # # CHANGELOG
## :rocket: Next ## :rocket: Next
### :boom: Breaking changes
### :sparkles: New features ### :sparkles: New features
### :bug: Bugs fixed ### :bug: Bugs fixed
### :arrow_up: Deps updates ### :arrow_up: Deps updates
### :boom: Breaking changes ### :boom: Breaking changes
### :heart: Community contributions by (Thank you!) ### :heart: Community contributions by (Thank you!)
## 1.8.0-alpha
### :boom: Breaking changes
- This release includes a new approach for handling share links, and
this feature is incompatible with the previous one. This means that
all the public share links generated previously will stop working.
### :sparkles: New features
- Add tooltips to color picker tabs [Taiga #1814](https://tree.taiga.io/project/penpot/us/1814).
- Add styling to the end point of any open paths [Taiga #1107](https://tree.taiga.io/project/penpot/us/1107).
- Allow to zoom with ctrl + middle button [Taiga #1428](https://tree.taiga.io/project/penpot/us/1428).
- Auto placement of duplicated objects [Taiga #1386](https://tree.taiga.io/project/penpot/us/1386).
- Enable penpot SVG metadata only when exporting complete files [Taiga #1914](https://tree.taiga.io/project/penpot/us/1914?milestone=295883).
- Export to PDF all artboards of one page [Taiga #1895](https://tree.taiga.io/project/penpot/us/1895).
- Go to a undo step clicking on a history element of the list [Taiga #1374](https://tree.taiga.io/project/penpot/us/1374).
- Increment font size by 10 with shift+arrows [1047](https://github.com/penpot/penpot/issues/1047).
- New shortcut to detach components Ctrl+Shift+K [Taiga #1799](https://tree.taiga.io/project/penpot/us/1799).
- Set email inputs to type "email", to aid keyboard entry [Taiga #1921](https://tree.taiga.io/project/penpot/issue/1921).
- Use shift+move to move element orthogonally [#823](https://github.com/penpot/penpot/issues/823).
- Use space + mouse drag to pan, instead of only space [Taiga #1800](https://tree.taiga.io/project/penpot/us/1800).
- Allow navigate through pages on the viewer [Taiga #1550](https://tree.taiga.io/project/penpot/us/1550).
- Allow create share links with specific pages [Taiga #1844](https://tree.taiga.io/project/penpot/us/1844).
### :bug: Bugs fixed
- Prevent adding numeric suffix to layer names when not needed [Taiga #1929](https://tree.taiga.io/project/penpot/us/1929).
- Prevent deleting or moving the drafts project [Taiga #1935](https://tree.taiga.io/project/penpot/issue/1935).
- Fix problem with zoom and selection [Taiga #1919](https://tree.taiga.io/project/penpot/issue/1919)
- Fix problem with borders on shape export [#1092](https://github.com/penpot/penpot/issues/1092)
- Fix thumbnail cropping issue [Taiga #1964](https://tree.taiga.io/project/penpot/issue/1964)
- Fix repeated fetch on file selection [Taiga #1933](https://tree.taiga.io/project/penpot/issue/1933)
- Fix rename typography on text options [Taiga #1963](https://tree.taiga.io/project/penpot/issue/1963)
- Fix problems with order in groups [Taiga #1960](https://tree.taiga.io/project/penpot/issue/1960)
- Fix SVG components preview [#1134](https://github.com/penpot/penpot/issues/1134)
- Fix group renaming problem [Taiga #1969](https://tree.taiga.io/project/penpot/issue/1969)
- Fix problem with import broken images links [#1197](https://github.com/penpot/penpot/issues/1197)
- Fix problem while moving imported SVG's [#1199](https://github.com/penpot/penpot/issues/1199)
### :arrow_up: Deps updates
### :boom: Breaking changes
### :heart: Community contributions by (Thank you!)
- eduayme [#1129](https://github.com/penpot/penpot/pull/1129).
## 1.7.4-alpha ## 1.7.4-alpha
### :bug: Bugs fixed ### :bug: Bugs fixed
@ -43,7 +91,6 @@
- Update frontend build tooling. - Update frontend build tooling.
### :boom: Breaking changes
### :heart: Community contributions by (Thank you!) ### :heart: Community contributions by (Thank you!)
- soultipsy [#1100](https://github.com/penpot/penpot/pull/1100) - soultipsy [#1100](https://github.com/penpot/penpot/pull/1100)
@ -94,10 +141,6 @@
- Fix dynamic alignment enabled with hidden objects [#1063](https://github.com/penpot/penpot/issues/1063) - Fix dynamic alignment enabled with hidden objects [#1063](https://github.com/penpot/penpot/issues/1063)
### :arrow_up: Deps updates
### :boom: Breaking changes
### :heart: Community contributions by (Thank you!)
## 1.6.5-alpha ## 1.6.5-alpha
### :bug: Bugs fixed ### :bug: Bugs fixed

View file

@ -22,7 +22,7 @@
<mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text> <mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text>
<mj-text> <mj-text>
Thanks for signing up for your Penpot account! Please verify your Thanks for signing up for your Penpot account! Please verify your
email using the link below adn get started building mockups and email using the link below and get started building mockups and
prototypes today! prototypes today!
</mj-text> </mj-text>
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}"> <mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">

View file

@ -173,7 +173,7 @@
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below adn get started building mockups and prototypes today!</div> <div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below and get started building mockups and prototypes today!</div>
</td> </td>
</tr> </tr>
<tr> <tr>

View file

@ -1,7 +1,7 @@
Hello {{name}}! Hello {{name}}!
Thanks for signing up for your Penpot account! Please verify your email using the Thanks for signing up for your Penpot account! Please verify your email using the
link below adn get started building mockups and prototypes today! link below and get started building mockups and prototypes today!
{{ public-uri }}/#/auth/verify-token?token={{token}} {{ public-uri }}/#/auth/verify-token?token={{token}}

View file

@ -231,9 +231,9 @@
(defn get-by-params (defn get-by-params
([ds table params] ([ds table params]
(get-by-params ds table params nil)) (get-by-params ds table params nil))
([ds table params {:keys [uncheked] :or {uncheked false} :as opts}] ([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}]
(let [res (exec-one! ds (sql/select table params opts))] (let [res (exec-one! ds (sql/select table params opts))]
(when (and (not uncheked) (or (not res) (is-deleted? res))) (when (and check-not-found (or (not res) (is-deleted? res)))
(ex/raise :type :not-found (ex/raise :type :not-found
:table table :table table
:hint "database object not found")) :hint "database object not found"))
@ -267,13 +267,28 @@
(instance? PGpoint v)) (instance? PGpoint v))
(defn pgarray? (defn pgarray?
[v] ([v] (instance? PgArray v))
(instance? PgArray v)) ([v type]
(and (instance? PgArray v)
(= type (.getBaseTypeName ^PgArray v)))))
(defn pgarray-of-uuid? (defn pgarray-of-uuid?
[v] [v]
(and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v)))) (and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v))))
(defn decode-pgarray
([v] (into [] (.getArray ^PgArray v)))
([v in] (into in (.getArray ^PgArray v)))
([v in xf] (into in xf (.getArray ^PgArray v))))
(defn pgarray->set
[v]
(set (.getArray ^PgArray v)))
(defn pgarray->vector
[v]
(vec (.getArray ^PgArray v)))
(defn pgpoint (defn pgpoint
[p] [p]
(PGpoint. (:x p) (:y p))) (PGpoint. (:x p) (:y p)))
@ -285,7 +300,6 @@
(.createArrayOf conn ^String type (into-array Object objects)) (.createArrayOf conn ^String type (into-array Object objects))
(.createArrayOf conn ^String type objects)))) (.createArrayOf conn ^String type objects))))
(defn decode-pgpoint (defn decode-pgpoint
[^PGpoint v] [^PGpoint v]
(gpt/point (.-x v) (.-y v))) (gpt/point (.-x v) (.-y v)))
@ -369,15 +383,6 @@
(.setType "jsonb") (.setType "jsonb")
(.setValue (json/encode-str data)))) (.setValue (json/encode-str data))))
(defn pgarray->set
[v]
(set (.getArray ^PgArray v)))
(defn pgarray->vector
[v]
(vec (.getArray ^PgArray v)))
;; --- Locks ;; --- Locks
(defn- xact-check-param (defn- xact-check-param

View file

@ -114,9 +114,14 @@
(s/def ::storage map?) (s/def ::storage map?)
(s/def ::assets map?) (s/def ::assets map?)
(s/def ::feedback fn?) (s/def ::feedback fn?)
(s/def ::error-report-handler fn?)
(s/def ::audit-http-handler fn?)
(defmethod ig/pre-init-spec ::router [_] (defmethod ig/pre-init-spec ::router [_]
(s/keys :req-un [::rpc ::session ::mtx/metrics ::oauth ::storage ::assets ::feedback])) (s/keys :req-un [::rpc ::session ::mtx/metrics
::oauth ::storage ::assets ::feedback
::error-report-handler
::audit-http-handler]))
(defmethod ig/init-key ::router (defmethod ig/init-key ::router
[_ {:keys [session rpc oauth metrics assets feedback] :as cfg}] [_ {:keys [session rpc oauth metrics assets feedback] :as cfg}]
@ -136,9 +141,7 @@
["/webhooks" ["/webhooks"
["/sns" {:post (:sns-webhook cfg)}]] ["/sns" {:post (:sns-webhook cfg)}]]
["/api" {:middleware [ ["/api" {:middleware [[middleware/etag]
;; Temporary disabled
#_[middleware/etag]
[middleware/format-response-body] [middleware/format-response-body]
[middleware/params] [middleware/params]
[middleware/multipart-params] [middleware/multipart-params]
@ -149,10 +152,12 @@
["/feedback" {:middleware [(:middleware session)] ["/feedback" {:middleware [(:middleware session)]
:post feedback}] :post feedback}]
["/auth/oauth/:provider" {:post (:handler oauth)}] ["/auth/oauth/:provider" {:post (:handler oauth)}]
["/auth/oauth/:provider/callback" {:get (:callback-handler oauth)}] ["/auth/oauth/:provider/callback" {:get (:callback-handler oauth)}]
["/audit/events" {:middleware [(:middleware session)]
:post (:audit-http-handler cfg)}]
["/rpc" {:middleware [(:middleware session)]} ["/rpc" {:middleware [(:middleware session)]}
["/query/:type" {:get (:query-handler rpc) ["/query/:type" {:get (:query-handler rpc)
:post (:query-handler rpc)}] :post (:query-handler rpc)}]

View file

@ -13,7 +13,6 @@
[buddy.core.codecs :as bc] [buddy.core.codecs :as bc]
[buddy.core.hash :as bh] [buddy.core.hash :as bh]
[clojure.java.io :as io] [clojure.java.io :as io]
[ring.core.protocols :as rp]
[ring.middleware.cookies :refer [wrap-cookies]] [ring.middleware.cookies :refer [wrap-cookies]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]] [ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.multipart-params :refer [wrap-multipart-params]] [ring.middleware.multipart-params :refer [wrap-multipart-params]]
@ -74,33 +73,15 @@
{:name ::parse-request-body {:name ::parse-request-body
:compile (constantly wrap-parse-request-body)}) :compile (constantly wrap-parse-request-body)})
(defn- transit-streamable-body
[data opts]
(reify rp/StreamableResponseBody
(write-body-to-stream [_ response output-stream]
(try
(let [tw (t/writer output-stream opts)]
(t/write! tw data))
(finally
(.close ^java.io.OutputStream output-stream))))))
(defn- impl-format-response-body (defn- impl-format-response-body
[response _request] [response _request]
(let [body (:body response) (let [body (:body response)
opts {:type :json-verbose}] opts {:type :json}]
(cond (cond
(coll? body) (coll? body)
(-> response (-> response
(update :headers assoc "content-type" "application/transit+json") (update :headers assoc "content-type" "application/transit+json")
(assoc :body (transit-streamable-body body opts))) (assoc :body (t/encode body opts)))
;; ;; Temporary disabled
;; (-> response
;; (update :headers assoc "content-type" "application/transit+json")
;; (assoc :body
;; (if (= :post (:request-method request))
;; (transit-streamable-body body opts)
;; (t/encode body opts))))
(nil? body) (nil? body)
(assoc response :status 204 :body "") (assoc response :status 204 :body "")

View file

@ -23,7 +23,8 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
[lambdaisland.uri :as u])) [lambdaisland.uri :as u]
[promesa.exec :as px]))
(defn parse-client-ip (defn parse-client-ip
[{:keys [headers] :as request}] [{:keys [headers] :as request}]
@ -67,6 +68,65 @@
(update event :props #(-> % clean-common clean-profile-id clean-complex-data)))) (update event :props #(-> % clean-common clean-profile-id clean-complex-data))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP Handler
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare persist-http-events)
(s/def ::profile-id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::type ::us/string)
(s/def ::props (s/map-of ::us/keyword any?))
(s/def ::timestamp dt/instant?)
(s/def ::context (s/map-of ::us/keyword any?))
(s/def ::event
(s/keys :req-un [::type ::name ::props ::timestamp ::profile-id]
:opt-un [::context]))
(s/def ::events (s/every ::event))
(defmethod ig/init-key ::http-handler
[_ {:keys [executor enabled] :as cfg}]
(fn [{:keys [params _headers _cookies profile-id] :as request}]
(when enabled
(let [events (->> (:events params)
(remove #(not= profile-id (:profile-id %)))
(us/conform ::events))
ip-addr (parse-client-ip request)
cfg (-> cfg
(assoc :source "frontend")
(assoc :events events)
(assoc :ip-addr ip-addr))]
(px/run! executor #(persist-http-events cfg))))
{:status 204 :body ""}))
(defn- persist-http-events
[{:keys [pool events ip-addr source] :as cfg}]
(try
(let [columns [:id :name :source :type :tracked-at :profile-id :ip-addr :props :context]
prepare-xf (map (fn [event]
[(uuid/next)
(:name event)
source
(:type event)
(:timestamp event)
(:profile-id event)
(db/inet ip-addr)
(db/tjson (:props event))
(db/tjson (d/without-nils (:context event)))]))
events (us/conform ::events events)
rows (into [] prepare-xf events)]
(db/insert-multi! pool :audit-log columns rows))
(catch Throwable e
(let [xdata (ex-data e)]
(if (= :spec-validation (:code xdata))
(l/error ::l/raw (str "spec validation on persist-events:\n"
(:explain xdata)))
(l/error :hint "error on persist-events"
:cause e))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Collector ;; Collector
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -103,7 +163,9 @@
(recur))) (recur)))
(fn [& {:keys [cmd] :as params}] (fn [& {:keys [cmd] :as params}]
(let [params (dissoc params :cmd)] (let [params (-> params
(dissoc :cmd)
(assoc :tracked-at (dt/now)))]
(case cmd (case cmd
:stop (a/close! input) :stop (a/close! input)
:submit (when-not (a/offer! input params) :submit (when-not (a/offer! input params)
@ -117,13 +179,14 @@
(:name event) (:name event)
(:type event) (:type event)
(:profile-id event) (:profile-id event)
(:tracked-at event)
(some-> (:ip-addr event) db/inet) (some-> (:ip-addr event) db/inet)
(db/tjson (:props event))])] (db/tjson (:props event))
"backend"])]
(aa/with-thread executor (aa/with-thread executor
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(db/insert-multi! conn :audit-log (db/insert-multi! conn :audit-log
[:id :name :type :profile-id :ip-addr :props] [:id :name :type :profile-id :tracked-at :ip-addr :props :source]
(sequence (map event->row) events)))))) (sequence (map event->row) events))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -144,16 +207,22 @@
(defmethod ig/init-key ::archive-task (defmethod ig/init-key ::archive-task
[_ {:keys [uri enabled] :as cfg}] [_ {:keys [uri enabled] :as cfg}]
(fn [_] (fn [props]
(when (and enabled (not uri)) ;; NOTE: this let allows overwrite default configured values from
(ex/raise :type :internal ;; the repl, when manually invoking the task.
:code :task-not-configured (let [enabled (or enabled (:enabled props false))
:hint "archive task not configured, missing uri")) uri (or uri (:uri props))
(loop [] cfg (assoc cfg :uri uri)]
(let [res (archive-events cfg)] (when (and enabled (not uri))
(when (= res :continue) (ex/raise :type :internal
(aa/thread-sleep 200) :code :task-not-configured
(recur)))))) :hint "archive task not configured, missing uri"))
(when enabled
(loop []
(let [res (archive-events cfg)]
(when (= res :continue)
(aa/thread-sleep 200)
(recur))))))))
(def sql:retrieve-batch-of-audit-log (def sql:retrieve-batch-of-audit-log
"select * from audit_log "select * from audit_log
@ -164,22 +233,27 @@
(defn archive-events (defn archive-events
[{:keys [pool uri tokens] :as cfg}] [{:keys [pool uri tokens] :as cfg}]
(letfn [(decode-row [{:keys [props ip-addr] :as row}] (letfn [(decode-row [{:keys [props ip-addr context] :as row}]
(cond-> row (cond-> row
(db/pgobject? props) (db/pgobject? props)
(assoc :props (db/decode-transit-pgobject props)) (assoc :props (db/decode-transit-pgobject props))
(db/pgobject? context)
(assoc :context (db/decode-transit-pgobject context))
(db/pgobject? ip-addr "inet") (db/pgobject? ip-addr "inet")
(assoc :ip-addr (db/decode-inet ip-addr)))) (assoc :ip-addr (db/decode-inet ip-addr))))
(row->event [{:keys [name type created-at profile-id props ip-addr]}] (row->event [row]
(cond-> {:type type (select-keys row [:type
:name name :name
:timestamp created-at :source
:profile-id profile-id :created-at
:props props} :tracked-at
(some? ip-addr) :profile-id
(update :context assoc :source-ip ip-addr))) :ip-addr
:props
:context]))
(send [events] (send [events]
(let [token (tokens :generate {:iss "authentication" (let [token (tokens :generate {:iss "authentication"

View file

@ -28,11 +28,24 @@
{:name "actions_profile_register_count" {:name "actions_profile_register_count"
:help "A global counter of user registrations." :help "A global counter of user registrations."
:type :counter} :type :counter}
:profile-activation :profile-activation
{:name "actions_profile_activation_count" {:name "actions_profile_activation_count"
:help "A global counter of profile activations" :help "A global counter of profile activations"
:type :counter}
:update-file-changes
{:name "rpc_update_file_changes_total"
:help "A total number of changes submitted to update-file."
:type :counter}
:update-file-bytes-processed
{:name "rpc_update_file_bytes_processed_total"
:help "A total number of bytes processed by update-file."
:type :counter}}} :type :counter}}}
:app.migrations/all :app.migrations/all
{:main (ig/ref :app.migrations/migrations)} {:main (ig/ref :app.migrations/migrations)}
@ -95,6 +108,7 @@
:storage (ig/ref :app.storage/storage) :storage (ig/ref :app.storage/storage)
:sns-webhook (ig/ref :app.http.awsns/handler) :sns-webhook (ig/ref :app.http.awsns/handler)
:feedback (ig/ref :app.http.feedback/handler) :feedback (ig/ref :app.http.feedback/handler)
:audit-http-handler (ig/ref :app.loggers.audit/http-handler)
:error-report-handler (ig/ref :app.loggers.mattermost/handler)} :error-report-handler (ig/ref :app.loggers.mattermost/handler)}
:app.http.assets/handlers :app.http.assets/handlers
@ -289,6 +303,11 @@
:app.loggers.zmq/receiver :app.loggers.zmq/receiver
{:endpoint (cf/get :loggers-zmq-uri)} {:endpoint (cf/get :loggers-zmq-uri)}
:app.loggers.audit/http-handler
{:enabled (cf/get :audit-enabled false)
:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)}
:app.loggers.audit/collector :app.loggers.audit/collector
{:enabled (cf/get :audit-enabled false) {:enabled (cf/get :audit-enabled false)
:pool (ig/ref :app.db/pool) :pool (ig/ref :app.db/pool)

View file

@ -92,18 +92,14 @@
_ (when (seq labels) _ (when (seq labels)
(.labelNames instance (into-array String labels))) (.labelNames instance (into-array String labels)))
instance (.register instance registry)] instance (.register instance registry)]
(reify
clojure.lang.IDeref
(deref [_] instance)
clojure.lang.IFn {::instance instance
(invoke [_ cmd] ::fn (fn [{:keys [by labels] :or {by 1}}]
(.inc ^Counter instance)) (if labels
(.. ^Counter instance
(invoke [_ cmd labels] (labels (into-array String labels))
(.. ^Counter instance (inc by))
(labels (into-array String labels)) (.inc ^Counter instance by)))}))
(inc))))))
(defn make-gauge (defn make-gauge
[{:keys [name help registry reg labels] :as props}] [{:keys [name help registry reg labels] :as props}]
@ -115,21 +111,16 @@
(.labelNames instance (into-array String labels))) (.labelNames instance (into-array String labels)))
instance (.register instance registry)] instance (.register instance registry)]
(reify {::instance instance
clojure.lang.IDeref ::fn (fn [{:keys [cmd by labels] :or {by 1}}]
(deref [_] instance) (if labels
(let [labels (into-array String [labels])]
clojure.lang.IFn (case cmd
(invoke [_ cmd] :inc (.. ^Gauge instance (labels labels) (inc by))
(case cmd :dec (.. ^Gauge instance (labels labels) (dec by))))
:inc (.inc ^Gauge instance) (case cmd
:dec (.dec ^Gauge instance))) :inc (.inc ^Gauge instance by)
:dec (.dec ^Gauge instance by))))}))
(invoke [_ cmd labels]
(let [labels (into-array String [labels])]
(case cmd
:inc (.. ^Gauge instance (labels labels) (inc))
:dec (.. ^Gauge instance (labels labels) (dec))))))))
(def default-quantiles (def default-quantiles
[[0.75 0.02] [[0.75 0.02]
@ -150,18 +141,14 @@
_ (when (seq labels) _ (when (seq labels)
(.labelNames instance (into-array String labels))) (.labelNames instance (into-array String labels)))
instance (.register instance registry)] instance (.register instance registry)]
(reify
clojure.lang.IDeref
(deref [_] instance)
clojure.lang.IFn {::instance instance
(invoke [_ cmd val] ::fn (fn [{:keys [val labels]}]
(.observe ^Summary instance val)) (if labels
(.. ^Summary instance
(invoke [_ cmd val labels] (labels (into-array String labels))
(.. ^Summary instance (observe val))
(labels (into-array String labels)) (.observe ^Summary instance val)))}))
(observe val))))))
(def default-histogram-buckets (def default-histogram-buckets
[1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500]) [1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
@ -177,18 +164,14 @@
_ (when (seq labels) _ (when (seq labels)
(.labelNames instance (into-array String labels))) (.labelNames instance (into-array String labels)))
instance (.register instance registry)] instance (.register instance registry)]
(reify
clojure.lang.IDeref
(deref [_] instance)
clojure.lang.IFn {::instance instance
(invoke [_ cmd val] ::fn (fn [{:keys [val labels]}]
(.observe ^Histogram instance val)) (if labels
(.. ^Histogram instance
(invoke [_ cmd val labels] (labels (into-array String labels))
(.. ^Histogram instance (observe val))
(labels (into-array String labels)) (.observe ^Histogram instance val)))}))
(observe val))))))
(defn create (defn create
[{:keys [type] :as props}] [{:keys [type] :as props}]
@ -205,19 +188,19 @@
(with-meta (with-meta
(fn (fn
([a] ([a]
(mobj :inc) ((::fn mobj) nil)
(origf a)) (origf a))
([a b] ([a b]
(mobj :inc) ((::fn mobj) nil)
(origf a b)) (origf a b))
([a b c] ([a b c]
(mobj :inc) ((::fn mobj) nil)
(origf a b c)) (origf a b c))
([a b c d] ([a b c d]
(mobj :inc) ((::fn mobj) nil)
(origf a b c d)) (origf a b c d))
([a b c d & more] ([a b c d & more]
(mobj :inc) ((::fn mobj) nil)
(apply origf a b c d more))) (apply origf a b c d more)))
(assoc mdata ::original origf)))) (assoc mdata ::original origf))))
([rootf mobj labels] ([rootf mobj labels]
@ -226,13 +209,13 @@
(with-meta (with-meta
(fn (fn
([a] ([a]
(mobj :inc labels) ((::fn mobj) {:labels labels})
(origf a)) (origf a))
([a b] ([a b]
(mobj :inc labels) ((::fn mobj) {:labels labels})
(origf a b)) (origf a b))
([a b & more] ([a b & more]
(mobj :inc labels) ((::fn mobj) {:labels labels})
(apply origf a b more))) (apply origf a b more)))
(assoc mdata ::original origf))))) (assoc mdata ::original origf)))))
@ -245,15 +228,15 @@
([a] ([a]
(with-measure (with-measure
:expr (origf a) :expr (origf a)
:cb #(mobj :observe %))) :cb #((::fn mobj) {:val %})))
([a b] ([a b]
(with-measure (with-measure
:expr (origf a b) :expr (origf a b)
:cb #(mobj :observe %))) :cb #((::fn mobj) {:val %})))
([a b & more] ([a b & more]
(with-measure (with-measure
:expr (apply origf a b more) :expr (apply origf a b more)
:cb #(mobj :observe %)))) :cb #((::fn mobj) {:val %}))))
(assoc mdata ::original origf)))) (assoc mdata ::original origf))))
([rootf mobj labels] ([rootf mobj labels]
@ -264,26 +247,26 @@
([a] ([a]
(with-measure (with-measure
:expr (origf a) :expr (origf a)
:cb #(mobj :observe % labels))) :cb #((::fn mobj) {:val % :labels labels})))
([a b] ([a b]
(with-measure (with-measure
:expr (origf a b) :expr (origf a b)
:cb #(mobj :observe % labels))) :cb #((::fn mobj) {:val % :labels labels})))
([a b & more] ([a b & more]
(with-measure (with-measure
:expr (apply origf a b more) :expr (apply origf a b more)
:cb #(mobj :observe % labels)))) :cb #((::fn mobj) {:val % :labels labels}))))
(assoc mdata ::original origf))))) (assoc mdata ::original origf)))))
(defn instrument-vars! (defn instrument-vars!
[vars {:keys [wrap] :as props}] [vars {:keys [wrap] :as props}]
(let [obj (create props)] (let [obj (create props)]
(cond (cond
(instance? Counter @obj) (instance? Counter (::instance obj))
(doseq [var vars] (doseq [var vars]
(alter-var-root var (or wrap wrap-counter) obj)) (alter-var-root var (or wrap wrap-counter) obj))
(instance? Summary @obj) (instance? Summary (::instance obj))
(doseq [var vars] (doseq [var vars]
(alter-var-root var (or wrap wrap-summary) obj)) (alter-var-root var (or wrap wrap-summary) obj))
@ -294,13 +277,13 @@
[f {:keys [wrap] :as props}] [f {:keys [wrap] :as props}]
(let [obj (create props)] (let [obj (create props)]
(cond (cond
(instance? Counter @obj) (instance? Counter (::instance obj))
((or wrap wrap-counter) f obj) ((or wrap wrap-counter) f obj)
(instance? Summary @obj) (instance? Summary (::instance obj))
((or wrap wrap-summary) f obj) ((or wrap wrap-summary) f obj)
(instance? Histogram @obj) (instance? Histogram (::instance obj))
((or wrap wrap-summary) f obj) ((or wrap wrap-summary) f obj)
:else :else

View file

@ -193,6 +193,15 @@
{:name "0061-mod-file-table" {:name "0061-mod-file-table"
:fn (mg/resource "app/migrations/sql/0061-mod-file-table.sql")} :fn (mg/resource "app/migrations/sql/0061-mod-file-table.sql")}
{:name "0062-fix-metadata-media"
:fn (mg/resource "app/migrations/sql/0062-fix-metadata-media.sql")}
{:name "0063-add-share-link-table"
:fn (mg/resource "app/migrations/sql/0063-add-share-link-table.sql")}
{:name "0064-mod-audit-log-table"
:fn (mg/resource "app/migrations/sql/0064-mod-audit-log-table.sql")}
]) ])

View file

@ -0,0 +1,12 @@
CREATE TABLE share_link (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE DEFERRABLE,
owner_id uuid NULL REFERENCES profile(id) ON DELETE SET NULL DEFERRABLE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
pages uuid[],
flags text[]
);
CREATE INDEX share_link_file_id_idx ON share_link(file_id);
CREATE INDEX share_link_owner_id_idx ON share_link(owner_id);

View file

@ -0,0 +1,13 @@
ALTER TABLE audit_log
ADD COLUMN tracked_at timestamptz NULL DEFAULT clock_timestamp(),
ADD COLUMN source text NULL,
ADD COLUMN context jsonb NULL;
ALTER TABLE audit_log
ALTER COLUMN source SET STORAGE external,
ALTER COLUMN context SET STORAGE external;
UPDATE audit_log SET source = 'backend', tracked_at=created_at;
-- ALTER TABLE audit_log ALTER COLUMN source SET NOT NULL;
-- ALTER TABLE audit_log ALTER COLUMN tracked_at SET NOT NULL;

View file

@ -117,14 +117,14 @@
profile-id (or (:profile-id params') profile-id (or (:profile-id params')
(:profile-id result) (:profile-id result)
(::audit/profile-id resultm)) (::audit/profile-id resultm))
props (d/merge params (::audit/props resultm))] props (d/merge params' (::audit/props resultm))]
(audit :cmd :submit (audit :cmd :submit
:type (::type cfg) :type (::type cfg)
:name (or (::audit/name resultm) :name (or (::audit/name resultm)
(::sv/name mdata)) (::sv/name mdata))
:profile-id profile-id :profile-id profile-id
:ip-addr (audit/parse-client-ip request) :ip-addr (audit/parse-client-ip request)
:props (audit/profile->props props)))) :props props)))
result)))) result))))
@ -175,6 +175,7 @@
'app.rpc.mutations.management 'app.rpc.mutations.management
'app.rpc.mutations.ldap 'app.rpc.mutations.ldap
'app.rpc.mutations.fonts 'app.rpc.mutations.fonts
'app.rpc.mutations.share-link
'app.rpc.mutations.verify-token) 'app.rpc.mutations.verify-token)
(map (partial process-method cfg)) (map (partial process-method cfg))
(into {})))) (into {}))))

View file

@ -12,6 +12,7 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.metrics :as mtx]
[app.rpc.permissions :as perms] [app.rpc.permissions :as perms]
[app.rpc.queries.files :as files] [app.rpc.queries.files :as files]
[app.rpc.queries.projects :as proj] [app.rpc.queries.projects :as proj]
@ -291,7 +292,7 @@
(simpl/del-object backend file))) (simpl/del-object backend file)))
(defn- update-file (defn- update-file
[{:keys [conn] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}] [{:keys [conn metrics] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}]
(when (> (:revn params) (when (> (:revn params)
(:revn file)) (:revn file))
@ -301,14 +302,22 @@
:context {:incoming-revn (:revn params) :context {:incoming-revn (:revn params)
:stored-revn (:revn file)})) :stored-revn (:revn file)}))
(let [changes (if changes-with-metadata (let [mtx1 (get-in metrics [:definitions :update-file-changes])
mtx2 (get-in metrics [:definitions :update-file-bytes-processed])
changes (if changes-with-metadata
(mapcat :changes changes-with-metadata) (mapcat :changes changes-with-metadata)
changes) changes)
;; Trace the number of changes processed
_ ((::mtx/fn mtx1) {:by (count changes)})
ts (dt/now) ts (dt/now)
file (-> (files/retrieve-data cfg file) file (-> (files/retrieve-data cfg file)
(update :revn inc) (update :revn inc)
(update :data (fn [data] (update :data (fn [data]
;; Trace the length of bytes of processed data
((::mtx/fn mtx2) {:by (alength data)})
(-> data (-> data
(blob/decode) (blob/decode)
(assoc :id (:id file)) (assoc :id (:id file))

View file

@ -15,6 +15,7 @@
[app.http.oauth :refer [extract-props]] [app.http.oauth :refer [extract-props]]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.media :as media] [app.media :as media]
[app.metrics :as mtx]
[app.rpc.mutations.projects :as projects] [app.rpc.mutations.projects :as projects]
[app.rpc.mutations.teams :as teams] [app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile] [app.rpc.queries.profile :as profile]
@ -150,7 +151,8 @@
transaction is completed." transaction is completed."
[metrics] [metrics]
(fn [] (fn []
((get-in metrics [:definitions :profile-register]) :inc))) (let [mobj (get-in metrics [:definitions :profile-register])]
((::mtx/fn mobj) {:by 1}))))
(defn register-profile (defn register-profile
[{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}] [{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]

View file

@ -0,0 +1,67 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.rpc.mutations.share-link
"Share link related rpc mutation methods."
(:require
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc.queries.files :as files]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::flags (s/every ::us/string :kind set?))
(s/def ::pages (s/every ::us/uuid :kind set?))
;; --- Mutation: Create Share Link
(declare create-share-link)
(s/def ::create-share-link
(s/keys :req-un [::profile-id ::file-id ::flags]
:opt-un [::pages]))
(sv/defmethod ::create-share-link
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(create-share-link conn params)))
(defn create-share-link
[conn {:keys [profile-id file-id pages flags]}]
(let [pages (db/create-array conn "uuid" pages)
flags (->> (map name flags)
(db/create-array conn "text"))
slink (db/insert! conn :share-link
{:id (uuid/next)
:file-id file-id
:flags flags
:pages pages
:owner-id profile-id})]
(-> slink
(update :pages db/decode-pgarray #{})
(update :flags db/decode-pgarray #{}))))
;; --- Mutation: Delete Share Link
(declare delete-share-link)
(s/def ::delete-share-link
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::delete-share-link
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [slink (db/get-by-id conn :share-link id)]
(files/check-edition-permissions! conn profile-id (:file-id slink))
(db/delete! conn :share-link {:id id})
nil)))

View file

@ -9,6 +9,7 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.db :as db] [app.db :as db]
[app.metrics :as mtx]
[app.rpc.mutations.teams :as teams] [app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile] [app.rpc.queries.profile :as profile]
[app.util.services :as sv] [app.util.services :as sv]
@ -42,7 +43,8 @@
transaction is completed." transaction is completed."
[metrics] [metrics]
(fn [] (fn []
((get-in metrics [:definitions :profile-activation]) :inc))) (let [mobj (get-in metrics [:definitions :profile-activation])]
((::mtx/fn mobj) {:by 1}))))
(defmethod process-token :verify-email (defmethod process-token :verify-email
[{:keys [conn session metrics] :as cfg} _ {:keys [profile-id] :as claims}] [{:keys [conn session metrics] :as cfg} _ {:keys [profile-id] :as claims}]

View file

@ -37,6 +37,41 @@
:is-admin false :is-admin false
:can-edit false))) :can-edit false)))
(defn make-edition-predicate-fn
"A simple factory for edition permission predicate functions."
[qfn]
(us/assert fn? qfn)
(fn [& args]
(let [rows (apply qfn args)]
(when-not (or (empty? rows)
(not (or (some :can-edit rows)
(some :is-admin rows)
(some :is-owner rows))))
rows))))
(defn make-read-predicate-fn
"A simple factory for read permission predicate functions."
[qfn]
(us/assert fn? qfn)
(fn [& args]
(let [rows (apply qfn args)]
(when (seq rows)
rows))))
(defn make-check-fn
"Helper that converts a predicate permission function to a check
function (function that raises an exception)."
[pred]
(fn [& args]
(when-not (seq (apply pred args))
(ex/raise :type :not-found
:code :object-not-found
:hint "not found"))))
;; TODO: the following functions are deprecated and replaced with the
;; new ones. Should not be used.
(defn make-edition-check-fn (defn make-edition-check-fn
"A simple factory for edition permission check functions." "A simple factory for edition permission check functions."
[qfn] [qfn]

View file

@ -61,16 +61,23 @@
(defn- retrieve-file-permissions (defn- retrieve-file-permissions
[conn profile-id file-id] [conn profile-id file-id]
(db/exec! conn [sql:file-permissions (when (and profile-id file-id)
file-id profile-id (db/exec! conn [sql:file-permissions
file-id profile-id file-id profile-id
file-id profile-id])) file-id profile-id
file-id profile-id])))
(def has-edit-permissions?
(perms/make-edition-predicate-fn retrieve-file-permissions))
(def has-read-permissions?
(perms/make-read-predicate-fn retrieve-file-permissions))
(def check-edition-permissions! (def check-edition-permissions!
(perms/make-edition-check-fn retrieve-file-permissions)) (perms/make-check-fn has-edit-permissions?))
(def check-read-permissions! (def check-read-permissions!
(perms/make-read-check-fn retrieve-file-permissions)) (perms/make-check-fn has-read-permissions?))
;; --- Query: Files search ;; --- Query: Files search

View file

@ -14,24 +14,98 @@
[app.util.services :as sv] [app.util.services :as sv]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
;; --- Query: View Only Bundle
(defn- decode-share-link-row
[row]
(-> row
(update :flags db/decode-pgarray #{})
(update :pages db/decode-pgarray #{})))
(defn- retrieve-project
[conn id]
(db/get-by-id conn :project id {:columns [:id :name :team-id]}))
(defn- retrieve-share-link
[{:keys [conn]} file-id id]
(some-> (db/get-by-params conn :share-link
{:id id :file-id file-id}
{:check-not-found false})
(decode-share-link-row)))
(defn- retrieve-bundle
[{:keys [conn] :as cfg} file-id]
(let [file (files/retrieve-file cfg file-id)
project (retrieve-project conn (:project-id file))
libs (files/retrieve-file-libraries cfg false file-id)
users (teams/retrieve-users conn (:team-id project))
links (->> (db/query conn :share-link {:file-id file-id})
(mapv decode-share-link-row))
fonts (db/query conn :team-font-variant
{:team-id (:team-id project)
:deleted-at nil})]
{:file file
:users users
:fonts fonts
:project project
:share-links links
:libraries libs}))
(s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::share-id ::us/uuid)
(s/def ::view-only-bundle
(s/keys :req-un [::file-id] :opt-un [::profile-id ::share-id]))
(sv/defmethod ::view-only-bundle {:auth false}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)
bundle (retrieve-bundle cfg file-id)
slink (retrieve-share-link cfg file-id share-id)]
;; When we have neither profile nor share, we just return a not
;; found response to the user.
(when (and (not profile-id)
(not slink))
(ex/raise :type :not-found
:code :object-not-found))
;; When we have only profile, we need to check read permissiones
;; on file.
(when (and profile-id (not slink))
(files/check-read-permissions! conn profile-id file-id))
(cond-> bundle
;; If we have current profile, put
(some? profile-id)
(as-> $ (let [edit? (boolean (files/has-edit-permissions? conn profile-id file-id))
read? (boolean (files/has-read-permissions? conn profile-id file-id))]
(-> (assoc $ :permissions {:read read? :edit edit?})
(cond-> (not edit?) (dissoc :share-links)))))
(some? slink)
(assoc :share slink)
(and (some? slink)
(not (contains? (:flags slink) "view-all-pages")))
(update-in [:file :data] (fn [data]
(let [allowed-pages (:pages slink)]
(-> data
(update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages)))
(update :pages-index (fn [index] (select-keys index allowed-pages)))))))))))
;; --- Query: Viewer Bundle (by Page ID) ;; --- Query: Viewer Bundle (by Page ID)
;; DEPRECATED: should be removed in 1.9.x
(declare check-shared-token!) (declare check-shared-token!)
(declare retrieve-shared-token) (declare retrieve-shared-token)
(def ^:private
sql:project
"select p.id, p.name, p.team_id
from project as p
where p.id = ?
and p.deleted_at is null")
(defn- retrieve-project
[conn id]
(db/exec-one! conn [sql:project id]))
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::page-id ::us/uuid) (s/def ::page-id ::us/uuid)
(s/def ::token ::us/string) (s/def ::token ::us/string)
@ -81,6 +155,3 @@
[conn file-id page-id] [conn file-id page-id]
(let [sql "select * from file_share_token where file_id=? and page_id=?"] (let [sql "select * from file_share_token where file_id=? and page_id=?"]
(db/exec-one! conn [sql file-id page-id]))) (db/exec-one! conn [sql file-id page-id])))

View file

@ -60,7 +60,7 @@
(defmethod handle-deletion :team-font-variant (defmethod handle-deletion :team-font-variant
[{:keys [conn storage]} {:keys [id] :as props}] [{:keys [conn storage]} {:keys [id] :as props}]
(let [font (db/get-by-id conn :team-font-variant id {:uncheked true}) (let [font (db/get-by-id conn :team-font-variant id {:check-not-found false})
storage (assoc storage :conn conn)] storage (assoc storage :conn conn)]
(when (:deleted-at font) (when (:deleted-at font)
(db/delete! conn :team-font-variant {:id id}) (db/delete! conn :team-font-variant {:id id})

View file

@ -16,18 +16,18 @@
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest retrieve-bundle (t/deftest retrieve-bundle
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
prof2 (th/create-profile* 2 {:is-active true}) prof2 (th/create-profile* 2 {:is-active true})
team-id (:default-team-id prof) team-id (:default-team-id prof)
proj-id (:default-project-id prof) proj-id (:default-project-id prof)
file (th/create-file* 1 {:profile-id (:id prof) file (th/create-file* 1 {:profile-id (:id prof)
:project-id proj-id :project-id proj-id
:is-shared false}) :is-shared false})
token (atom nil)] share-id (atom nil)]
(t/testing "authenticated with page-id" (t/testing "authenticated with page-id"
(let [data {::th/type :viewer-bundle (let [data {::th/type :view-only-bundle
:profile-id (:id prof) :profile-id (:id prof)
:file-id (:id file) :file-id (:id file)
:page-id (get-in file [:data :pages 0])} :page-id (get-in file [:data :pages 0])}
@ -38,64 +38,67 @@
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (contains? result :token)) (t/is (contains? result :share-links))
(t/is (contains? result :page)) (t/is (contains? result :permissions))
(t/is (contains? result :libraries))
(t/is (contains? result :file)) (t/is (contains? result :file))
(t/is (contains? result :project))))) (t/is (contains? result :project)))))
(t/testing "generate share token" (t/testing "generate share token"
(let [data {::th/type :create-file-share-token (let [data {::th/type :create-share-link
:profile-id (:id prof) :profile-id (:id prof)
:file-id (:id file) :file-id (:id file)
:page-id (get-in file [:data :pages 0])} :pages #{(get-in file [:data :pages 0])}
:flags #{}}
out (th/mutation! data)] out (th/mutation! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (string? (:token result))) (t/is (uuid? (:id result)))
(reset! token (:token result))))) (reset! share-id (:id result)))))
(t/testing "not authenticated with page-id" (t/testing "not authenticated with page-id"
(let [data {::th/type :viewer-bundle (let [data {::th/type :view-only-bundle
:profile-id (:id prof2) :profile-id (:id prof2)
:file-id (:id file) :file-id (:id file)
:page-id (get-in file [:data :pages 0])} :page-id (get-in file [:data :pages 0])}
out (th/query! data)] out (th/query! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(let [error (:error out) (let [error (:error out)
error-data (ex-data error)] error-data (ex-data error)]
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found)) (t/is (= (:type error-data) :not-found))
(t/is (= (:code error-data) :object-not-found))))) (t/is (= (:code error-data) :object-not-found)))))
;; (t/testing "authenticated with token & profile" (t/testing "authenticated with token & profile"
;; (let [data {::sq/type :viewer-bundle (let [data {::th/type :view-only-bundle
;; :profile-id (:id prof2) :profile-id (:id prof2)
;; :token @token :share-id @share-id
;; :file-id (:id file) :file-id (:id file)
;; :page-id (get-in file [:data :pages 0])} :page-id (get-in file [:data :pages 0])}
;; out (th/try-on! (sq/handle data))] out (th/query! data)]
;; ;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out)))
;; (let [result (:result out)] (let [result (:result out)]
;; (t/is (contains? result :page)) (t/is (contains? result :share))
;; (t/is (contains? result :file)) (t/is (contains? result :file))
;; (t/is (contains? result :project))))) (t/is (contains? result :project)))))
;; (t/testing "authenticated with token" (t/testing "authenticated with token"
;; (let [data {::sq/type :viewer-bundle (let [data {::th/type :view-only-bundle
;; :token @token :share-id @share-id
;; :file-id (:id file) :file-id (:id file)
;; :page-id (get-in file [:data :pages 0])} :page-id (get-in file [:data :pages 0])}
;; out (th/try-on! (sq/handle data))] out (th/query! data)]
;; ;; (th/print-result! out) ;; (th/print-result! out)
(let [result (:result out)]
(t/is (contains? result :file))
(t/is (contains? result :share))
(t/is (contains? result :project)))))
;; (let [result (:result out)]
;; (t/is (contains? result :page))
;; (t/is (contains? result :file))
;; (t/is (contains? result :project)))))
)) ))

View file

@ -228,9 +228,12 @@
([params] (update-file* *pool* params)) ([params] (update-file* *pool* params))
([conn {:keys [file-id changes session-id profile-id revn] ([conn {:keys [file-id changes session-id profile-id revn]
:or {session-id (uuid/next) revn 0}}] :or {session-id (uuid/next) revn 0}}]
(let [file (db/get-by-id conn :file file-id) (let [file (db/get-by-id conn :file file-id)
msgbus (:app.msgbus/msgbus *system*)] msgbus (:app.msgbus/msgbus *system*)
(#'files/update-file {:conn conn :msgbus msgbus} metrics (:app.metrics/metrics *system*)]
(#'files/update-file {:conn conn
:msgbus msgbus
:metrics metrics}
{:file file {:file file
:revn revn :revn revn
:changes changes :changes changes

View file

@ -0,0 +1,32 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.common.flags
"Flags parsing algorithm."
(:require
[cuerdas.core :as str]))
(defn parse
[default flags]
(loop [flags (seq flags)
result default]
(let [item (first flags)]
(if (nil? item)
result
(let [sname (name item)]
(cond
(str/starts-with? sname "enable-")
(recur (rest flags)
(conj result (keyword (subs sname 7))))
(str/starts-with? sname "disable-")
(recur (rest flags)
(disj result (keyword (subs sname 8))))
:else
(recur (rest flags) result)))))))

View file

@ -487,6 +487,7 @@
(d/parse-double) (d/parse-double)
(* (get-in modifiers [:resize-vector :x] 1)) (* (get-in modifiers [:resize-vector :x] 1))
(* (get-in modifiers [:resize-vector-2 :x] 1)) (* (get-in modifiers [:resize-vector-2 :x] 1))
(mth/precision 2)
(str))] (str))]
(attrs/merge attrs {:font-size font-size})))] (attrs/merge attrs {:font-size font-size})))]
(update shape :content #(txt/transform-nodes (update shape :content #(txt/transform-nodes

View file

@ -37,6 +37,8 @@
:stroke-style :stroke-group :stroke-style :stroke-group
:stroke-width :stroke-group :stroke-width :stroke-group
:stroke-alignment :stroke-group :stroke-alignment :stroke-group
:stroke-cap-start :stroke-group
:stroke-cap-end :stroke-group
:rx :radius-group :rx :radius-group
:ry :radius-group :ry :radius-group
:r1 :radius-group :r1 :radius-group

View file

@ -15,7 +15,7 @@
(def empty-page-data (def empty-page-data
{:options {} {:options {}
:name "Page" :name "Page-1"
:objects :objects
{root {root
{:id root {:id root
@ -38,7 +38,7 @@
(def ^:private minimal-shapes (def ^:private minimal-shapes
[{:type :rect [{:type :rect
:name "Rect" :name "Rect-1"
:fill-color default-color :fill-color default-color
:fill-opacity 1 :fill-opacity 1
:stroke-style :none :stroke-style :none
@ -52,7 +52,7 @@
{:type :image} {:type :image}
{:type :circle {:type :circle
:name "Circle" :name "Circle-1"
:fill-color default-color :fill-color default-color
:fill-opacity 1 :fill-opacity 1
:stroke-style :none :stroke-style :none
@ -62,7 +62,7 @@
:stroke-opacity 0} :stroke-opacity 0}
{:type :path {:type :path
:name "Path" :name "Path-1"
:stroke-style :solid :stroke-style :solid
:stroke-alignment :center :stroke-alignment :center
:stroke-width 2 :stroke-width 2
@ -70,7 +70,7 @@
:stroke-opacity 1} :stroke-opacity 1}
{:type :frame {:type :frame
:name "Artboard" :name "Artboard-1"
:fill-color "#ffffff" :fill-color "#ffffff"
:fill-opacity 1 :fill-opacity 1
:stroke-style :none :stroke-style :none
@ -80,7 +80,7 @@
:stroke-opacity 0} :stroke-opacity 0}
{:type :text {:type :text
:name "Text" :name "Text-1"
:content nil} :content nil}
{:type :svg-raw}]) {:type :svg-raw}])

View file

@ -10,6 +10,7 @@
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[clojure.set :as set]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
;; --- Specs ;; --- Specs
@ -254,6 +255,17 @@
(s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?)) (s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?))
(s/def :internal.shape/stroke-opacity ::safe-number) (s/def :internal.shape/stroke-opacity ::safe-number)
(s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none :svg}) (s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none :svg})
(def stroke-caps-line #{:round :square})
(def stroke-caps-marker #{:line-arrow :triangle-arrow :square-marker :circle-marker :diamond-marker})
(def stroke-caps (set/union stroke-caps-line stroke-caps-marker))
(s/def :internal.shape/stroke-cap-start stroke-caps)
(s/def :internal.shape/stroke-cap-end stroke-caps)
(defn has-caps?
[shape]
(= (:type shape) :path))
(s/def :internal.shape/stroke-width ::safe-number) (s/def :internal.shape/stroke-width ::safe-number)
(s/def :internal.shape/stroke-alignment #{:center :inner :outer}) (s/def :internal.shape/stroke-alignment #{:center :inner :outer})
(s/def :internal.shape/text-align #{"left" "right" "center" "justify"}) (s/def :internal.shape/text-align #{"left" "right" "center" "justify"})
@ -342,6 +354,8 @@
:internal.shape/stroke-style :internal.shape/stroke-style
:internal.shape/stroke-width :internal.shape/stroke-width
:internal.shape/stroke-alignment :internal.shape/stroke-alignment
:internal.shape/stroke-cap-start
:internal.shape/stroke-cap-end
:internal.shape/text-align :internal.shape/text-align
:internal.shape/transform :internal.shape/transform
:internal.shape/transform-inverse :internal.shape/transform-inverse

View file

@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION=v14.17.3 \ ENV NODE_VERSION=v14.17.5 \
CLOJURE_VERSION=1.10.3.929 \ CLOJURE_VERSION=1.10.3.933 \
CLJKONDO_VERSION=2021.06.18 \ CLJKONDO_VERSION=2021.07.28 \
BABASHKA_VERSION=0.5.0 \ BABASHKA_VERSION=0.5.1 \
LANG=en_US.UTF-8 \ LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 LC_ALL=en_US.UTF-8
@ -44,6 +44,7 @@ RUN set -ex; \
python \ python \
build-essential \ build-essential \
imagemagick \ imagemagick \
ghostscript \
netpbm \ netpbm \
potrace \ potrace \
webp \ webp \
@ -97,6 +98,15 @@ RUN set -ex; \
; \ ; \
rm -rf /var/lib/apt/lists/*; rm -rf /var/lib/apt/lists/*;
RUN set -x; \
apt-get -qq update; \
curl -LfsSo /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb; \
dpkg -i /tmp/chrome.deb; \
apt-get -fy install; \
rm -rf /var/lib/apt/lists/*; \
rm -rf /tmp/chrome.deb;
RUN set -ex; \ RUN set -ex; \
curl -LfsSo /tmp/openjdk.tar.gz https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz; \ curl -LfsSo /tmp/openjdk.tar.gz https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz; \
mkdir -p /usr/lib/jvm/openjdk16; \ mkdir -p /usr/lib/jvm/openjdk16; \

View file

@ -9,6 +9,7 @@ FROM gitpod/workspace-postgres
RUN set -ex; \ RUN set -ex; \
brew install redis; \ brew install redis; \
brew install imagemagick; \ brew install imagemagick; \
brew install ghostscript; \
brew install mailhog; \ brew install mailhog; \
brew install openldap; \ brew install openldap; \
sudo mkdir -p /var/log/nginx; \ sudo mkdir -p /var/log/nginx; \

View file

@ -1,11 +1,11 @@
FROM ubuntu:20.04 FROM debian:bullseye
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>" LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV LANG=en_US.UTF-8 \ ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \
NODE_VERSION=v14.16.0 NODE_VERSION=v14.17.5
RUN set -ex; \ RUN set -ex; \
mkdir -p /etc/resolvconf/resolv.conf.d; \ mkdir -p /etc/resolvconf/resolv.conf.d; \
@ -20,6 +20,7 @@ RUN set -ex; \
apt-get -qq update; \ apt-get -qq update; \
apt-get -qqy install \ apt-get -qqy install \
imagemagick \ imagemagick \
ghostscript \
netpbm \ netpbm \
potrace \ potrace \
gconf-service \ gconf-service \
@ -55,9 +56,9 @@ RUN set -ex; \
libxss1 \ libxss1 \
libxtst6 \ libxtst6 \
fonts-liberation \ fonts-liberation \
libappindicator1 \
libnss3 \ libnss3 \
libgbm1 \ libgbm1 \
chromium \
; \ ; \
rm -rf /var/lib/apt/lists/*; rm -rf /var/lib/apt/lists/*;

View file

@ -4,7 +4,7 @@
binaryage/devtools {:mvn/version "RELEASE"} binaryage/devtools {:mvn/version "RELEASE"}
metosin/reitit-core {:mvn/version "0.5.13"} metosin/reitit-core {:mvn/version "0.5.13"}
lambdaisland/glogi {:mvn/version "1.0.106"} lambdaisland/glogi {:mvn/version "1.0.106"}
funcool/beicon {:mvn/version "2021.04.29-0"} funcool/beicon {:mvn/version "2021.07.05-1"}
} }
:aliases :aliases
{:outdated {:outdated
@ -14,7 +14,7 @@
:dev :dev
{:extra-deps {:extra-deps
{thheller/shadow-cljs {:mvn/version "2.14.1"}}} {thheller/shadow-cljs {:mvn/version "2.15.2"}}}
:shadow-cljs :shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]} {:main-opts ["-m" "shadow.cljs.devtools.cli"]}

View file

@ -9,18 +9,18 @@
"author": "UXBOX LABS SL", "author": "UXBOX LABS SL",
"license": "SEE LICENSE IN <LICENSE>", "license": "SEE LICENSE IN <LICENSE>",
"dependencies": { "dependencies": {
"generic-pool": "^3.8.2",
"inflation": "^2.0.0", "inflation": "^2.0.0",
"jszip": "^3.6.0", "jszip": "^3.7.0",
"koa": "^2.13.0", "koa": "^2.13.0",
"luxon": "^1.27.0", "luxon": "^2.0.1",
"puppeteer": "^10.0.0", "puppeteer-core": "^10.1.0",
"puppeteer-cluster": "^0.22.0",
"raw-body": "^2.4.1", "raw-body": "^2.4.1",
"xml-js": "^1.6.11", "xml-js": "^1.6.11",
"xregexp": "^5.0.2" "xregexp": "^5.0.2"
}, },
"devDependencies": { "devDependencies": {
"shadow-cljs": "^2.14.2", "shadow-cljs": "^2.15.2",
"source-map-support": "^0.5.19" "source-map-support": "^0.5.19"
} }
} }

View file

@ -6,8 +6,10 @@
(ns app.browser (ns app.browser
(:require (:require
["puppeteer-cluster" :as ppc] ["puppeteer-core" :as pp]
["generic-pool" :as gp]
[app.common.data :as d] [app.common.data :as d]
[app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[lambdaisland.glogi :as log] [lambdaisland.glogi :as log]
[promesa.core :as p])) [promesa.core :as p]))
@ -20,12 +22,6 @@
(str "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " (str "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36")) "(KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"))
(defn exec!
[browser f]
(.execute ^js browser (fn [props]
(let [page (unchecked-get props "page")]
(f page)))))
(defn set-cookie! (defn set-cookie!
[page {:keys [key value domain]}] [page {:keys [key value domain]}]
(.setCookie ^js page #js {:name key (.setCookie ^js page #js {:name key
@ -73,12 +69,14 @@
(defn pdf (defn pdf
([page] (pdf page nil)) ([page] (pdf page nil))
([page {:keys [viewport omit-background? prefer-css-page-size?] ([page {:keys [viewport omit-background? prefer-css-page-size? save-path]
:or {viewport {} :or {viewport {}
omit-background? true omit-background? true
prefer-css-page-size? true}}] prefer-css-page-size? true
save-path nil}}]
(let [viewport (d/merge default-viewport viewport)] (let [viewport (d/merge default-viewport viewport)]
(.pdf ^js page #js {:width (:width viewport) (.pdf ^js page #js {:path save-path
:width (:width viewport)
:height (:height viewport) :height (:height viewport)
:scale (:scale viewport) :scale (:scale viewport)
:omitBackground omit-background? :omitBackground omit-background?
@ -100,36 +98,76 @@
;; --- BROWSER STATE ;; --- BROWSER STATE
(def instance (atom nil)) (defonce pool (atom nil))
(defonce pool-browser-id (atom 1))
(defn- create-browser (def browser-pool-factory
[concurrency strategy] (letfn [(create []
(let [strategy (case strategy (let [path (cf/get :browser-executable-path "/usr/bin/google-chrome")]
:browser (.-CONCURRENCY_BROWSER ^js ppc/Cluster) (-> (pp/launch #js {:executablePath path :args #js ["--no-sandbox"]})
:incognito (.-CONCURRENCY_CONTEXT ^js ppc/Cluster) (p/then (fn [browser]
:page (.-CONCURRENCY_PAGE ^js ppc/Cluster)) (let [id (deref pool-browser-id)]
opts #js {:concurrency strategy (log/info :origin "factory" :action "create" :browser-id id)
:maxConcurrency concurrency (unchecked-set browser "__num_use" 0)
:puppeteerOptions #js {:args #js ["--no-sandbox"]}}] (unchecked-set browser "__id" id)
(.launch ^js ppc/Cluster opts))) (swap! pool-browser-id inc)
browser))))))
(destroy [obj]
(let [id (unchecked-get obj "__id")]
(log/info :origin "factory" :action "destroy" :browser-id id)
(.close ^js obj)))
(validate [obj]
(let [max-use (cf/get :browser-max-usage 10)
num-use (unchecked-get obj "__num_use")
id (unchecked-get obj "__id")]
(log/info :origin "factory" :action "validate" :browser-id id :max-use max-use :num-use num-use :obj obj)
(if (> num-use max-use)
(p/resolved false)
(do
(unchecked-set obj "__num_use" (inc num-use))
(p/resolved (.isConnected ^js obj))))))]
#js {:create create
:destroy destroy
:validate validate}))
(defn init (defn init
[] []
(let [concurrency (cf/get :browser-concurrency) (log/info :msg "initializing browser pool")
strategy (cf/get :browser-strategy)] (let [opts #js {:max (cf/get :browser-pool-max 3)
(-> (create-browser concurrency strategy) :min (cf/get :browser-pool-min 0)
(p/then #(reset! instance %)) :testOnBorrow true
(p/catch (fn [error] :evictionRunIntervalMillis 30000
(log/error :msg "failed to initialize browser") :numTestsPerEvictionRun 5
(js/console.error error)))))) :acquireTimeoutMillis 120000 ; 2min
:idleTimeoutMillis 30000}]
(reset! pool (gp/createPool browser-pool-factory opts))
(p/resolved nil)))
(defn stop (defn stop
[] []
(if-let [instance @instance] (when-let [pool (deref pool)]
(p/do! (log/info :msg "finalizing browser pool")
(.idle ^js instance) (-> (.drain ^js pool)
(.close ^js instance) (p/then (fn [] (.clear ^js pool))))))
(log/info :msg "shutdown headless browser"))
(p/resolved nil))) (defn exec!
[f]
(letfn [(on-acquire [pool browser]
(p/let [ctx (.createIncognitoBrowserContext ^js browser)
page (.newPage ^js ctx)]
(-> (p/do! (f page))
(p/handle
(fn [result error]
(-> (p/do! (.close ^js ctx)
(.release ^js pool browser))
(p/handle (fn [_ _]
(if result
(p/resolved result)
(p/rejected error))))))))))]
(when-let [pool (deref pool)]
(-> (.acquire ^js pool)
(p/then (partial on-acquire pool))))))

View file

@ -8,13 +8,15 @@
(:require (:require
[app.config :as cf] [app.config :as cf]
[app.http.export :refer [export-handler]] [app.http.export :refer [export-handler]]
[app.http.export-frames :refer [export-frames-handler]]
[app.http.impl :as impl] [app.http.impl :as impl]
[lambdaisland.glogi :as log] [lambdaisland.glogi :as log]
[promesa.core :as p] [promesa.core :as p]
[reitit.core :as r])) [reitit.core :as r]))
(def routes (def routes
[["/export" {:handler export-handler}]]) [["/export-frames" {:handler export-frames-handler}]
["/export" {:handler export-handler}]])
(def instance (atom nil)) (def instance (atom nil))

View file

@ -0,0 +1,69 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.http.export-frames
(:require
["path" :as path]
[app.common.exceptions :as exc :include-macros true]
[app.common.spec :as us]
[app.renderer.pdf :as rp]
[app.util.shell :as sh]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[promesa.core :as p]))
(s/def ::name ::us/string)
(s/def ::file-id ::us/uuid)
(s/def ::page-id ::us/uuid)
(s/def ::frame-id ::us/uuid)
(s/def ::frame-ids (s/coll-of ::frame-id :kind vector?))
(s/def ::handler-params
(s/keys :req-un [::file-id ::page-id ::frame-ids]))
(defn- export-frame
[tdpath file-id page-id token frame-id spaths]
(p/let [spath (path/join tdpath (str frame-id ".pdf"))
result (rp/render {:name (str frame-id)
:suffix ""
:token token
:file-id file-id
:page-id page-id
:object-id frame-id
:scale 1
:save-path spath})]
(cons spath spaths)))
(defn- join-files
[tdpath file-id paths]
(let [output-path (path/join tdpath (str file-id ".pdf"))
paths-str (str/join " " paths)]
(-> (sh/run-cmd! (str "gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile='" output-path "' " paths-str))
(p/then (constantly output-path)))))
(defn- clean-tmp-data
[tdpath data]
(p/do!
(sh/rmdir! tdpath)
data))
(defn export-frames-handler
[{:keys [params cookies] :as request}]
(let [{:keys [name file-id page-id frame-ids]} (us/conform ::handler-params params)
token (.get ^js cookies "auth-token")]
(p/let [tdpath (sh/create-tmpdir! "pdfexport-")
data (-> (reduce (fn [promis frame-id]
(p/then promis (partial export-frame tdpath file-id page-id token frame-id)))
(p/future [])
frame-ids)
(p/then (partial join-files tdpath file-id))
(p/then sh/read-file)
(p/then (partial clean-tmp-data tdpath)))]
{:status 200
:body data
:headers {"content-type" "application/pdf"
"content-length" (.-length data)}})))

View file

@ -29,7 +29,7 @@
:value token})) :value token}))
(defn screenshot-object (defn screenshot-object
[browser {:keys [file-id page-id object-id token scale type]}] [{:keys [file-id page-id object-id token scale type]}]
(letfn [(handle [page] (letfn [(handle [page]
(let [path (str "/render-object/" file-id "/" page-id "/" object-id) (let [path (str "/render-object/" file-id "/" page-id "/" object-id)
uri (-> (u/uri (cf/get :public-uri)) uri (-> (u/uri (cf/get :public-uri))
@ -55,7 +55,7 @@
:png (bw/screenshot dom {:omit-background? true :type type}) :png (bw/screenshot dom {:omit-background? true :type type})
:jpeg (bw/screenshot dom {:omit-background? false :type type}))))))] :jpeg (bw/screenshot dom {:omit-background? false :type type}))))))]
(bw/exec! browser handle))) (bw/exec! handle)))
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::suffix ::us/string) (s/def ::suffix ::us/string)
@ -74,22 +74,16 @@
(defn render (defn render
[params] [params]
(us/assert ::render-params params) (us/assert ::render-params params)
(let [browser @bw/instance] (p/let [content (screenshot-object params)]
(when-not browser {:content content
(ex/raise :type :internal :filename (or (:filename params)
:code :browser-not-ready (str (:name params)
:hint "browser cluster is not initialized yet")) (:suffix params "")
(case (:type params)
(p/let [content (screenshot-object browser params)] :png ".png"
{:content content :jpeg ".jpg")))
:filename (or (:filename params) :length (alength content)
(str (:name params) :mime-type (case (:type params)
(:suffix params "") :png "image/png"
(case (:type params) :jpeg "image/jpeg")}))
:png ".png"
:jpeg ".jpg")))
:length (alength content)
:mime-type (case (:type params)
:png "image/png"
:jpeg "image/jpeg")})))

View file

@ -26,7 +26,7 @@
:value token})) :value token}))
(defn pdf-from-object (defn pdf-from-object
[browser {:keys [file-id page-id object-id token scale type]}] [{:keys [file-id page-id object-id token scale type save-path]}]
(letfn [(handle [page] (letfn [(handle [page]
(let [path (str "/render-object/" file-id "/" page-id "/" object-id) (let [path (str "/render-object/" file-id "/" page-id "/" object-id)
uri (-> (u/uri (cf/get :public-uri)) uri (-> (u/uri (cf/get :public-uri))
@ -39,12 +39,14 @@
(log/info :uri uri) (log/info :uri uri)
(let [options {:cookie cookie}] (let [options {:cookie cookie}]
(p/do! (p/do!
(bw/configure-page! page options) (bw/configure-page! page options)
(bw/navigate! page uri) (bw/navigate! page uri)
(bw/wait-for page "#screenshot") (bw/wait-for page "#screenshot")
(bw/pdf page))))] (if save-path
(bw/pdf page {:save-path save-path})
(bw/pdf page)))))]
(bw/exec! browser handle))) (bw/exec! handle)))
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::suffix ::us/string) (s/def ::suffix ::us/string)
@ -54,26 +56,21 @@
(s/def ::scale ::us/number) (s/def ::scale ::us/number)
(s/def ::token ::us/string) (s/def ::token ::us/string)
(s/def ::filename ::us/string) (s/def ::filename ::us/string)
(s/def ::save-path ::us/string)
(s/def ::render-params (s/def ::render-params
(s/keys :req-un [::name ::suffix ::object-id ::page-id ::scale ::token ::file-id] (s/keys :req-un [::name ::suffix ::object-id ::page-id ::scale ::token ::file-id]
:opt-un [::filename])) :opt-un [::filename ::save-path]))
(defn render (defn render
[params] [params]
(us/assert ::render-params params) (us/assert ::render-params params)
(let [browser @bw/instance] (p/let [content (pdf-from-object params)]
(when-not browser {:content content
(ex/raise :type :internal :filename (or (:filename params)
:code :browser-not-ready (str (:name params)
:hint "browser cluster is not initialized yet")) (:suffix params "")
".pdf"))
(p/let [content (pdf-from-object browser params)] :length (alength content)
{:content content :mime-type "application/pdf"}))
:filename (or (:filename params)
(str (:name params)
(:suffix params "")
".pdf"))
:length (alength content)
:mime-type "application/pdf"})))

View file

@ -114,7 +114,7 @@
(defn- render-object (defn- render-object
[browser {:keys [page-id file-id object-id token scale suffix type]}] [{:keys [page-id file-id object-id token scale suffix type]}]
(letfn [(convert-to-ppm [pngpath] (letfn [(convert-to-ppm [pngpath]
(log/trace :fn :convert-to-ppm) (log/trace :fn :convert-to-ppm)
(let [basepath (path/dirname pngpath) (let [basepath (path/dirname pngpath)
@ -279,7 +279,7 @@
rctx {:cookie cookie rctx {:cookie cookie
:uri (str uri)}] :uri (str uri)}]
(log/info :uri (:uri rctx)) (log/info :uri (:uri rctx))
(bw/exec! browser (partial handle rctx))))) (bw/exec! (partial handle rctx)))))
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::suffix ::us/string) (s/def ::suffix ::us/string)
@ -298,18 +298,11 @@
(defn render (defn render
[params] [params]
(us/assert ::render-params params) (us/assert ::render-params params)
(let [browser @bw/instance] (p/let [content (render-object params)]
(when-not browser {:content content
(ex/raise :type :internal :filename (or (:filename params)
:code :browser-not-ready (str (:name params)
:hint "browser cluster is not initialized yet")) (:suffix params "")
".svg"))
:length (alength content)
(p/let [content (render-object browser params)] :mime-type "image/svg+xml"}))
{:content content
:filename (or (:filename params)
(str (:name params)
(:suffix params "")
".svg"))
:length (alength content)
:mime-type "image/svg+xml"})))

View file

@ -2,23 +2,23 @@
# yarn lockfile v1 # yarn lockfile v1
"@babel/runtime-corejs3@^7.12.1": "@babel/runtime-corejs3@^7.14.9":
version "7.14.0" version "7.15.3"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.0.tgz#6bf5fbc0b961f8e3202888cb2cd0fb7a0a9a3f66" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.15.3.tgz#28754263988198f2a928c09733ade2fb4d28089d"
integrity sha512-0R0HTZWHLk6G8jIk0FtoX+AatCtKnswS98VhXwGImFc759PJRp4Tru0PQYZofyijTFUr+gT8Mu7sgXVJLQ0ceg== integrity sha512-30A3lP+sRL6ml8uhoJSs+8jwpKzbw8CqBvDc1laeptxPm5FahumJxirigcbD2qTs71Sonvj1cyZB0OKGAmxQ+A==
dependencies: dependencies:
core-js-pure "^3.0.0" core-js-pure "^3.16.0"
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@types/node@*": "@types/node@*":
version "15.6.2" version "16.6.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.2.tgz#c61d49f38af70da32424b5322eee21f97e627175" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50"
integrity sha512-dxcOx8801kMo3KlU+C+/ctWrzREAH7YvoF3aoVpRdqgs+Kf7flp+PJDN/EX5bME3suDUZHsxes9hpvBmzYlWbA== integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA==
"@types/yauzl@^2.9.1": "@types/yauzl@^2.9.1":
version "2.9.1" version "2.9.2"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a"
integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA== integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
@ -60,11 +60,6 @@ assert@^1.1.1:
object-assign "^4.1.1" object-assign "^4.1.1"
util "0.10.3" util "0.10.3"
async-limiter@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@ -174,9 +169,9 @@ buffer-crc32@~0.2.3:
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-from@^1.0.0: buffer-from@^1.0.0:
version "1.1.1" version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buffer-xor@^1.0.3: buffer-xor@^1.0.3:
version "1.0.3" version "1.0.3"
@ -271,10 +266,10 @@ cookies@~0.8.0:
depd "~2.0.0" depd "~2.0.0"
keygrip "~1.1.0" keygrip "~1.1.0"
core-js-pure@^3.0.0: core-js-pure@^3.16.0:
version "3.13.1" version "3.16.2"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.13.1.tgz#5d139d346780f015f67225f45ee2362a6bed6ba1" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.16.2.tgz#0ef4b79cabafb251ea86eb7d139b42bd98c533e8"
integrity sha512-wVlh0IAi2t1iOEh16y4u1TRk6ubd4KvLE8dlMi+3QUI6SfKphQUh7tAwihGGSQ8affxEXpVIPpOdf9kjR4v4Pw== integrity sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw==
core-util-is@~1.0.0: core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
@ -329,7 +324,14 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0" randombytes "^2.0.0"
randomfill "^1.0.3" randomfill "^1.0.3"
debug@4, debug@4.3.1, debug@^4.1.1: debug@4, debug@^4.1.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
dependencies:
ms "2.1.2"
debug@4.3.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
@ -376,10 +378,10 @@ destroy@^1.0.4:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
devtools-protocol@0.0.883894: devtools-protocol@0.0.901419:
version "0.0.883894" version "0.0.901419"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.883894.tgz#d403f2c75cd6d71c916aee8dde9258da988a4da9" resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd"
integrity sha512-33idhm54QJzf3Q7QofMgCvIVSd2o9H3kQPWaKT/fhoZh+digc+WSiMhbkeG3iN79WY4Hwr9G05NpbhEVrsOYAg== integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==
diffie-hellman@^5.0.0: diffie-hellman@^5.0.0:
version "5.0.3" version "5.0.3"
@ -484,6 +486,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
generic-pool@^3.8.2:
version "3.8.2"
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9"
integrity sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==
get-stream@^5.1.0: get-stream@^5.1.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
@ -503,6 +510,18 @@ glob@^7.1.3:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
has-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
has-tostringtag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
dependencies:
has-symbols "^1.0.2"
hash-base@^3.0.0: hash-base@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
@ -618,9 +637,11 @@ inherits@2.0.3:
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
is-generator-function@^1.0.7: is-generator-function@^1.0.7:
version "1.0.9" version "1.0.10"
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.9.tgz#e5f82c2323673e7fcad3d12858c83c4039f6399c" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
integrity sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A== integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
dependencies:
has-tostringtag "^1.0.0"
isarray@^1.0.0, isarray@~1.0.0: isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0" version "1.0.0"
@ -632,10 +653,10 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
jszip@^3.6.0: jszip@^3.7.0:
version "3.6.0" version "3.7.1"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.6.0.tgz#839b72812e3f97819cc13ac4134ffced95dd6af9" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9"
integrity sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ== integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
dependencies: dependencies:
lie "~3.3.0" lie "~3.3.0"
pako "~1.0.2" pako "~1.0.2"
@ -712,10 +733,10 @@ locate-path@^5.0.0:
dependencies: dependencies:
p-locate "^4.1.0" p-locate "^4.1.0"
luxon@^1.27.0: luxon@^2.0.1:
version "1.27.0" version "2.0.2"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.27.0.tgz#ae10c69113d85dab8f15f5e8390d0cbeddf4f00f" resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133"
integrity sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA== integrity sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg==
md5.js@^1.3.4: md5.js@^1.3.4:
version "1.3.5" version "1.3.5"
@ -739,17 +760,17 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0" bn.js "^4.0.0"
brorand "^1.0.1" brorand "^1.0.1"
mime-db@1.48.0: mime-db@1.49.0:
version "1.48.0" version "1.49.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==
mime-types@^2.1.18, mime-types@~2.1.24: mime-types@^2.1.18, mime-types@~2.1.24:
version "2.1.31" version "2.1.32"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5"
integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==
dependencies: dependencies:
mime-db "1.48.0" mime-db "1.49.0"
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1" version "1.0.1"
@ -986,20 +1007,13 @@ punycode@^1.2.4:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
puppeteer-cluster@^0.22.0: puppeteer-core@^10.1.0:
version "0.22.0" version "10.2.0"
resolved "https://registry.yarnpkg.com/puppeteer-cluster/-/puppeteer-cluster-0.22.0.tgz#4ab214671f414f15ad6a94a4b61ed0b4172e86e6" resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-10.2.0.tgz#8d6606cf345fc0e421bc0612055579ea53234111"
integrity sha512-hmydtMwfVM+idFIDzS8OXetnujHGre7RY3BGL+3njy9+r8Dcu3VALkZHfuBEPf6byKssTCgzxU1BvLczifXd5w== integrity sha512-c1COxSnfynsE6Mtt+dW0t3TITjF9Ku4dnJbFMDDVhLQuMTYSpz4rkSP37qvzcSo3k02/Ac3GYWk0/ncp6DKZNA==
dependencies:
debug "^4.1.1"
puppeteer@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-10.0.0.tgz#1b597c956103e2d989ca17f41ba4693b20a3640c"
integrity sha512-AxHvCb9IWmmP3gMW+epxdj92Gglii+6Z4sb+W+zc2hTTu10HF0yg6hGXot5O74uYkVqG3lfDRLfnRpi6WOwi5A==
dependencies: dependencies:
debug "4.3.1" debug "4.3.1"
devtools-protocol "0.0.883894" devtools-protocol "0.0.901419"
extract-zip "2.0.1" extract-zip "2.0.1"
https-proxy-agent "5.0.0" https-proxy-agent "5.0.0"
node-fetch "2.6.1" node-fetch "2.6.1"
@ -1074,9 +1088,9 @@ readline-sync@^1.4.7:
integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw== integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==
regenerator-runtime@^0.13.4: regenerator-runtime@^0.13.4:
version "0.13.7" version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
rimraf@3.0.2: rimraf@3.0.2:
version "3.0.2" version "3.0.2"
@ -1146,17 +1160,17 @@ shadow-cljs-jar@1.3.2:
resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
shadow-cljs@^2.14.2: shadow-cljs@^2.15.2:
version "2.14.2" version "2.15.4"
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.14.2.tgz#dba651ea124028064aea6fa9a390f257cb6eede4" resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.4.tgz#0d657fc8ab9a02d8980db5c49cb1622e8fc6fa52"
integrity sha512-ficaYfBAATzJ6OGt/GbIl393+cqLchzNkdTrM2PY4ttbsAOyBfWd39t+PZcYpCqemXjkgfBdZt9DJda7WaHJGA== integrity sha512-xn8UsiVpOf2LTsQZLsCa910CcMCYdMRT6STAsgveOEIncC9cunGdqE7cTq69vTmIijVQmzf0A1nALidyzO3Hcw==
dependencies: dependencies:
node-libs-browser "^2.2.1" node-libs-browser "^2.2.1"
readline-sync "^1.4.7" readline-sync "^1.4.7"
shadow-cljs-jar "1.3.2" shadow-cljs-jar "1.3.2"
source-map-support "^0.4.15" source-map-support "^0.4.15"
which "^1.3.1" which "^1.3.1"
ws "^3.0.0" ws "^7.4.6"
source-map-support@^0.4.15: source-map-support@^0.4.15:
version "0.4.18" version "0.4.18"
@ -1282,11 +1296,6 @@ type-is@^1.6.16:
media-typer "0.3.0" media-typer "0.3.0"
mime-types "~2.1.24" mime-types "~2.1.24"
ultron@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
unbzip2-stream@1.3.3: unbzip2-stream@1.3.3:
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a"
@ -1354,14 +1363,10 @@ ws@7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
ws@^3.0.0: ws@^7.4.6:
version "3.3.3" version "7.5.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
dependencies:
async-limiter "~1.0.0"
safe-buffer "~5.1.0"
ultron "~1.1.0"
xml-js@^1.6.11: xml-js@^1.6.11:
version "1.6.11" version "1.6.11"
@ -1371,11 +1376,11 @@ xml-js@^1.6.11:
sax "^1.2.4" sax "^1.2.4"
xregexp@^5.0.2: xregexp@^5.0.2:
version "5.0.2" version "5.1.0"
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.0.2.tgz#798aa7757836f39cdbdeeba3daf94d75f7a9dcc1" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.1.0.tgz#c87e7ae5ffa5fdc520f898a467dcba02b0d391e9"
integrity sha512-JPNfN40YMNSDxZrahMrmtNH1QqPJp0/qNeEJM2nnOlhcBdfCCjekPYFV2OnwKxwvpEYglH1RBotbpRRaEuCG8Q== integrity sha512-PynwUWtXnSZr8tpQlDPMZfPTyv78EYuA4oI959ukxcQ0a9O/lvndLVKy5wpImzzA26eMxpZmnAXJYiQA13AtWA==
dependencies: dependencies:
"@babel/runtime-corejs3" "^7.12.1" "@babel/runtime-corejs3" "^7.14.9"
xtend@^4.0.0: xtend@^4.0.0:
version "4.0.2" version "4.0.2"

View file

@ -0,0 +1 @@
<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><rect rx="6" ry="6" x="10" width="6" height="6"/><path d="M0 3h14.5" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 165 B

View file

@ -0,0 +1 @@
<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><rect rx="0" ry="0" x="11" y="1" transform="rotate(45 13 3)" width="4" height="4"/><path d="M0 3h14.5" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 199 B

View file

@ -0,0 +1 @@
<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><path d="M0 3h14.5M11.7 0l1 1 1.6 2-2.6 3" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 139 B

View file

@ -0,0 +1 @@
<svg viewBox="1863 1374 16 8" width="16" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M1879 1374h-12s-4 0-4 4 4 4 4 4h12" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 166 B

View file

@ -0,0 +1 @@
<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><rect rx="0" ry="0" x="10" width="6" height="6" fill="#070707"/><path d="M0 3h14.5" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 180 B

View file

@ -0,0 +1 @@
<svg viewBox="1863 1407 16 8" width="16" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M1879 1407h-16v8h16" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View file

@ -0,0 +1 @@
<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><path d="M0 3h14.5" fill="none" stroke="#000"/><path d="M13 0l2.9 3L13 6V0z"/></svg>

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View file

@ -0,0 +1,5 @@
<svg viewBox="0 0 500 500" width="500" height="500" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M374.8 238.3l-19.6 18.5 94.8 97.3-437.2.3V383l437.2.3-94.8 97.3 18.8 19 126.4-130.8zM126 260.9l19.6-18.6L50.8 145H488v-28.8L50.8 116l94.8-97.2L126.8-.4.4 130.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View file

@ -131,6 +131,24 @@
} }
} }
.btn-text-dark {
@extend %btn;
background: $color-gray-60;
color: $color-gray-20;
svg {
fill: $color-gray-20;
}
&:hover {
background: $color-primary;
color: $color-gray-60;
svg {
fill: $color-gray-60;
}
}
}
.btn-gray { .btn-gray {
@extend %btn; @extend %btn;
background: $color-gray-30; background: $color-gray-30;
@ -588,7 +606,6 @@ input.element-name {
box-sizing: border-box; box-sizing: border-box;
flex-shrink: 0; flex-shrink: 0;
} }
} }
&.column { &.column {
@ -975,6 +992,14 @@ input[type=range]:focus::-ms-fill-upper {
} }
} }
&.tooltip-expand {
&:hover {
&::after {
min-width: 100%;
}
}
}
&.tooltip-bottom-left { &.tooltip-bottom-left {
&:hover { &:hover {
&::after { &::after {
@ -1130,7 +1155,7 @@ input[type=range]:focus::-ms-fill-upper {
padding-left: 16px; padding-left: 16px;
top: 16px; top: 16px;
right: 16px; right: 16px;
z-index: 13; z-index: 1005;
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -88,3 +88,4 @@
@import "main/partials/color-bullet"; @import "main/partials/color-bullet";
@import "main/partials/handoff"; @import "main/partials/handoff";
@import "main/partials/exception-page"; @import "main/partials/exception-page";
@import "main/partials/share-link";

View file

@ -53,8 +53,8 @@
.icon { .icon {
display: flex; display: flex;
align-items: center; align-items: center;
width: 25px; width: 20px;
height: 25px; height: 20px;
margin-right: 7px; margin-right: 7px;
} }
} }

View file

@ -154,6 +154,10 @@
.modal-footer .action-buttons { .modal-footer .action-buttons {
justify-content: space-around; justify-content: space-around;
} }
.fields-container {
margin-top: 1rem;
}
} }
.confirm-dialog { .confirm-dialog {
@ -807,7 +811,7 @@
border-top-left-radius: 5px; border-top-left-radius: 5px;
border-bottom-left-radius: 5px; border-bottom-left-radius: 5px;
height: 100%; height: 100%;
width: 106%; width: 115%;
} }
} }
} }

View file

@ -0,0 +1,141 @@
.share-link-dialog {
width: 475px;
background-color: $color-white;
.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
height: unset;
padding: 16px 26px;
.btn-primary,
.btn-secondary,
.btn-warning {
width: 126px;
margin-bottom: 0px;
&:not(:last-child) {
margin-right: 10px;
}
}
.confirm-dialog {
display: flex;
flex-direction: column;
background-color: unset;
.description {
font-size: $fs14;
margin-bottom: 16px;
}
.actions {
display: flex;
justify-content: flex-end;
}
}
}
.modal-content {
padding: 26px;
&:first-child {
border-top: 0px;
}
.title {
display: flex;
justify-content: space-between;
h2 {
font-size: $fs18;
color: $color-black;
}
.modal-close-button {
margin-right: 0px;
}
}
.share-link-section {
margin-top: 12px;
label {
font-size: $fs11;
color: $color-black;
}
.hint {
padding-top: 10px;
font-size: $fs14;
color: $color-gray-40;
}
.help-icon {
cursor: pointer;
}
}
.view-mode,
.access-mode {
display: flex;
flex-direction: column;
.title {
color: $color-black;
font-weight: 400;
}
.items {
padding-left: 20px;
display: flex;
> .input-checkbox, > .input-radio {
display: flex;
user-select: none;
/* input { */
/* appearance: checkbox; */
/* } */
label {
display: flex;
align-items: center;
color: $color-black;
.hint {
margin-left: 5px;
color: $color-gray-30;
}
}
&.disabled {
label {
color: $color-gray-30;
}
}
}
}
}
.pages-selection {
padding-left: 20px;
max-height: 200px;
overflow-y: scroll;
user-select: none;
label {
color: $color-black;
}
}
.custom-input {
input {
padding: 0 40px 0 15px;
}
}
}
}

View file

@ -1316,7 +1316,7 @@
&::after { &::after {
content: ' '; content: ' ';
background-color: $color-gray-20; background-color: $color-gray-30;
} }
&.active, &.active,
@ -1436,5 +1436,57 @@
} }
} }
} }
}
.cap-select {
background-color: transparent;
border: 1px solid transparent;
border-bottom-color: $color-gray-40;
color: $color-gray-10;
cursor: pointer;
font-size: $fs11;
margin: $x-small;
overflow: hidden;
padding: $x-small;
padding-right: 20px;
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
& .cap-select-button {
svg {
fill: $color-gray-10;
height: 11px;
position: absolute;
right: 5px;
top: 6px;
width: 11px;
}
}
&:hover {
border-color: $color-gray-40;
}
&:focus {
border-color: $color-primary;
}
}
.cap-select-dropdown {
right: 5px;
top: 30px;
z-index: 12;
min-width: 200px;
position: fixed;
& li.separator {
border-top: 1px solid $color-gray-10;
}
& li img {
width: 16px;
margin-right: $small;
}
} }

View file

@ -42,56 +42,64 @@
} }
} }
.view-options { .options-zone {
.icon { align-items: center;
align-items: center; display: flex;
cursor: pointer; // width: 384px;
display: flex; justify-content: flex-end;
justify-content: center; position: relative;
svg { > * {
fill: $color-gray-30; margin-left: $big;
height: 30px; }
width: 28px;
}
&:hover { .btn-primary {
> svg { flex-shrink: 0;
fill: $color-primary; }
}
.zoom-widget {
.dropdown {
top: 45px;
left: 25px;
} }
} }
.dropdown { .view-options {
min-width: 260px;
left: 0px;
top: 40px;
}
.view-options-dropdown {
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
width: 90px;
span { > span {
color: $color-gray-10; color: $color-gray-10;
font-size: $fs13; font-size: $fs13;
margin-right: $x-small; margin-right: $x-small;
} }
svg { > .icon {
fill: $color-gray-10; align-items: center;
height: 12px; cursor: pointer;
width: 12px; display: flex;
} justify-content: center;
}
}
.file-menu { svg {
.dropdown { fill: $color-gray-10;
min-width: 100px; height: 12px;
right: 0px; width: 12px;
top: 40px; }
&:hover {
> svg {
fill: $color-primary;
}
}
}
.dropdown {
min-width: 260px;
top: 45px;
left: -25px;
}
} }
} }
@ -100,39 +108,50 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
padding: $x-small; padding: $x-small;
position: relative;
svg { .icon {
fill: $color-gray-20; display: flex;
height: 20px; justify-content: center;
margin-right: $small; align-items: center;
width: 20px;
}
span { svg {
color: $color-gray-20; fill: $color-gray-20;
margin-right: $x-small; height: 12px;
font-size: $fs14; margin-right: $small;
overflow-x: hidden; width: 12px;
text-overflow: ellipsis;
white-space: nowrap;
&.frame-name {
color: $color-white;
} }
} }
.show-thumbnails-button svg { .breadcrumb, .current-frame {
fill: $color-white; display: flex;
height: 10px; position: relative;
width: 10px;
> span {
color: $color-gray-20;
margin-right: $x-small;
font-size: $fs14;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
> .dropdown {
top: 45px;
right: 10px;
}
} }
.page-name { .current-frame {
color: $color-white; display: flex;
} span {
color: $color-white;
margin-right: $x-small;
}
.counters { .counters {
margin-left: $size-3; color: $color-gray-20;
}
} }
} }
@ -166,133 +185,6 @@
} }
} }
.options-zone {
align-items: center;
display: flex;
width: 384px;
justify-content: flex-end;
position: relative;
> * {
margin-left: $big;
}
.btn-share {
display: flex;
align-items: center;
justify-content: center;
width: 25px;
height: 25px;
cursor: pointer;
svg {
fill: $color-gray-20;
width: 20px;
height: 20px;
}
}
.btn-primary {
flex-shrink: 0;
}
}
.share-link-dropdown {
background-color: $color-white;
border-radius: $br-small;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
display: flex;
flex-direction: column;
left: -135px;
position: absolute;
padding: 1rem;
top: 45px;
width: 400px;
.share-link-title {
color: $color-black;
font-size: $fs15;
padding-bottom: 1rem;
}
.share-link-subtitle {
color: $color-gray-40;
padding-bottom: 1rem;
}
.share-link-buttons {
display: flex;
justify-content: center;
align-items: center;
.btn-warning,
.btn-primary {
width: 50%;
}
}
.share-link-input {
border: 1px solid $color-gray-20;
border-radius: 3px;
display: flex;
height: 40px;
justify-content: space-between;
margin-bottom: 1rem;
padding: 9px $small;
overflow: hidden;
.link {
&:before {
content: '';
position: absolute;
width: 50%;
background: linear-gradient(45deg, transparent, #ffffff);
height: 100%;
top: 0;
left: 0;
pointer-events: none;
margin-left: 50%;
}
overflow: hidden;
white-space: nowrap;
position: relative;
color: $color-gray-50;
line-height: 1.5;
user-select: all;
overflow: hidden;
}
.link-button {
color: $color-primary-dark;
cursor: pointer;
flex-shrink: 0;
font-size: $fs15;
&:hover {
color: $color-black;
}
}
}
&:before {
background-color: $color-white;
content: "";
height: 16px;
left: 53%;
position: absolute;
transform: rotate(45deg);
top: -5px;
width: 16px;
}
}
.zoom-dropdown {
left: 180px;
top: 40px;
}
.users-zone { .users-zone {
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;

View file

@ -1,4 +1,3 @@
.viewer-thumbnails { .viewer-thumbnails {
grid-row: 1 / span 1; grid-row: 1 / span 1;
grid-column: 1 / span 1; grid-column: 1 / span 1;
@ -9,6 +8,11 @@
flex-direction: column; flex-direction: column;
z-index: 12; z-index: 12;
&.invisible {
visibility: hidden;
pointer-events: none;
}
&.expanded { &.expanded {
grid-row: 1 / span 2; grid-row: 1 / span 2;
@ -159,7 +163,7 @@
&:hover { &:hover {
border-color: $color-primary; border-color: $color-primary;
border-width: 2px; outline: 2px solid $color-primary;
} }
} }

View file

@ -8,13 +8,13 @@
margin-left: $x-small; margin-left: $x-small;
} }
.dropdown-button svg { .icon svg {
fill: $color-gray-10; fill: $color-gray-10;
height: 10px; height: 10px;
width: 10px; width: 10px;
} }
.zoom-dropdown { .dropdown {
position: absolute; position: absolute;
z-index: 12; z-index: 12;
width: 210px; width: 210px;

View file

@ -6,6 +6,7 @@
(ns app.config (ns app.config
(:require (:require
[app.common.flags :as flags]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uri :as u] [app.common.uri :as u]
[app.common.version :as v] [app.common.version :as v]
@ -53,10 +54,14 @@
:browser :browser
:webworker)) :webworker))
(def default-flags
#{:registration :demo-users})
(defn- parse-flags (defn- parse-flags
[global] [global]
(let [flags (obj/get global "penpotFlags" "")] (let [flags (obj/get global "penpotFlags" "")
(into #{} (map keyword) (str/words flags)))) flags (into #{} (map keyword) (str/words flags))]
(flags/parse default-flags flags)))
(defn- parse-version (defn- parse-version
[global] [global]
@ -68,26 +73,27 @@
(def default-theme "default") (def default-theme "default")
(def default-language "en") (def default-language "en")
(def demo-warning (obj/get global "penpotDemoWarning" false))
(def feedback-enabled (obj/get global "penpotFeedbackEnabled" false))
(def allow-demo-users (obj/get global "penpotAllowDemoUsers" true))
(def google-client-id (obj/get global "penpotGoogleClientID" nil)) (def google-client-id (obj/get global "penpotGoogleClientID" nil))
(def gitlab-client-id (obj/get global "penpotGitlabClientID" nil)) (def gitlab-client-id (obj/get global "penpotGitlabClientID" nil))
(def github-client-id (obj/get global "penpotGithubClientID" nil)) (def github-client-id (obj/get global "penpotGithubClientID" nil))
(def oidc-client-id (obj/get global "penpotOIDCClientID" nil)) (def oidc-client-id (obj/get global "penpotOIDCClientID" nil))
(def login-with-ldap (obj/get global "penpotLoginWithLDAP" false))
(def registration-enabled (obj/get global "penpotRegistrationEnabled" true))
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js")) (def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
(def translations (obj/get global "penpotTranslations")) (def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes")) (def themes (obj/get global "penpotThemes"))
(def analytics (obj/get global "penpotAnalyticsEnabled" false))
(def flags (delay (parse-flags global))) (def flags (atom (parse-flags global)))
(def version (atom (parse-version global)))
(def target (atom (parse-target global)))
(def browser (atom (parse-browser)))
(def platform (atom (parse-platform)))
(def version (delay (parse-version global))) ;; mantain for backward compatibility
(def target (delay (parse-target global))) (let [login-with-ldap (obj/get global "penpotLoginWithLDAP" false)
(def browser (delay (parse-browser))) registration (obj/get global "penpotRegistrationEnabled" true)]
(def platform (delay (parse-platform))) (when login-with-ldap
(swap! flags conj :login-with-ldap))
(when (false? registration)
(swap! flags disj :registration)))
(def public-uri (def public-uri
(let [uri (u/uri (or (obj/get global "penpotPublicURI") (let [uri (u/uri (or (obj/get global "penpotPublicURI")

View file

@ -42,10 +42,13 @@
(if-let [conform (get-in match [:data :conform])] (if-let [conform (get-in match [:data :conform])]
(let [spath (get conform :path-params ::any) (let [spath (get conform :path-params ::any)
squery (get conform :query-params ::any)] squery (get conform :query-params ::any)]
(-> (dissoc match :params) (try
(assoc :path-params (us/conform spath (get match :path-params)) (-> (dissoc match :params)
:query-params (us/conform squery (get match :query-params))))) (assoc :path-params (us/conform spath (get match :path-params))
match))) :query-params (us/conform squery (get match :query-params))))
(catch :default _
nil)))
match)))
(defn on-navigate (defn on-navigate
[router path] [router path]

View file

@ -72,7 +72,7 @@
(update :workspace-drawing dissoc :comment) (update :workspace-drawing dissoc :comment)
(update-in [:comments id] assoc (:id comment) comment)))] (update-in [:comments id] assoc (:id comment) comment)))]
(ptk/reify ::create-thread (ptk/reify ::create-comment-thread
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(->> (rp/mutation :create-comment-thread params) (->> (rp/mutation :create-comment-thread params)
@ -94,6 +94,8 @@
[{:keys [id is-resolved] :as thread}] [{:keys [id is-resolved] :as thread}]
(us/assert ::comment-thread thread) (us/assert ::comment-thread thread)
(ptk/reify ::update-comment-thread (ptk/reify ::update-comment-thread
IDeref
(-deref [_] {:is-resolved is-resolved})
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
@ -122,7 +124,7 @@
(defn update-comment (defn update-comment
[{:keys [id content thread-id] :as comment}] [{:keys [id content thread-id] :as comment}]
(us/assert ::comment comment) (us/assert ::comment comment)
(ptk/reify :update-comment (ptk/reify ::update-comment
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(d/update-in-when state [:comments thread-id id] assoc :content content)) (d/update-in-when state [:comments thread-id id] assoc :content content))
@ -135,7 +137,7 @@
(defn delete-comment-thread (defn delete-comment-thread
[{:keys [id] :as thread}] [{:keys [id] :as thread}]
(us/assert ::comment-thread thread) (us/assert ::comment-thread thread)
(ptk/reify :delete-comment-thread (ptk/reify ::delete-comment-thread
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
@ -150,7 +152,7 @@
(defn delete-comment (defn delete-comment
[{:keys [id thread-id] :as comment}] [{:keys [id thread-id] :as comment}]
(us/assert ::comment comment) (us/assert ::comment comment)
(ptk/reify :delete-comment (ptk/reify ::delete-comment
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(d/update-in-when state [:comments thread-id] dissoc id)) (d/update-in-when state [:comments thread-id] dissoc id))
@ -212,7 +214,7 @@
(defn open-thread (defn open-thread
[{:keys [id] :as thread}] [{:keys [id] :as thread}]
(us/assert ::comment-thread thread) (us/assert ::comment-thread thread)
(ptk/reify ::open-thread (ptk/reify ::open-comment-thread
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
@ -221,7 +223,7 @@
(defn close-thread (defn close-thread
[] []
(ptk/reify ::close-thread (ptk/reify ::close-comment-thread
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state

View file

@ -0,0 +1,46 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.data.common
"A general purpose events."
(:require
[app.main.repo :as rp]
[beicon.core :as rx]
[potok.core :as ptk]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHARE LINK
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn share-link-created
[link]
(ptk/reify ::share-link-created
ptk/UpdateEvent
(update [_ state]
(update state :share-links (fnil conj []) link))))
(defn create-share-link
[params]
(ptk/reify ::create-share-link
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/mutation! :create-share-link params)
(rx/map share-link-created)))))
(defn delete-share-link
[{:keys [id] :as link}]
(ptk/reify ::delete-share-link
ptk/UpdateEvent
(update [_ state]
(update state :share-links
(fn [links]
(filterv #(not= id (:id %)) links))))
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/mutation! :delete-share-link {:id id})
(rx/ignore)))))

View file

@ -9,6 +9,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.events :as ev]
[app.main.data.fonts :as df] [app.main.data.fonts :as df]
[app.main.data.media :as di] [app.main.data.media :as di]
[app.main.data.users :as du] [app.main.data.users :as du]
@ -386,6 +387,9 @@
(us/assert ::us/email email) (us/assert ::us/email email)
(us/assert ::us/keyword role) (us/assert ::us/keyword role)
(ptk/reify ::invite-team-member (ptk/reify ::invite-team-member
IDeref
(-deref [_] {:role role})
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [{:keys [on-success on-error] (let [{:keys [on-success on-error]
@ -475,6 +479,10 @@
(us/assert ::us/uuid id) (us/assert ::us/uuid id)
(us/assert ::us/uuid team-id) (us/assert ::us/uuid team-id)
(ptk/reify ::move-project (ptk/reify ::move-project
IDeref
(-deref [_]
{:id id :team-id team-id})
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(let [{:keys [on-success on-error] (let [{:keys [on-success on-error]
@ -566,6 +574,10 @@
[{:keys [id name] :as params}] [{:keys [id name] :as params}]
(us/assert ::file params) (us/assert ::file params)
(ptk/reify ::rename-file (ptk/reify ::rename-file
IDeref
(-deref [_]
{::ev/origin "dashboard" :id id :name name})
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
@ -585,6 +597,10 @@
[{:keys [id is-shared] :as params}] [{:keys [id is-shared] :as params}]
(us/assert ::file params) (us/assert ::file params)
(ptk/reify ::set-file-shared (ptk/reify ::set-file-shared
IDeref
(-deref [_]
{::ev/origin "dashboard" :id id :shared is-shared})
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
@ -663,12 +679,16 @@
(us/assert ::set-of-uuid ids) (us/assert ::set-of-uuid ids)
(us/assert ::us/uuid project-id) (us/assert ::us/uuid project-id)
(ptk/reify ::move-files (ptk/reify ::move-files
IDeref
(-deref [_]
{:num-files (count ids)
:project-id project-id})
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(let [{:keys [on-success on-error] (let [{:keys [on-success on-error]
:or {on-success identity :or {on-success identity
on-error rx/throw}} (meta params)] on-error rx/throw}} (meta params)]
(->> (rp/mutation! :move-files {:ids ids :project-id project-id}) (->> (rp/mutation! :move-files {:ids ids :project-id project-id})
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (rx/catch on-error))))))
@ -690,14 +710,14 @@
(defn go-to-files (defn go-to-files
([project-id] ([project-id]
(ptk/reify ::go-to-files (ptk/reify ::go-to-files-1
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [team-id (:current-team-id state)] (let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-files {:team-id team-id (rx/of (rt/nav :dashboard-files {:team-id team-id
:project-id project-id})))))) :project-id project-id}))))))
([team-id project-id] ([team-id project-id]
(ptk/reify ::go-to-files (ptk/reify ::go-to-files-2
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (rt/nav :dashboard-files {:team-id team-id (rx/of (rt/nav :dashboard-files {:team-id team-id
@ -719,13 +739,13 @@
(defn go-to-projects (defn go-to-projects
([] ([]
(ptk/reify ::go-to-projects (ptk/reify ::go-to-projects-0
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [team-id (:current-team-id state)] (let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-projects {:team-id team-id})))))) (rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
([team-id] ([team-id]
(ptk/reify ::go-to-projects (ptk/reify ::go-to-projects-1
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(du/set-current-team! team-id) (du/set-current-team! team-id)

View file

@ -71,18 +71,84 @@
;; --- EVENT TRANSLATION ;; --- EVENT TRANSLATION
(defmulti ^:private process-event ptk/type) (derive :app.main.data.comments/create-comment ::generic-action)
(derive :app.main.data.comments/create-comment-thread ::generic-action)
(derive :app.main.data.comments/delete-comment ::generic-action)
(derive :app.main.data.comments/delete-comment-thread ::generic-action)
(derive :app.main.data.comments/open-comment-thread ::generic-action)
(derive :app.main.data.comments/update-comment ::generic-action)
(derive :app.main.data.comments/update-comment-thread ::generic-action)
(derive :app.main.data.comments/update-comment-thread-status ::generic-action)
(derive :app.main.data.dashboard/delete-team-member ::generic-action)
(derive :app.main.data.dashboard/duplicate-project ::generic-action)
(derive :app.main.data.dashboard/file-created ::generic-action)
(derive :app.main.data.dashboard/invite-team-member ::generic-action)
(derive :app.main.data.dashboard/leave-team ::generic-action)
(derive :app.main.data.dashboard/move-files ::generic-action)
(derive :app.main.data.dashboard/move-project ::generic-action)
(derive :app.main.data.dashboard/project-created ::generic-action)
(derive :app.main.data.dashboard/rename-file ::generic-action)
(derive :app.main.data.dashboard/set-file-shared ::generic-action)
(derive :app.main.data.dashboard/update-team-member-role ::generic-action)
(derive :app.main.data.dashboard/update-team-photo ::generic-action)
(derive :app.main.data.fonts/add-font ::generic-action)
(derive :app.main.data.fonts/delete-font ::generic-action)
(derive :app.main.data.fonts/delete-font-variant ::generic-action)
(derive :app.main.data.users/logout ::generic-action)
(derive :app.main.data.users/request-email-change ::generic-action)
(derive :app.main.data.users/update-password ::generic-action)
(derive :app.main.data.users/update-photo ::generic-action)
(derive :app.main.data.workspace.comments/open-comment-thread ::generic-action)
(derive :app.main.data.workspace.libraries/add-color ::generic-action)
(derive :app.main.data.workspace.libraries/add-media ::generic-action)
(derive :app.main.data.workspace.libraries/add-typography ::generic-action)
(derive :app.main.data.workspace.libraries/delete-color ::generic-action)
(derive :app.main.data.workspace.libraries/delete-media ::generic-action)
(derive :app.main.data.workspace.libraries/delete-typography ::generic-action)
(derive :app.main.data.workspace.persistence/attach-library ::generic-action)
(derive :app.main.data.workspace.persistence/detach-library ::generic-action)
(derive :app.main.data.workspace.persistence/set-file-shard ::generic-action)
(derive :app.main.data.workspace/create-page ::generic-action)
(derive :app.main.data.workspace/set-workspace-layout ::generic-action)
(defmulti process-event ptk/type)
(defmethod process-event :default [_] nil) (defmethod process-event :default [_] nil)
(defmethod process-event ::event (defmethod process-event ::event
[event] [event]
(let [data (deref event)] (let [data (deref event)
origin (::origin data)]
(when (::name data) (when (::name data)
(d/without-nils (d/without-nils
{:type (::type data "action") {:type (::type data "action")
:name (::name data) :name (::name data)
:context (::context data) :context (::context data)
:props (dissoc data ::name ::type ::context)})))) :props (-> data
(dissoc ::name)
(dissoc ::type)
(dissoc ::origin)
(dissoc ::context)
(cond-> origin (assoc :origin origin)))}))))
(defmethod process-event ::generic-action
[event]
(let [type (ptk/type event)
mdata (meta event)
data (if (satisfies? IDeref event)
(deref event)
{})
name (or (::name mdata)
(name type))]
{:type "action"
:name (name type)
:props (merge data (d/without-nils (::props mdata)))
:context (d/without-nils
{:event-origin (::origin mdata)
:event-namespace (namespace type)
:event-symbol (name type)})}))
(defmethod process-event :app.util.router/navigated (defmethod process-event :app.util.router/navigated
[event] [event]
@ -113,42 +179,6 @@
:profile-id (:id data) :profile-id (:id data)
:props (d/without-nils props)})) :props (d/without-nils props)}))
(defmethod process-event :app.main.data.dashboard/project-created
[event]
(let [data (deref event)]
{:type "action"
:name "create-project"
:props {:id (:id data)
:team-id (:team-id data)}}))
(defmethod process-event :app.main.data.dashboard/file-created
[event]
(let [data (deref event)]
{:type "action"
:name "create-file"
:props {:id (:id data)
:project-id (:project-id data)}}))
(defmethod process-event :app.main.data.workspace/create-page
[event]
(let [data (deref event)]
{:type "action"
:name "create-page"
:props {:id (:id data)
:file-id (:file-id data)
:project-id (:project-id data)}}))
(defn- event->generic-action
[_ name]
{:type "action"
:name name
:props {}})
(defmethod process-event :app.main.data.users/logout
[event]
(event->generic-action event "signout"))
;; --- MAIN LOOP ;; --- MAIN LOOP
(defn- append-to-buffer (defn- append-to-buffer
@ -164,7 +194,7 @@
(defn- persist-events (defn- persist-events
[events] [events]
(if (seq events) (if (seq events)
(let [uri (u/join cf/public-uri "events") (let [uri (u/join cf/public-uri "api/audit/events")
params {:events events}] params {:events events}]
(->> (http/send! {:uri uri (->> (http/send! {:uri uri
:method :post :method :post
@ -203,8 +233,7 @@
ptk/EffectEvent ptk/EffectEvent
(effect [_ _ stream] (effect [_ _ stream]
(let [events (methods process-event) (let [session (atom nil)
session (atom nil)
profile (->> (rx/from-atom storage {:emit-current-value? true}) profile (->> (rx/from-atom storage {:emit-current-value? true})
(rx/map :profile) (rx/map :profile)
@ -215,12 +244,9 @@
(rx/with-latest-from profile) (rx/with-latest-from profile)
(rx/map (fn [result] (rx/map (fn [result]
(let [event (aget result 0) (let [event (aget result 0)
profile-id (aget result 1) profile-id (aget result 1)]
type (ptk/type event) (some-> (process-event event)
impl-fn (get events type)] (update :profile-id #(or % profile-id))))))
(when (fn? impl-fn)
(some-> (impl-fn event)
(update :profile-id #(or % profile-id)))))))
(rx/filter :profile-id) (rx/filter :profile-id)
(rx/map (fn [event] (rx/map (fn [event]
(let [session* (or @session (dt/now)) (let [session* (or @session (dt/now))
@ -242,6 +268,6 @@
(defmethod ptk/resolve ::initialize (defmethod ptk/resolve ::initialize
[_ params] [_ params]
(if cf/analytics (if (contains? @cf/flags :audit-log)
(initialize) (initialize)
(ptk/data-event ::initialize params))) (ptk/data-event ::initialize params)))

View file

@ -187,6 +187,9 @@
(defn add-font (defn add-font
[font] [font]
(ptk/reify ::add-font (ptk/reify ::add-font
IDeref
(-deref [_] (select-keys font [:font-family :font-style :font-weight]))
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :dashboard-fonts assoc (:id font) font)))) (update state :dashboard-fonts assoc (:id font) font))))

View file

@ -24,6 +24,10 @@
;; --- COMMON SPECS ;; --- COMMON SPECS
(defn is-authenticated?
[{:keys [id]}]
(and (uuid? id) (not= id uuid/zero)))
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::fullname ::us/string) (s/def ::fullname ::us/string)
(s/def ::email ::us/email) (s/def ::email ::us/email)

View file

@ -14,24 +14,12 @@
[app.main.data.comments :as dcm] [app.main.data.comments :as dcm]
[app.main.data.fonts :as df] [app.main.data.fonts :as df]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.util.globals :as ug]
[app.util.router :as rt] [app.util.router :as rt]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[potok.core :as ptk])) [potok.core :as ptk]))
;; --- General Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::project (s/keys :req-un [::id ::name]))
(s/def ::file (s/keys :req-un [::id ::name]))
(s/def ::page ::cp/page)
(s/def ::bundle
(s/keys :req-un [::project ::file ::page]))
;; --- Local State Initialization ;; --- Local State Initialization
(def ^:private (def ^:private
@ -49,25 +37,24 @@
(declare fetch-bundle) (declare fetch-bundle)
(declare bundle-fetched) (declare bundle-fetched)
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
(s/def ::index ::us/integer) (s/def ::index ::us/integer)
(s/def ::token (s/nilable ::us/string)) (s/def ::page-id (s/nilable ::us/uuid))
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::section ::us/string) (s/def ::section ::us/string)
(s/def ::initialize-params (s/def ::initialize-params
(s/keys :req-un [::page-id ::file-id] (s/keys :req-un [::file-id]
:opt-un [::token])) :opt-un [::share-id ::page-id]))
(defn initialize (defn initialize
[{:keys [page-id file-id] :as params}] [{:keys [file-id] :as params}]
(us/assert ::initialize-params params) (us/assert ::initialize-params params)
(ptk/reify ::initialize (ptk/reify ::initialize
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(assoc :current-file-id file-id) (assoc :current-file-id file-id)
(assoc :current-page-id page-id)
(update :viewer-local (update :viewer-local
(fn [lstate] (fn [lstate]
(if (nil? lstate) (if (nil? lstate)
@ -77,55 +64,72 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (fetch-bundle params) (rx/of (fetch-bundle params)
(fetch-comment-threads params))))) (fetch-comment-threads params)))
;; --- Data Fetching ptk/EffectEvent
(effect [_ _ _]
;; Set the window name, the window name is used on inter-tab
;; navigation; in other words: when a user opens a tab with a
;; name, if there are already opened tab with that name, the
;; browser just focus the opened tab instead of creating new
;; tab.
(let [name (str "viewer-" file-id)]
(unchecked-set ug/global "name" name)))))
(s/def ::fetch-bundle-params (defn finalize
(s/keys :req-un [::page-id ::file-id] [_]
:opt-un [::token])) (ptk/reify ::finalize
ptk/UpdateEvent
(update [_ state]
(dissoc state :viewer))))
(defn fetch-bundle (defn select-frames
[{:keys [page-id file-id token] :as params}] [{:keys [objects] :as page}]
(us/assert ::fetch-bundle-params params)
(ptk/reify ::fetch-file
ptk/WatchEvent
(watch [_ _ _]
(let [params (cond-> {:page-id page-id
:file-id file-id}
(string? token) (assoc :token token))]
(->> (rp/query :viewer-bundle params)
(rx/mapcat
(fn [{:keys [fonts] :as bundle}]
(rx/of (df/fonts-fetched fonts)
(bundle-fetched bundle)))))))))
(defn- extract-frames
[objects]
(let [root (get objects uuid/zero)] (let [root (get objects uuid/zero)]
(into [] (comp (map #(get objects %)) (into [] (comp (map #(get objects %))
(filter #(= :frame (:type %)))) (filter #(= :frame (:type %))))
(reverse (:shapes root))))) (reverse (:shapes root)))))
;; --- Data Fetching
(s/def ::fetch-bundle-params
(s/keys :req-un [::page-id ::file-id]
:opt-un [::share-id]))
(defn fetch-bundle
[{:keys [file-id share-id] :as params}]
(us/assert ::fetch-bundle-params params)
(ptk/reify ::fetch-file
ptk/WatchEvent
(watch [_ _ _]
(let [params' (cond-> {:file-id file-id}
(uuid? share-id) (assoc :share-id share-id))]
(->> (rp/query :view-only-bundle params')
(rx/mapcat
(fn [{:keys [fonts] :as bundle}]
(rx/of (df/fonts-fetched fonts)
(bundle-fetched (merge bundle params))))))))))
(defn bundle-fetched (defn bundle-fetched
[{:keys [project file page share-token token libraries users] :as bundle}] [{:keys [project file share-links libraries users permissions] :as bundle}]
(us/verify ::bundle bundle) (let [pages (->> (get-in file [:data :pages])
(ptk/reify ::bundle-fetched (map (fn [page-id]
ptk/UpdateEvent (let [data (get-in file [:data :pages-index page-id])]
(update [_ state] [page-id (assoc data :frames (select-frames data))])))
(let [objects (:objects page) (into {}))]
frames (extract-frames objects)]
(ptk/reify ::bundle-fetched
ptk/UpdateEvent
(update [_ state]
(-> state (-> state
(assoc :viewer-libraries (d/index-by :id libraries)) (assoc :share-links share-links)
(update :viewer-data assoc (assoc :viewer {:libraries (d/index-by :id libraries)
:project project :users (d/index-by :id users)
:objects objects :permissions permissions
:users (d/index-by :id users) :project project
:file file :pages pages
:page page :file file}))))))
:frames frames
:token token
:share-token share-token))))))
(defn fetch-comment-threads (defn fetch-comment-threads
[{:keys [file-id page-id] :as params}] [{:keys [file-id page-id] :as params}]
@ -168,32 +172,6 @@
(->> (rp/query :comments {:thread-id thread-id}) (->> (rp/query :comments {:thread-id thread-id})
(rx/map #(partial fetched %))))))) (rx/map #(partial fetched %)))))))
(defn create-share-link
[]
(ptk/reify ::create-share-link
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)]
(->> (rp/mutation! :create-file-share-token {:file-id file-id
:page-id page-id})
(rx/map (fn [{:keys [token]}]
#(assoc-in % [:viewer-data :token] token))))))))
(defn delete-share-link
[]
(ptk/reify ::delete-share-link
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
token (get-in state [:viewer-data :token])
params {:file-id file-id
:page-id page-id
:token token}]
(->> (rp/mutation :delete-file-share-token params)
(rx/map (fn [_] #(update % :viewer-data dissoc :token))))))))
;; --- Zoom Management ;; --- Zoom Management
(def increase-zoom (def increase-zoom
@ -245,29 +223,32 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route) qparams (:query-params route)
pparams (:path-params route) pparams (:path-params route)
index (:index qparams)] index (:index qparams)]
(when (pos? index) (when (pos? index)
(rx/of (rx/of
(dcm/close-thread) (dcm/close-thread)
(rt/nav screen pparams (assoc qparams :index (dec index))))))))) (rt/nav :viewer pparams (assoc qparams :index (dec index)))))))))
(def select-next-frame (def select-next-frame
(ptk/reify ::select-prev-frame (ptk/reify ::select-next-frame
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(prn "select-next-frame")
(let [route (:route state) (let [route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route)
pparams (:path-params route) pparams (:path-params route)
qparams (:query-params route)
page-id (:page-id qparams)
index (:index qparams) index (:index qparams)
total (count (get-in state [:viewer-data :frames]))]
total (count (get-in state [:viewer :pages page-id :frames]))]
(when (< index (dec total)) (when (< index (dec total))
(rx/of (rx/of
(dcm/close-thread) (dcm/close-thread)
(rt/nav screen pparams (assoc qparams :index (inc index))))))))) (rt/nav :viewer pparams (assoc qparams :index (inc index)))))))))
(s/def ::interactions-mode #{:hide :show :show-on-click}) (s/def ::interactions-mode #{:hide :show :show-on-click})
@ -309,7 +290,7 @@
(defn go-to-frame-by-index (defn go-to-frame-by-index
[index] [index]
(ptk/reify ::go-to-frame (ptk/reify ::go-to-frame-by-index
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [route (:route state)
@ -324,12 +305,15 @@
(ptk/reify ::go-to-frame (ptk/reify ::go-to-frame
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [frames (get-in state [:viewer-data :frames]) (let [route (:route state)
qparams (:query-params route)
page-id (:page-id qparams)
frames (get-in state [:viewer :pages page-id :frames])
index (d/index-of-pred frames #(= (:id %) frame-id))] index (d/index-of-pred frames #(= (:id %) frame-id))]
(when index (when index
(rx/of (go-to-frame-by-index index))))))) (rx/of (go-to-frame-by-index index)))))))
(defn go-to-section (defn go-to-section
[section] [section]
(ptk/reify ::go-to-section (ptk/reify ::go-to-section
@ -340,13 +324,6 @@
qparams (:query-params route)] qparams (:query-params route)]
(rx/of (rt/nav :viewer pparams (assoc qparams :section section))))))) (rx/of (rt/nav :viewer pparams (assoc qparams :section section)))))))
(defn set-current-frame [frame-id]
(ptk/reify ::set-current-frame
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-data :current-frame-id] frame-id))))
(defn deselect-all [] (defn deselect-all []
(ptk/reify ::deselect-all (ptk/reify ::deselect-all
ptk/UpdateEvent ptk/UpdateEvent
@ -376,7 +353,10 @@
(ptk/reify ::shift-select-to (ptk/reify ::shift-select-to
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [objects (get-in state [:viewer-data :objects]) (let [route (:route state)
qparams (:query-params route)
page-id (:page-id qparams)
objects (get-in state [:viewer :pages page-id :objects])
selection (-> state selection (-> state
(get-in [:viewer-local :selected] #{}) (get-in [:viewer-local :selected] #{})
(conj id))] (conj id))]
@ -389,8 +369,13 @@
(ptk/reify ::select-all (ptk/reify ::select-all
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [objects (get-in state [:viewer-data :objects]) (let [route (:route state)
frame-id (get-in state [:viewer-data :current-frame-id]) qparams (:query-params route)
page-id (:page-id qparams)
index (:index qparams)
objects (get-in state [:viewer :pages page-id :objects])
frame-id (get-in state [:viewer :pages page-id :frames index :id])
selection (->> objects selection (->> objects
(filter #(= (:frame-id (second %)) frame-id)) (filter #(= (:frame-id (second %)) frame-id))
(map first) (map first)
@ -405,18 +390,50 @@
(let [toggled? (contains? (get-in state [:viewer-local :collapsed]) id)] (let [toggled? (contains? (get-in state [:viewer-local :collapsed]) id)]
(update-in state [:viewer-local :collapsed] (if toggled? disj conj) id))))) (update-in state [:viewer-local :collapsed] (if toggled? disj conj) id)))))
(defn hover-shape [id hover?] (defn hover-shape
[id hover?]
(ptk/reify ::hover-shape (ptk/reify ::hover-shape
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:viewer-local :hover] (when hover? id))))) (assoc-in state [:viewer-local :hover] (when hover? id)))))
;; --- NAV
(defn go-to-dashboard (defn go-to-dashboard
([] (go-to-dashboard nil)) []
([{:keys [team-id]}] (ptk/reify ::go-to-dashboard
(ptk/reify ::go-to-dashboard ptk/WatchEvent
ptk/WatchEvent (watch [_ state _]
(let [team-id (get-in state [:viewer :project :team-id])
params {:team-id team-id}]
(rx/of (rt/nav :dashboard-projects params))))))
(defn go-to-page
[page-id]
(ptk/reify ::go-to-page
ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [team-id (or team-id (get-in state [:viewer-data :project :team-id]))] (let [route (:route state)
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))) pparams (:path-params route)
qparams (-> (:query-params route)
(assoc :index 0)
(assoc :page-id page-id))
rname (get-in route [:data :name])]
(rx/of (rt/nav rname pparams qparams))))))
(defn go-to-workspace
[page-id]
(ptk/reify ::go-to-workspace
ptk/WatchEvent
(watch [_ state _]
(let [project-id (get-in state [:viewer :project :id])
file-id (get-in state [:viewer :file :id])
pparams {:project-id project-id :file-id file-id}
qparams {:page-id page-id}]
(rx/of (rt/nav-new-window*
{:rname :workspace
:path-params pparams
:query-params qparams
:name (str "workspace-" file-id)}))))))

View file

@ -20,6 +20,7 @@
[app.common.transit :as t] [app.common.transit :as t]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cfg] [app.config :as cfg]
[app.main.data.events :as ev]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.workspace.changes :as dch] [app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
@ -37,6 +38,7 @@
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.streams :as ms] [app.main.streams :as ms]
[app.main.worker :as uw] [app.main.worker :as uw]
[app.util.globals :as ug]
[app.util.http :as http] [app.util.http :as http]
[app.util.i18n :as i18n] [app.util.i18n :as i18n]
[app.util.router :as rt] [app.util.router :as rt]
@ -48,7 +50,6 @@
[potok.core :as ptk])) [potok.core :as ptk]))
;; (log/set-level! :trace) ;; (log/set-level! :trace)
;; --- Specs
(s/def ::shape-attrs ::cp/shape-attrs) (s/def ::shape-attrs ::cp/shape-attrs)
(s/def ::set-of-string (s/def ::set-of-string
@ -87,7 +88,7 @@
:snap-grid :snap-grid
:dynamic-alignment}) :dynamic-alignment})
(def layout-names (def layout-presets
{:assets {:assets
{:del #{:sitemap :layers :document-history } {:del #{:sitemap :layers :document-history }
:add #{:assets}} :add #{:assets}}
@ -121,22 +122,31 @@
:picked-color nil :picked-color nil
:picked-color-select false}) :picked-color-select false})
(declare ensure-layout) (defn ensure-layout
[lname]
(defn initialize-layout (ptk/reify ::ensure-layout
[layout-name]
(us/verify (s/nilable ::us/keyword) layout-name)
(ptk/reify ::initialize-layout
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :workspace-layout (update state :workspace-layout
(fn [layout] (fn [stored]
(or layout default-layout)))) (let [todel (get-in layout-presets [lname :del] #{})
toadd (get-in layout-presets [lname :add] #{})]
(-> stored
(set/difference todel)
(set/union toadd))))))))
(defn setup-layout
[lname]
(us/verify (s/nilable ::us/keyword) lname)
(ptk/reify ::setup-layout
ptk/UpdateEvent
(update [_ state]
(update state :workspace-layout #(or % default-layout)))
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(if (and layout-name (contains? layout-names layout-name)) (if (and lname (contains? layout-presets lname))
(rx/of (ensure-layout layout-name)) (rx/of (ensure-layout lname))
(rx/of (ensure-layout :layers)))))) (rx/of (ensure-layout :layers))))))
(defn initialize-file (defn initialize-file
@ -171,7 +181,12 @@
(->> stream (->> stream
(rx/filter #(= ::dwc/index-initialized %)) (rx/filter #(= ::dwc/index-initialized %))
(rx/first) (rx/first)
(rx/map #(file-initialized bundle))))))))))) (rx/map #(file-initialized bundle)))))))))
ptk/EffectEvent
(effect [_ _ _]
(let [name (str "workspace-" file-id)]
(unchecked-set ug/global "name" name)))))
(defn- file-initialized (defn- file-initialized
[{:keys [file users project libraries] :as bundle}] [{:keys [file users project libraries] :as bundle}]
@ -219,8 +234,10 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (dwn/finalize file-id) (rx/merge
::dwp/finalize)))) (rx/of (dwn/finalize file-id))
(->> (rx/of ::dwp/finalize)
(rx/observe-on :async))))))
(defn initialize-page (defn initialize-page
[page-id] [page-id]
@ -274,7 +291,7 @@
(watch [it state _] (watch [it state _]
(let [pages (get-in state [:workspace-data :pages-index]) (let [pages (get-in state [:workspace-data :pages-index])
unames (dwc/retrieve-used-names pages) unames (dwc/retrieve-used-names pages)
name (dwc/generate-unique-name unames "Page") name (dwc/generate-unique-name unames "Page-1")
rchange {:type :add-page rchange {:type :add-page
:id id :id id
@ -348,7 +365,6 @@
(when (= id (:current-page-id state)) (when (= id (:current-page-id state))
go-to-file)))))) go-to-file))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WORKSPACE File Actions ;; WORKSPACE File Actions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -357,6 +373,10 @@
[id name] [id name]
{:pre [(uuid? id) (string? name)]} {:pre [(uuid? id) (string? name)]}
(ptk/reify ::rename-file (ptk/reify ::rename-file
IDeref
(-deref [_]
{::ev/origin "workspace" :id id :name name})
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:workspace-file :name] name)) (assoc-in state [:workspace-file :name] name))
@ -373,6 +393,9 @@
;; --- Viewport Sizing ;; --- Viewport Sizing
(declare increase-zoom)
(declare decrease-zoom)
(declare set-zoom)
(declare zoom-to-fit-all) (declare zoom-to-fit-all)
(defn initialize-viewport (defn initialize-viewport
@ -457,7 +480,6 @@
(update :height #(/ % hprop)) (update :height #(/ % hprop))
(assoc :left-offset left-offset)))))))))))) (assoc :left-offset left-offset))))))))))))
(defn start-panning [] (defn start-panning []
(ptk/reify ::start-panning (ptk/reify ::start-panning
ptk/WatchEvent ptk/WatchEvent
@ -484,23 +506,32 @@
(-> state (-> state
(update :workspace-local dissoc :panning))))) (update :workspace-local dissoc :panning)))))
(defn start-zooming [pt]
(ptk/reify ::start-zooming
ptk/WatchEvent
(watch [_ state stream]
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-zooming)))]
(when-not (get-in state [:workspace-local :zooming])
(rx/concat
(rx/of #(-> % (assoc-in [:workspace-local :zooming] true)))
(->> stream
(rx/filter ms/pointer-event?)
(rx/filter #(= :delta (:source %)))
(rx/map :pt)
(rx/take-until stopper)
(rx/map (fn [delta]
(let [scale (+ 1 (/ (:y delta) 100))] ;; this number may be adjusted after user testing
(set-zoom pt scale)))))))))))
;; --- Toggle layout flag (defn finish-zooming []
(ptk/reify ::finish-zooming
(defn ensure-layout
[layout-name]
(assert (contains? layout-names layout-name)
(str "unexpected layout name: " layout-name))
(ptk/reify ::ensure-layout
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :workspace-layout (-> state
(fn [stored] (update :workspace-local dissoc :zooming)))))
(let [todel (get-in layout-names [layout-name :del] #{})
toadd (get-in layout-names [layout-name :add] #{})]
(-> stored ;; --- Toggle layout flag
(set/difference todel)
(set/union toadd))))))))
(defn toggle-layout-flags (defn toggle-layout-flags
[& flags] [& flags]
@ -569,6 +600,16 @@
(update state :workspace-local (update state :workspace-local
#(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01))))))) #(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01)))))))
(defn set-zoom
[center scale]
(ptk/reify ::set-zoom
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local
#(impl-update-zoom % center (fn [z] (-> (* z scale)
(max 0.01)
(min 200))))))))
(def reset-zoom (def reset-zoom
(ptk/reify ::reset-zoom (ptk/reify ::reset-zoom
ptk/UpdateEvent ptk/UpdateEvent
@ -1058,6 +1099,9 @@
:group :group
(rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)]))) (rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)])))
:svg-raw
nil
(rx/of (dwc/start-edition-mode id) (rx/of (dwc/start-edition-mode id)
(dwdp/start-path-edit id))))))))) (dwdp/start-path-edit id)))))))))
@ -1089,7 +1133,7 @@
(defn align-objects (defn align-objects
[axis] [axis]
(us/verify ::gal/align-axis axis) (us/verify ::gal/align-axis axis)
(ptk/reify :align-objects (ptk/reify ::align-objects
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
@ -1120,7 +1164,7 @@
(defn distribute-objects (defn distribute-objects
[axis] [axis]
(us/verify ::gal/dist-axis axis) (us/verify ::gal/dist-axis axis)
(ptk/reify :align-objects (ptk/reify ::distribute-objects
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
@ -1195,7 +1239,7 @@
(rx/of (rt/nav' :workspace pparams qparams)))))) (rx/of (rt/nav' :workspace pparams qparams))))))
([page-id] ([page-id]
(us/verify ::us/uuid page-id) (us/verify ::us/uuid page-id)
(ptk/reify ::go-to-page (ptk/reify ::go-to-page-2
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [project-id (:current-project-id state) (let [project-id (:current-project-id state)
@ -1207,7 +1251,10 @@
(defn go-to-layout (defn go-to-layout
[layout] [layout]
(us/verify ::layout-flag layout) (us/verify ::layout-flag layout)
(ptk/reify ::go-to-layout (ptk/reify ::set-workspace-layout
IDeref
(-deref [_] {:layout layout})
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [project-id (get-in state [:workspace-project :id]) (let [project-id (get-in state [:workspace-project :id])
@ -1234,10 +1281,14 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [{:keys [current-file-id current-page-id]} state (let [{:keys [current-file-id current-page-id]} state
params {:file-id (or file-id current-file-id) pparams {:file-id (or file-id current-file-id)}
:page-id (or page-id current-page-id)}] qparams {:page-id (or page-id current-page-id)
:index 0}]
(rx/of ::dwp/force-persist (rx/of ::dwp/force-persist
(rt/nav-new-window :viewer params {:index 0}))))))) (rt/nav-new-window* {:rname :viewer
:path-params pparams
:query-params qparams
:name (str "viewer-" (:file-id pparams))})))))))
(defn go-to-dashboard (defn go-to-dashboard
([] (go-to-dashboard nil)) ([] (go-to-dashboard nil))
@ -1251,7 +1302,7 @@
(defn go-to-dashboard-fonts (defn go-to-dashboard-fonts
[] []
(ptk/reify ::go-to-dashboard (ptk/reify ::go-to-dashboard-fonts
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [team-id (:current-team-id state)] (let [team-id (:current-team-id state)]

View file

@ -69,7 +69,7 @@
(defn show-palette (defn show-palette
"Show the palette tool and change the library it uses" "Show the palette tool and change the library it uses"
[selected] [selected]
(ptk/reify ::change-palette-selected (ptk/reify ::show-palette
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state

View file

@ -71,7 +71,7 @@
(defn center-to-comment-thread (defn center-to-comment-thread
[{:keys [position] :as thread}] [{:keys [position] :as thread}]
(us/assert ::dcm/comment-thread thread) (us/assert ::dcm/comment-thread thread)
(ptk/reify :center-to-comment-thread (ptk/reify ::center-to-comment-thread
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :workspace-local (update state :workspace-local
@ -89,7 +89,7 @@
(defn navigate (defn navigate
[thread] [thread]
(us/assert ::dcm/comment-thread thread) (us/assert ::dcm/comment-thread thread)
(ptk/reify ::navigate (ptk/reify ::open-comment-thread
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ stream] (watch [_ _ stream]
(let [pparams {:project-id (:project-id thread) (let [pparams {:project-id (:project-id thread)

View file

@ -68,19 +68,17 @@
(defn generate-unique-name (defn generate-unique-name
"A unique name generator" "A unique name generator"
([used basename] [used basename]
(generate-unique-name used basename false)) (s/assert ::set-of-string used)
([used basename prefix-first?] (s/assert ::us/string basename)
(s/assert ::set-of-string used) (if-not (contains? used basename)
(s/assert ::us/string basename) basename
(let [[prefix initial] (extract-numeric-suffix basename)] (let [[prefix initial] (extract-numeric-suffix basename)]
(loop [counter initial] (loop [counter initial]
(let [candidate (if (and (= 1 counter) prefix-first?) (let [candidate (str prefix "-" counter)]
(str prefix) (if (contains? used candidate)
(str prefix "-" counter))] (recur (inc counter))
(if (contains? used candidate) candidate))))))
(recur (inc counter))
candidate))))))
;; --- Shape attrs (Layers Sidebar) ;; --- Shape attrs (Layers Sidebar)
@ -144,6 +142,38 @@
:origin it :origin it
:save-undo? false})))))))))) :save-undo? false}))))))))))
(defn undo-to-index
"Repeat undoing or redoing until dest-index is reached."
[dest-index]
(ptk/reify ::undo-to-index
ptk/WatchEvent
(watch [it state _]
(let [edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
(when-not (or (some? edition) (not-empty drawing))
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))]
(when (and (some? items)
(<= 0 dest-index (dec (count items))))
(let [changes (vec (apply concat
(cond
(< dest-index index)
(->> (subvec items (inc dest-index) (inc index))
(reverse)
(map :undo-changes))
(> dest-index index)
(->> (subvec items (inc index) (inc dest-index))
(map :redo-changes))
:else [])))]
(when (seq changes)
(rx/of (dwu/materialize-undo changes dest-index)
(dch/commit-changes {:redo-changes changes
:undo-changes []
:origin it
:save-undo? false})))))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shapes ;; Shapes
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -54,7 +54,7 @@
(defn remove-frame-grid (defn remove-frame-grid
[frame-id index] [frame-id index]
(ptk/reify ::set-frame-grid (ptk/reify ::remove-frame-grid
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (dch/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) [])))))))) (rx/of (dch/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) []))))))))

View file

@ -182,7 +182,7 @@
shapes (shapes-for-grouping objects selected)] shapes (shapes-for-grouping objects selected)]
(when-not (empty? shapes) (when-not (empty? shapes)
(let [[group rchanges uchanges] (let [[group rchanges uchanges]
(prepare-create-group objects page-id shapes "Group" false)] (prepare-create-group objects page-id shapes "Group-1" false)]
(rx/of (dch/commit-changes {:redo-changes rchanges (rx/of (dch/commit-changes {:redo-changes rchanges
:undo-changes uchanges :undo-changes uchanges
:origin it}) :origin it})
@ -221,7 +221,7 @@
(if (and (= (count shapes) 1) (if (and (= (count shapes) 1)
(= (:type (first shapes)) :group)) (= (:type (first shapes)) :group))
[(first shapes) [] []] [(first shapes) [] []]
(prepare-create-group objects page-id shapes "Group" true)) (prepare-create-group objects page-id shapes "Group-1" true))
rchanges (d/concat rchanges rchanges (d/concat rchanges
[{:type :mod-obj [{:type :mod-obj

View file

@ -83,12 +83,15 @@
(defn add-color (defn add-color
[color] [color]
(let [id (uuid/next) (let [id (uuid/next)
color (assoc color color (-> color
:id id (assoc :id id)
:name (default-color-name color))] (assoc :name (default-color-name color)))]
(us/assert ::cp/color color) (us/assert ::cp/color color)
(ptk/reify ::add-color (ptk/reify ::add-color
IDeref
(-deref [_] color)
ptk/WatchEvent ptk/WatchEvent
(watch [it _ _] (watch [it _ _]
(let [rchg {:type :add-color (let [rchg {:type :add-color
@ -211,6 +214,9 @@
(let [typography (update typography :id #(or % (uuid/next)))] (let [typography (update typography :id #(or % (uuid/next)))]
(us/assert ::cp/typography typography) (us/assert ::cp/typography typography)
(ptk/reify ::add-typography (ptk/reify ::add-typography
IDeref
(-deref [_] typography)
ptk/WatchEvent ptk/WatchEvent
(watch [it _ _] (watch [it _ _]
(let [rchg {:type :add-typography (let [rchg {:type :add-typography
@ -258,17 +264,20 @@
:undo-changes [uchg] :undo-changes [uchg]
:origin it})))))) :origin it}))))))
(def add-component
"Add a new component to current file library, from the currently selected shapes." (defn- add-component2
(ptk/reify ::add-component "This is the second step of the component creation."
[selected]
(ptk/reify ::add-component2
IDeref
(-deref [_] {:num-shapes (count selected)})
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [file-id (:current-file-id state) (let [file-id (:current-file-id state)
page-id (:current-page-id state) page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id) objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state) shapes (dwg/shapes-for-grouping objects selected)]
selected (cp/clean-loops objects selected)
shapes (dwg/shapes-for-grouping objects selected)]
(when-not (empty? shapes) (when-not (empty? shapes)
(let [[group rchanges uchanges] (let [[group rchanges uchanges]
(dwlh/generate-add-component shapes objects page-id file-id)] (dwlh/generate-add-component shapes objects page-id file-id)]
@ -278,6 +287,20 @@
:origin it}) :origin it})
(dwc/select-shapes (d/ordered-set (:id group))))))))))) (dwc/select-shapes (d/ordered-set (:id group)))))))))))
(defn add-component
"Add a new component to current file library, from the currently selected shapes.
This operation is made in two steps, first one for calculate the
shapes that will be part of the component and the second one with
the component creation."
[]
(ptk/reify ::add-component
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
selected (->> (wsh/lookup-selected state)
(cp/clean-loops objects))]
(rx/of (add-component2 selected))))))
(defn rename-component (defn rename-component
"Rename the component with the given id, in the current file library." "Rename the component with the given id, in the current file library."
[id new-name] [id new-name]
@ -462,6 +485,31 @@
:undo-changes uchanges :undo-changes uchanges
:origin it})))))) :origin it}))))))
(def detach-selected-components
(ptk/reify ::detach-selected-components
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
local-library (dwlh/get-local-file state)
container (cp/get-container page-id :page local-library)
selected (->> state
(wsh/lookup-selected)
(cp/clean-loops objects))
[rchanges uchanges]
(reduce (fn [changes id]
(dwlh/concat-changes
changes
(dwlh/generate-detach-instance id container)))
dwlh/empty-changes
selected)]
(rx/of (dch/commit-changes {:redo-changes rchanges
:undo-changes uchanges
:origin it}))))))
(defn nav-to-component-file (defn nav-to-component-file
[file-id] [file-id]
(us/assert ::us/uuid file-id) (us/assert ::us/uuid file-id)

View file

@ -129,7 +129,7 @@
(if (and (= (count shapes) 1) (if (and (= (count shapes) 1)
(= (:type (first shapes)) :group)) (= (:type (first shapes)) :group))
[(first shapes) [] []] [(first shapes) [] []]
(dwg/prepare-create-group objects page-id shapes "Component" true)) (dwg/prepare-create-group objects page-id shapes "Component-1" true))
[new-shape new-shapes updated-shapes] [new-shape new-shapes updated-shapes]
(make-component-shape group objects file-id) (make-component-shape group objects file-id)

View file

@ -59,7 +59,8 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [wsession (get-in state [:ws file-id]) (let [wsession (get-in state [:ws file-id])
stoper (rx/filter #(= ::finalize %) stream) stoper (->> stream
(rx/filter (ptk/type? ::finalize)))
interval (* 1000 60)] interval (* 1000 60)]
(->> (rx/merge (->> (rx/merge
;; Each 60 seconds send a keepalive message for maintain ;; Each 60 seconds send a keepalive message for maintain
@ -106,7 +107,7 @@
(defn- handle-pointer-send (defn- handle-pointer-send
[file-id point] [file-id point]
(ptk/reify ::handle-pointer-update (ptk/reify ::handle-pointer-send
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (effect [_ state _]
(let [ws (get-in state [:ws file-id]) (let [ws (get-in state [:ws file-id])
@ -122,11 +123,10 @@
(defn finalize (defn finalize
[file-id] [file-id]
(ptk/reify ::finalize (ptk/reify ::finalize
ptk/WatchEvent ptk/EffectEvent
(watch [_ state _] (effect [_ state _]
(when-let [ws (get-in state [:ws file-id])] (when-let [ws (get-in state [:ws file-id])]
(ws/-close ws)) (ws/-close ws)))))
(rx/of ::finalize))))
;; --- Handle: Presence ;; --- Handle: Presence

View file

@ -179,7 +179,7 @@
:right (gpt/point 1 0))) :right (gpt/point 1 0)))
(defn finish-move-selected [] (defn finish-move-selected []
(ptk/reify ::move-selected (ptk/reify ::finish-move-selected
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [id (get-in state [:workspace-local :edition])] (let [id (get-in state [:workspace-local :edition])]

View file

@ -18,7 +18,7 @@
(defn end-path-event? [event] (defn end-path-event? [event]
(or (= (ptk/type event) ::common/finish-path) (or (= (ptk/type event) ::common/finish-path)
(= (ptk/type event) :esc-pressed) (= (ptk/type event) :app.main.data.workspace.path.shortcuts/esc-pressed)
(= :app.main.data.workspace.common/clear-edition-mode (ptk/type event)) (= :app.main.data.workspace.common/clear-edition-mode (ptk/type event))
(= :app.main.data.workspace/finalize-page (ptk/type event)) (= :app.main.data.workspace/finalize-page (ptk/type event))
(= event :interrupt) ;; ESC (= event :interrupt) ;; ESC

View file

@ -20,7 +20,7 @@
;; Shortcuts format https://github.com/ccampbell/mousetrap ;; Shortcuts format https://github.com/ccampbell/mousetrap
(defn esc-pressed [] (defn esc-pressed []
(ptk/reify :esc-pressed (ptk/reify ::esc-pressed
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
;; Not interrupt when we're editing a path ;; Not interrupt when we're editing a path

View file

@ -90,7 +90,7 @@
"Joins the head with the previous undo in one. This is done so when the user changes a "Joins the head with the previous undo in one. This is done so when the user changes a
node handlers after adding it the undo merges both in one operation only" node handlers after adding it the undo merges both in one operation only"
[] []
(ptk/reify ::add-undo-entry (ptk/reify ::merge-head
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [id (st/get-path-id state) (let [id (st/get-path-id state)

View file

@ -12,6 +12,7 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.fonts :as df] [app.main.data.fonts :as df]
[app.main.data.media :as di] [app.main.data.media :as di]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
@ -275,6 +276,10 @@
[id is-shared] [id is-shared]
{:pre [(uuid? id) (boolean? is-shared)]} {:pre [(uuid? id) (boolean? is-shared)]}
(ptk/reify ::set-file-shared (ptk/reify ::set-file-shared
IDeref
(-deref [_]
{::ev/origin "workspace" :id id :shared is-shared})
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:workspace-file :is-shared] is-shared)) (assoc-in state [:workspace-file :is-shared] is-shared))
@ -313,7 +318,7 @@
(defn link-file-to-library (defn link-file-to-library
[file-id library-id] [file-id library-id]
(ptk/reify ::link-file-to-library (ptk/reify ::attach-library
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(let [fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1) (let [fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1)
@ -325,7 +330,7 @@
(defn unlink-file-from-library (defn unlink-file-from-library
[file-id library-id] [file-id library-id]
(ptk/reify ::unlink-file-from-library (ptk/reify ::detach-library
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(d/dissoc-in state [:workspace-libraries library-id])) (d/dissoc-in state [:workspace-libraries library-id]))

View file

@ -114,7 +114,7 @@
(defn deselect-shape (defn deselect-shape
[id] [id]
(us/verify ::us/uuid id) (us/verify ::us/uuid id)
(ptk/reify ::select-shape (ptk/reify ::deselect-shape
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update-in state [:workspace-local :selected] disj id)))) (update-in state [:workspace-local :selected] disj id))))
@ -219,17 +219,15 @@
lks/empty-linked-set) lks/empty-linked-set)
selrect (get-in state [:workspace-local :selrect]) selrect (get-in state [:workspace-local :selrect])
blocked? (fn [id] (get-in objects [id :blocked] false))] blocked? (fn [id] (get-in objects [id :blocked] false))]
(rx/merge (when selrect
(->> (uw/ask! {:cmd :selection/query
(when selrect :page-id page-id
(->> (uw/ask! {:cmd :selection/query :rect selrect
:page-id page-id :include-frames? true
:rect selrect :full-frame? true})
:include-frames? true (rx/map #(cp/clean-loops objects %))
:full-frame? true}) (rx/map #(into initial-set (filter (comp not blocked?)) %))
(rx/map #(cp/clean-loops objects %)) (rx/map select-shapes)))))))
(rx/map #(into initial-set (filter (comp not blocked?)) %))
(rx/map select-shapes))))))))
(defn select-inside-group (defn select-inside-group
[group-id position] [group-id position]
@ -383,6 +381,53 @@
(into [fch] sch))) (into [fch] sch)))
(defn clear-memorize-duplicated
[]
(ptk/reify ::clear-memorize-duplicated
ptk/UpdateEvent
(update [_ state]
(d/dissoc-in state [:workspace-local :duplicated]))))
(defn memorize-duplicated
"When duplicate an object, remember the operation during the following seconds.
If the user moves the duplicated object, and then duplicates it again, check
the displacement and apply it to the third copy. This is useful for doing
grids or cascades of cloned objects."
[id-original id-duplicated]
(ptk/reify ::memorize-duplicated
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :duplicated] {:id-original id-original
:id-duplicated id-duplicated}))
ptk/WatchEvent
(watch [_ _ stream]
(let [stoper (rx/filter (ptk/type? ::memorize-duplicated) stream)]
(->> (rx/timer 10000) ;; This time may be adjusted after some user testing.
(rx/take-until stoper)
(rx/map clear-memorize-duplicated))))))
(defn calc-duplicate-delta
[obj state objects]
(let [{:keys [id-original id-duplicated]}
(get-in state [:workspace-local :duplicated])]
(if (and (not= id-original (:id obj))
(not= id-duplicated (:id obj)))
;; The default is leave normal shapes in place, but put
;; new frames to the right of the original.
(if (= (:type obj) :frame)
(gpt/point (+ (:width obj) 50) 0)
(gpt/point 0 0))
(let [obj-original (get objects id-original)
obj-duplicated (get objects id-duplicated)
distance (gpt/subtract (gpt/point obj-duplicated)
(gpt/point obj-original))
new-pos (gpt/add (gpt/point obj-duplicated) distance)
delta (gpt/subtract new-pos (gpt/point obj))]
delta))))
(def duplicate-selected (def duplicate-selected
(ptk/reify ::duplicate-selected (ptk/reify ::duplicate-selected
ptk/WatchEvent ptk/WatchEvent
@ -390,7 +435,10 @@
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id) objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state) selected (wsh/lookup-selected state)
delta (gpt/point 0 0) delta (if (= (count selected) 1)
(let [obj (get objects (first selected))]
(calc-duplicate-delta obj state objects))
(gpt/point 0 0))
unames (dwc/retrieve-used-names objects) unames (dwc/retrieve-used-names objects)
@ -400,15 +448,20 @@
uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %)) uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %))
(reverse rchanges)) (reverse rchanges))
id-original (when (= (count selected) 1) (first selected))
selected (->> rchanges selected (->> rchanges
(filter #(selected (:old-id %))) (filter #(selected (:old-id %)))
(map #(get-in % [:obj :id])) (map #(get-in % [:obj :id]))
(into (d/ordered-set)))] (into (d/ordered-set)))
id-duplicated (when (= (count selected) 1) (first selected))]
(rx/of (dch/commit-changes {:redo-changes rchanges (rx/of (dch/commit-changes {:redo-changes rchanges
:undo-changes uchanges :undo-changes uchanges
:origin it}) :origin it})
(select-shapes selected)))))) (select-shapes selected)
(memorize-duplicated id-original id-duplicated))))))
(defn change-hover-state (defn change-hover-state
[id value] [id value]

View file

@ -91,7 +91,11 @@
:create-component {:tooltip (ds/meta "K") :create-component {:tooltip (ds/meta "K")
:command (ds/c-mod "k") :command (ds/c-mod "k")
:fn #(st/emit! dwl/add-component)} :fn #(st/emit! (dwl/add-component))}
:detach-component {:tooltip (ds/meta-shift "K")
:command (ds/c-mod "shift+k")
:fn #(st/emit! dwl/detach-selected-components)}
:flip-vertical {:tooltip (ds/shift "V") :flip-vertical {:tooltip (ds/shift "V")
:command "shift+v" :command "shift+v"

View file

@ -95,7 +95,11 @@
(d/parse-double)))))) (d/parse-double))))))
(defn setup-stroke [shape] (defn setup-stroke [shape]
(let [shape (let [stroke-linecap (-> (or (get-in shape [:svg-attrs :stroke-linecap])
(get-in shape [:svg-attrs :style :stroke-linecap]))
((d/nilf str/trim))
((d/nilf keyword)))
shape
(cond-> shape (cond-> shape
(uc/color? (get-in shape [:svg-attrs :stroke])) (uc/color? (get-in shape [:svg-attrs :stroke]))
(-> (update :svg-attrs dissoc :stroke) (-> (update :svg-attrs dissoc :stroke)
@ -113,8 +117,16 @@
(get-in shape [:svg-attrs :style :stroke-width]) (get-in shape [:svg-attrs :style :stroke-width])
(-> (update-in [:svg-attrs :style] dissoc :stroke-width) (-> (update-in [:svg-attrs :style] dissoc :stroke-width)
(assoc :stroke-width (-> (get-in shape [:svg-attrs :style :stroke-width]) (assoc :stroke-width (-> (get-in shape [:svg-attrs :style :stroke-width])
(d/parse-double)))))] (d/parse-double))))
(if (d/any-key? shape :stroke-color :stroke-opacity :stroke-width)
(and stroke-linecap (= (:type shape) :path))
(-> (update-in [:svg-attrs :style] dissoc :stroke-linecap)
(cond->
(#{:round :square} stroke-linecap)
(assoc :stroke-cap-start stroke-linecap
:stroke-cap-end stroke-linecap))))]
(if (d/any-key? shape :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end)
(merge {:stroke-style :svg} shape) (merge {:stroke-style :svg} shape)
shape))) shape)))
@ -331,7 +343,7 @@
(let [{:keys [tag attrs]} element-data (let [{:keys [tag attrs]} element-data
attrs (usvg/format-styles attrs) attrs (usvg/format-styles attrs)
element-data (cond-> element-data (map? element-data) (assoc :attrs attrs)) element-data (cond-> element-data (map? element-data) (assoc :attrs attrs))
name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag)) true) name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag)))
att-refs (usvg/find-attr-references attrs) att-refs (usvg/find-attr-references attrs)
references (usvg/find-def-references (:defs svg-data) att-refs) references (usvg/find-def-references (:defs svg-data) att-refs)

View file

@ -11,6 +11,7 @@
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.spec :as us] [app.common.spec :as us]
[app.main.data.workspace.changes :as dch] [app.main.data.workspace.changes :as dch]
@ -222,7 +223,7 @@
root root
transformed-root)))] transformed-root)))]
(reduce set-child (reduce set-child
(update-in modif-tree [(:id shape) :modifiers] #(merge % modifiers)) (assoc-in modif-tree [(:id shape) :modifiers] modifiers)
children))) children)))
(defn- check-delta (defn- check-delta
@ -281,7 +282,7 @@
(defn start-resize (defn start-resize
"Enter mouse resize mode, until mouse button is released." "Enter mouse resize mode, until mouse button is released."
[handler ids shape] [handler ids shape]
(letfn [(resize [shape initial layout [point lock? point-snap]] (letfn [(resize [shape initial layout [point lock? center? point-snap]]
(let [{:keys [width height]} (:selrect shape) (let [{:keys [width height]} (:selrect shape)
{:keys [rotation]} shape {:keys [rotation]} shape
rotation (or rotation 0) rotation (or rotation 0)
@ -315,17 +316,34 @@
scalev) scalev)
;; Resize origin point given the selected handler
origin (handler-resize-origin (:selrect shape) handler)
shape-center (gsh/center-shape shape)
shape-transform (:transform shape (gmt/matrix)) shape-transform (:transform shape (gmt/matrix))
shape-transform-inverse (:transform-inverse shape (gmt/matrix)) shape-transform-inverse (:transform-inverse shape (gmt/matrix))
shape-center (gsh/center-shape shape) ;; If we want resize from center, displace the shape
;; so it is still centered after resize.
displacement (when center?
(-> shape-center
(gpt/subtract origin)
(gpt/multiply scalev)
(gpt/add origin)
(gpt/subtract shape-center)
(gpt/multiply (gpt/point -1 -1))
(gpt/transform shape-transform)))
;; Resize origin point given the selected handler origin (cond-> (gsh/transform-point-center origin shape-center shape-transform)
origin (-> (handler-resize-origin (:selrect shape) handler) (some? displacement)
(gsh/transform-point-center shape-center shape-transform))] (gpt/add displacement))
displacement (when (some? displacement)
(gmt/translate-matrix displacement))]
(rx/of (set-modifiers ids (rx/of (set-modifiers ids
{:resize-vector scalev {:displacement displacement
:resize-vector scalev
:resize-origin origin :resize-origin origin
:resize-transform shape-transform :resize-transform shape-transform
:resize-scale-text scale-text :resize-scale-text scale-text
@ -334,9 +352,9 @@
;; Unifies the instantaneous proportion lock modifier ;; Unifies the instantaneous proportion lock modifier
;; activated by Shift key and the shapes own proportion ;; activated by Shift key and the shapes own proportion
;; lock flag that can be activated on element options. ;; lock flag that can be activated on element options.
(normalize-proportion-lock [[point shift?]] (normalize-proportion-lock [[point shift? alt?]]
(let [proportion-lock? (:proportion-lock shape)] (let [proportion-lock? (:proportion-lock shape)]
[point (or proportion-lock? shift?)]))] [point (or proportion-lock? shift?) alt?]))]
(reify (reify
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
@ -358,11 +376,11 @@
(rx/concat (rx/concat
(rx/of (dch/update-shapes text-shapes-ids #(assoc % :grow-type :fixed))) (rx/of (dch/update-shapes text-shapes-ids #(assoc % :grow-type :fixed)))
(->> ms/mouse-position (->> ms/mouse-position
(rx/with-latest vector ms/mouse-position-shift) (rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt)
(rx/map normalize-proportion-lock) (rx/map normalize-proportion-lock)
(rx/switch-map (fn [[point :as current]] (rx/switch-map (fn [[point _ _ :as current]]
(->> (snap/closest-snap-point page-id resizing-shapes layout zoom point) (->> (snap/closest-snap-point page-id resizing-shapes layout zoom point)
(rx/map #(conj current %))))) (rx/map #(conj current %)))))
(rx/mapcat (partial resize shape initial-position layout)) (rx/mapcat (partial resize shape initial-position layout))
(rx/take-until stoper)) (rx/take-until stoper))
(rx/of (apply-modifiers ids) (rx/of (apply-modifiers ids)
@ -493,7 +511,7 @@
(defn- start-move-duplicate (defn- start-move-duplicate
[from-position] [from-position]
(ptk/reify ::start-move-selected (ptk/reify ::start-move-duplicate
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ stream] (watch [_ _ stream]
(->> stream (->> stream
@ -521,10 +539,18 @@
layout (get state :workspace-layout) layout (get state :workspace-layout)
zoom (get-in state [:workspace-local :zoom] 1) zoom (get-in state [:workspace-local :zoom] 1)
fix-axis (fn [[position shift?]]
(let [delta (gpt/to-vec from-position position)]
(if shift?
(if (> (mth/abs (:x delta)) (mth/abs (:y delta)))
(gpt/point (:x delta) 0)
(gpt/point 0 (:y delta)))
delta)))
position (->> ms/mouse-position position (->> ms/mouse-position
(rx/take-until stopper) (rx/take-until stopper)
(rx/map #(gpt/to-vec from-position %))) (rx/with-latest-from ms/mouse-position-shift)
(rx/map #(fix-axis %)))
snap-delta (rx/concat snap-delta (rx/concat
;; We send the nil first so the stream is not waiting for the first value ;; We send the nil first so the stream is not waiting for the first value

View file

@ -44,7 +44,8 @@
(defn- calculate-dimensions (defn- calculate-dimensions
[{:keys [objects] :as data} vport] [{:keys [objects] :as data} vport]
(let [shapes (cp/select-toplevel-shapes objects {:include-frames? true}) (let [shapes (cp/select-toplevel-shapes objects {:include-frames? true
:include-frame-children? false})
to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val)) to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val))
rect (cond->> (gsh/selection-rect shapes) rect (cond->> (gsh/selection-rect shapes)
(some? vport) (some? vport)
@ -131,7 +132,8 @@
(mf/defc page-svg (mf/defc page-svg
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [data width height thumbnails? embed?] :as props}] [{:keys [data width height thumbnails? embed? include-metadata?] :as props
:or {embed? false include-metadata? false}}]
(let [objects (:objects data) (let [objects (:objects data)
root (get objects uuid/zero) root (get objects uuid/zero)
shapes shapes
@ -158,35 +160,36 @@
(mf/deps objects) (mf/deps objects)
#(shape-wrapper-factory objects))] #(shape-wrapper-factory objects))]
[:& (mf/provider embed/context) {:value embed?} [:& (mf/provider embed/context) {:value embed?}
[:svg {:view-box vbox [:& (mf/provider use/include-metadata-ctx) {:value include-metadata?}
:version "1.1" [:svg {:view-box vbox
:xmlnsXlink "http://www.w3.org/1999/xlink" :version "1.1"
:xmlns "http://www.w3.org/2000/svg" :xmlns "http://www.w3.org/2000/svg"
:xmlns:penpot "https://penpot.app/xmlns" :xmlnsXlink "http://www.w3.org/1999/xlink"
:style {:width "100%" :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
:height "100%" :style {:width "100%"
:background background-color}} :height "100%"
:background background-color}}
[:& use/export-page {:options (:options data)}] [:& use/export-page {:options (:options data)}]
[:& ff/fontfaces-style {:shapes root-children}] [:& ff/fontfaces-style {:shapes root-children}]
(for [item shapes] (for [item shapes]
(let [frame? (= (:type item) :frame)] (let [frame? (= (:type item) :frame)]
(cond (cond
(and frame? thumbnails? (some? (:thumbnail item))) (and frame? thumbnails? (some? (:thumbnail item)))
[:image {:xlinkHref (:thumbnail item) [:image {:xlinkHref (:thumbnail item)
:x (:x item) :x (:x item)
:y (:y item) :y (:y item)
:width (:width item) :width (:width item)
:height (:height item) :height (:height item)
;; DEBUG ;; DEBUG
;; :style {:filter "sepia(1)"} ;; :style {:filter "sepia(1)"}
}] }]
frame? frame?
[:& frame-wrapper {:shape item [:& frame-wrapper {:shape item
:key (:id item)}] :key (:id item)}]
:else :else
[:& shape-wrapper {:shape item [:& shape-wrapper {:shape item
:key (:id item)}])))]])) :key (:id item)}])))]]]))
(mf/defc frame-svg (mf/defc frame-svg
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
@ -197,6 +200,8 @@
frame-id (:id frame) frame-id (:id frame)
include-metadata? (mf/use-ctx use/include-metadata-ctx)
modifier-ids (concat [frame-id] (cp/get-children frame-id objects)) modifier-ids (concat [frame-id] (cp/get-children frame-id objects))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
objects (reduce update-fn objects modifier-ids) objects (reduce update-fn objects modifier-ids)
@ -214,9 +219,9 @@
:width width :width width
:height height :height height
:version "1.1" :version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg" :xmlns "http://www.w3.org/2000/svg"
:xmlns:penpot "https://penpot.app/xmlns"} :xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")}
[:& wrapper {:shape frame :view-box vbox}]])) [:& wrapper {:shape frame :view-box vbox}]]))
(mf/defc component-svg (mf/defc component-svg
@ -229,6 +234,8 @@
group-id (:id group) group-id (:id group)
include-metadata? (mf/use-ctx use/include-metadata-ctx)
modifier-ids (concat [group-id] (cp/get-children group-id objects)) modifier-ids (concat [group-id] (cp/get-children group-id objects))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
objects (reduce update-fn objects modifier-ids) objects (reduce update-fn objects modifier-ids)
@ -246,10 +253,11 @@
:width width :width width
:height height :height height
:version "1.1" :version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg" :xmlns "http://www.w3.org/2000/svg"
:xmlns:penpot "https://penpot.app/xmlns"} :xmlnsXlink "http://www.w3.org/1999/xlink"
[:& wrapper {:shape group :view-box vbox}]])) :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")}
[:> shape-container {:shape group}
[:& wrapper {:shape group :view-box vbox}]]]))
(mf/defc component-symbol (mf/defc component-symbol
[{:keys [id data] :as props}] [{:keys [id data] :as props}]
@ -287,20 +295,21 @@
(let [data (obj/get props "data") (let [data (obj/get props "data")
children (obj/get props "children") children (obj/get props "children")
embed? (obj/get props "embed?")] embed? (obj/get props "embed?")
include-metadata? (obj/get props "include-metadata?")]
[:& (mf/provider embed/context) {:value embed?} [:& (mf/provider embed/context) {:value embed?}
[:svg {:version "1.1" [:& (mf/provider use/include-metadata-ctx) {:value include-metadata?}
:xmlns "http://www.w3.org/2000/svg" [:svg {:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink" :xmlns "http://www.w3.org/2000/svg"
:xmlns:penpot "https://penpot.app/xmlns" :xmlnsXlink "http://www.w3.org/1999/xlink"
:style {:width "100vw" :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
:height "100vh" :style {:width "100vw"
:display (when-not (some? children) "none")}} :height "100vh"
:display (when-not (some? children) "none")}}
[:defs
(for [[component-id component-data] (:components data)]
[:& component-symbol {:id component-id
:key (str component-id)
:data component-data}])]
[:defs children]]]))
(for [[component-id component-data] (:components data)]
[:& component-symbol {:id component-id
:key (str component-id)
:data component-data}])]
children]]))

View file

@ -38,6 +38,9 @@
(def threads-ref (def threads-ref
(l/derived :comment-threads st/state)) (l/derived :comment-threads st/state))
(def share-links
(l/derived :share-links st/state))
;; ---- Dashboard refs ;; ---- Dashboard refs
(def dashboard-local (def dashboard-local
@ -110,6 +113,7 @@
:edit-path :edit-path
:tooltip :tooltip
:panning :panning
:zooming
:picking-color? :picking-color?
:transform :transform
:hover :hover
@ -286,8 +290,17 @@
;; ---- Viewer refs ;; ---- Viewer refs
(def viewer-file
(l/derived :viewer-file st/state))
(def viewer-project
(l/derived :viewer-file st/state))
(def viewer-data (def viewer-data
(l/derived :viewer-data st/state)) (l/derived :viewer st/state))
(def viewer-state
(l/derived :viewer st/state))
(def viewer-local (def viewer-local
(l/derived :viewer-local st/state)) (l/derived :viewer-local st/state))

View file

@ -63,7 +63,7 @@
(->> (rx/of data) (->> (rx/of data)
(rx/map (rx/map
(fn [data] (fn [data]
(let [elem (mf/element exports/page-svg #js {:data data :embed? true})] (let [elem (mf/element exports/page-svg #js {:data data :embed? true :include-metadata? true})]
(rds/renderToStaticMarkup elem))))))) (rds/renderToStaticMarkup elem)))))))
(defn render-components (defn render-components
@ -82,5 +82,5 @@
(->> (rx/of data) (->> (rx/of data)
(rx/map (rx/map
(fn [data] (fn [data]
(let [elem (mf/element exports/components-sprite-svg #js {:data data :embed? true})] (let [elem (mf/element exports/components-sprite-svg #js {:data data :embed? true :include-metadata? true})]
(rds/renderToStaticMarkup elem)))))))) (rds/renderToStaticMarkup elem))))))))

View file

@ -107,6 +107,14 @@
:response-type :blob}) :response-type :blob})
(rx/mapcat handle-response))) (rx/mapcat handle-response)))
(defmethod query :export-frames
[_ params]
(->> (http/send! {:method :post
:uri (u/join base-uri "export-frames")
:body (http/transit-data params)
:response-type :blob})
(rx/mapcat handle-response)))
(derive :upload-file-media-object ::multipart-upload) (derive :upload-file-media-object ::multipart-upload)
(derive :update-profile-photo ::multipart-upload) (derive :update-profile-photo ::multipart-upload)
(derive :update-team-photo ::multipart-upload) (derive :update-team-photo ::multipart-upload)

View file

@ -14,7 +14,7 @@
;; --- User Events ;; --- User Events
(defrecord KeyboardEvent [type key shift ctrl alt meta]) (defrecord KeyboardEvent [type key shift ctrl alt meta editing])
(defn keyboard-event? (defn keyboard-event?
[v] [v]
@ -137,3 +137,14 @@
(rx/dedupe))] (rx/dedupe))]
(rx/subscribe-with ob sub) (rx/subscribe-with ob sub)
sub)) sub))
(defonce keyboard-space
(let [sub (rx/behavior-subject nil)
ob (->> st/stream
(rx/filter keyboard-event?)
(rx/filter kbd/space?)
(rx/filter (comp not kbd/editing?))
(rx/map #(= :down (:type %)))
(rx/dedupe))]
(rx/subscribe-with ob sub)
sub))

View file

@ -20,14 +20,13 @@
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.cursors :as c] [app.main.ui.cursors :as c]
[app.main.ui.dashboard :refer [dashboard]] [app.main.ui.dashboard :refer [dashboard]]
[app.main.ui.handoff :refer [handoff]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.main.ui.onboarding] [app.main.ui.onboarding]
[app.main.ui.render :as render] [app.main.ui.render :as render]
[app.main.ui.settings :as settings] [app.main.ui.settings :as settings]
[app.main.ui.static :as static] [app.main.ui.static :as static]
[app.main.ui.viewer :refer [viewer-page]] [app.main.ui.viewer :as viewer]
[app.main.ui.workspace :as workspace] [app.main.ui.workspace :as workspace]
[app.util.timers :as ts] [app.util.timers :as ts]
[cljs.pprint :refer [pprint]] [cljs.pprint :refer [pprint]]
@ -41,25 +40,26 @@
(s/def ::page-id ::us/uuid) (s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
(s/def ::viewer-path-params
(s/keys :req-un [::file-id ::page-id]))
(s/def ::section ::us/keyword) (s/def ::section ::us/keyword)
(s/def ::index ::us/integer) (s/def ::index ::us/integer)
(s/def ::token (s/nilable ::us/string)) (s/def ::token (s/nilable ::us/not-empty-string))
(s/def ::share-id ::us/uuid)
(s/def ::viewer-path-params
(s/keys :req-un [::file-id]))
(s/def ::viewer-query-params (s/def ::viewer-query-params
(s/keys :req-un [::index] (s/keys :req-un [::index]
:opt-un [::token ::section])) :opt-un [::share-id ::section ::page-id]))
(def routes (def routes
[["/auth" [["/auth"
["/login" :auth-login] ["/login" :auth-login]
(when cf/registration-enabled (when (contains? @cf/flags :registration)
["/register" :auth-register]) ["/register" :auth-register])
(when cf/registration-enabled (when (contains? @cf/flags :registration)
["/register/validate" :auth-register-validate]) ["/register/validate" :auth-register-validate])
(when cf/registration-enabled (when (contains? @cf/flags :registration)
["/register/success" :auth-register-success]) ["/register/success" :auth-register-success])
["/recovery/request" :auth-recovery-request] ["/recovery/request" :auth-recovery-request]
["/recovery" :auth-recovery] ["/recovery" :auth-recovery]
@ -71,7 +71,7 @@
["/feedback" :settings-feedback] ["/feedback" :settings-feedback]
["/options" :settings-options]] ["/options" :settings-options]]
["/view/:file-id/:page-id" ["/view/:file-id"
{:name :viewer {:name :viewer
:conform :conform
{:path-params ::viewer-path-params {:path-params ::viewer-path-params
@ -143,26 +143,21 @@
:dashboard-team-settings) :dashboard-team-settings)
[:* [:*
#_[:div.modal-wrapper #_[:div.modal-wrapper
[:& app.main.ui.onboarding/release-notes-modal {:version "1.7"}]] [:& app.main.ui.onboarding/release-notes-modal {:version "1.8"}]]
[:& dashboard {:route route}]] [:& dashboard {:route route}]]
:viewer :viewer
(let [index (get-in route [:query-params :index]) (let [{:keys [query-params path-params]} route
token (get-in route [:query-params :token]) {:keys [index share-id section page-id] :or {section :interactions}} query-params
section (get-in route [:query-params :section] :interactions) {:keys [file-id]} path-params]
file-id (get-in route [:path-params :file-id])
page-id (get-in route [:path-params :page-id])]
[:& fs/fullscreen-wrapper {} [:& fs/fullscreen-wrapper {}
(if (= section :handoff) (if (:token query-params)
[:& handoff {:page-id page-id [:& viewer/breaking-change-notice]
:file-id file-id [:& viewer/viewer-page {:page-id page-id
:index index :file-id file-id
:token token}] :section section
[:& viewer-page {:page-id page-id :index index
:file-id file-id :share-id share-id}])])
:section section
:index index
:token token}])])
:render-object :render-object
(do (do

View file

@ -7,7 +7,7 @@
(ns app.main.ui.auth.login (ns app.main.ui.auth.login
(:require (:require
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cfg] [app.config :as cf]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.repo :as rp] [app.main.repo :as rp]
@ -23,10 +23,10 @@
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(def show-alt-login-buttons? (def show-alt-login-buttons?
(or cfg/google-client-id (or cf/google-client-id
cfg/gitlab-client-id cf/gitlab-client-id
cfg/github-client-id cf/github-client-id
cfg/oidc-client-id)) cf/oidc-client-id))
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string) (s/def ::password ::us/not-empty-string)
@ -97,7 +97,7 @@
[:div.fields-row [:div.fields-row
[:& fm/input [:& fm/input
{:name :email {:name :email
:type "text" :type "email"
:tab-index "2" :tab-index "2"
:help-icon i/at :help-icon i/at
:label (tr "auth.email")}]] :label (tr "auth.email")}]]
@ -113,7 +113,7 @@
[:& fm/submit-button [:& fm/submit-button
{:label (tr "auth.login-submit")}] {:label (tr "auth.login-submit")}]
(when cfg/login-with-ldap (when (contains? @cf/flags :login-with-ldap)
[:& fm/submit-button [:& fm/submit-button
{:label (tr "auth.login-with-ldap-submit") {:label (tr "auth.login-with-ldap-submit")
:on-click on-submit-ldap}])]]])) :on-click on-submit-ldap}])]]]))
@ -121,26 +121,26 @@
(mf/defc login-buttons (mf/defc login-buttons
[{:keys [params] :as props}] [{:keys [params] :as props}]
[:div.auth-buttons [:div.auth-buttons
(when cfg/google-client-id (when cf/google-client-id
[:a.btn-ocean.btn-large.btn-google-auth [:a.btn-ocean.btn-large.btn-google-auth
{:on-click #(login-with-oauth % :google params)} {:on-click #(login-with-oauth % :google params)}
(tr "auth.login-with-google-submit")]) (tr "auth.login-with-google-submit")])
(when cfg/gitlab-client-id (when cf/gitlab-client-id
[:a.btn-ocean.btn-large.btn-gitlab-auth [:a.btn-ocean.btn-large.btn-gitlab-auth
{:on-click #(login-with-oauth % :gitlab params)} {:on-click #(login-with-oauth % :gitlab params)}
[:img.logo [:img.logo
{:src "/images/icons/brand-gitlab.svg"}] {:src "/images/icons/brand-gitlab.svg"}]
(tr "auth.login-with-gitlab-submit")]) (tr "auth.login-with-gitlab-submit")])
(when cfg/github-client-id (when cf/github-client-id
[:a.btn-ocean.btn-large.btn-github-auth [:a.btn-ocean.btn-large.btn-github-auth
{:on-click #(login-with-oauth % :github params)} {:on-click #(login-with-oauth % :github params)}
[:img.logo [:img.logo
{:src "/images/icons/brand-github.svg"}] {:src "/images/icons/brand-github.svg"}]
(tr "auth.login-with-github-submit")]) (tr "auth.login-with-github-submit")])
(when cfg/oidc-client-id (when cf/oidc-client-id
[:a.btn-ocean.btn-large.btn-github-auth [:a.btn-ocean.btn-large.btn-github-auth
{:on-click #(login-with-oauth % :oidc params)} {:on-click #(login-with-oauth % :oidc params)}
(tr "auth.login-with-oidc-submit")])]) (tr "auth.login-with-oidc-submit")])])
@ -166,14 +166,13 @@
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))} [:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))}
(tr "auth.forgot-password")]] (tr "auth.forgot-password")]]
(when cfg/registration-enabled (when (contains? @cf/flags :registration)
[:div.link-entry [:div.link-entry
[:span (tr "auth.register") " "] [:span (tr "auth.register") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-register {} params))} [:a {:on-click #(st/emit! (rt/nav :auth-register {} params))}
(tr "auth.register-submit")]])] (tr "auth.register-submit")]])]
(when (contains? @cf/flags :demo-users)
(when cfg/allow-demo-users
[:div.links.demo [:div.links.demo
[:div.link-entry [:div.link-entry
[:span (tr "auth.create-demo-profile") " "] [:span (tr "auth.create-demo-profile") " "]

View file

@ -116,7 +116,7 @@
[:h1 (tr "auth.register-title")] [:h1 (tr "auth.register-title")]
[:div.subtitle (tr "auth.register-subtitle")] [:div.subtitle (tr "auth.register-subtitle")]
(when cf/demo-warning (when (contains? @cf/flags :demo-warning)
[:& demo-warning]) [:& demo-warning])
[:& register-form {:params params}] [:& register-form {:params params}]
@ -135,7 +135,7 @@
:tab-index "4"} :tab-index "4"}
(tr "auth.login-here")]] (tr "auth.login-here")]]
(when cf/allow-demo-users (when (contains? @cf/flags :demo-users)
[:div.link-entry [:div.link-entry
[:span (tr "auth.create-demo-profile") " "] [:span (tr "auth.create-demo-profile") " "]
[:a {:on-click #(st/emit! (du/create-demo-profile)) [:a {:on-click #(st/emit! (du/create-demo-profile))
@ -216,7 +216,7 @@
:label (tr "auth.terms-privacy-agreement") :label (tr "auth.terms-privacy-agreement")
:type "checkbox"}]] :type "checkbox"}]]
(when (contains? @cf/flags :show-newsletter-check-on-register-validation) (when (contains? @cf/flags :newsletter-registration-check)
[:div.fields-row [:div.fields-row
[:& fm/input {:name :accept-newsletter-subscription [:& fm/input {:name :accept-newsletter-subscription
:class "check-primary" :class "check-primary"

View file

@ -12,12 +12,14 @@
[beicon.core :as rx] [beicon.core :as rx]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(mf/defc copy-button [{:keys [data]}] (mf/defc copy-button [{:keys [data on-copied]}]
(let [just-copied (mf/use-state false)] (let [just-copied (mf/use-state false)]
(mf/use-effect (mf/use-effect
(mf/deps @just-copied) (mf/deps @just-copied)
(fn [] (fn []
(when @just-copied (when @just-copied
(when (fn? on-copied)
(on-copied))
(let [sub (timers/schedule 1000 #(reset! just-copied false))] (let [sub (timers/schedule 1000 #(reset! just-copied false))]
;; On unmount we dispose the timer ;; On unmount we dispose the timer
#(rx/-dispose sub))))) #(rx/-dispose sub)))))

Some files were not shown because too many files have changed in this diff Show more