Merge remote-tracking branch 'origin/staging' into main
|
@ -23,6 +23,9 @@
|
|||
{:unsorted-required-namespaces
|
||||
{:level :warning}
|
||||
|
||||
:potok/reify-type
|
||||
{:level :error}
|
||||
|
||||
:unresolved-namespace
|
||||
{:level :warning
|
||||
:exclude [data_readers]}
|
||||
|
|
|
@ -10,15 +10,34 @@
|
|||
sname])]
|
||||
{:node result}))
|
||||
|
||||
(def registry (atom {}))
|
||||
|
||||
(defn potok-reify
|
||||
[{:keys [:node]}]
|
||||
[{:keys [:node :filename] :as params}]
|
||||
(let [[rnode rtype & other] (:children node)
|
||||
result (api/list-node
|
||||
rsym (symbol (str "event-type-" (name (:k rtype))))
|
||||
reg (get @registry filename #{})]
|
||||
(when-not (:namespaced? rtype)
|
||||
(let [{:keys [:row :col]} (meta rtype)]
|
||||
(api/reg-finding! {:message "ptk/reify type should be namespaced"
|
||||
: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 (gensym (name (:k rtype))))
|
||||
(api/token-node rsym)
|
||||
(api/vector-node [])]
|
||||
other))]
|
||||
{:node result}))
|
||||
{:node result})))
|
||||
|
||||
(defn clojure-specify
|
||||
[{:keys [:node]}]
|
||||
|
|
57
CHANGES.md
|
@ -1,14 +1,62 @@
|
|||
# CHANGELOG #
|
||||
# CHANGELOG
|
||||
|
||||
## :rocket: Next
|
||||
|
||||
### :boom: Breaking changes
|
||||
### :sparkles: New features
|
||||
|
||||
### :bug: Bugs fixed
|
||||
### :arrow_up: Deps updates
|
||||
### :boom: Breaking changes
|
||||
### :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
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
@ -43,7 +91,6 @@
|
|||
|
||||
- Update frontend build tooling.
|
||||
|
||||
### :boom: Breaking changes
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- 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)
|
||||
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
### :boom: Breaking changes
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
## 1.6.5-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text>
|
||||
<mj-text>
|
||||
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!
|
||||
</mj-text>
|
||||
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">
|
||||
|
|
|
@ -173,7 +173,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Hello {{name}}!
|
||||
|
||||
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}}
|
||||
|
||||
|
|
|
@ -231,9 +231,9 @@
|
|||
(defn get-by-params
|
||||
([ds table params]
|
||||
(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))]
|
||||
(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
|
||||
:table table
|
||||
:hint "database object not found"))
|
||||
|
@ -267,13 +267,28 @@
|
|||
(instance? PGpoint v))
|
||||
|
||||
(defn pgarray?
|
||||
[v]
|
||||
(instance? PgArray v))
|
||||
([v] (instance? PgArray v))
|
||||
([v type]
|
||||
(and (instance? PgArray v)
|
||||
(= type (.getBaseTypeName ^PgArray v)))))
|
||||
|
||||
(defn pgarray-of-uuid?
|
||||
[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
|
||||
[p]
|
||||
(PGpoint. (:x p) (:y p)))
|
||||
|
@ -285,7 +300,6 @@
|
|||
(.createArrayOf conn ^String type (into-array Object objects))
|
||||
(.createArrayOf conn ^String type objects))))
|
||||
|
||||
|
||||
(defn decode-pgpoint
|
||||
[^PGpoint v]
|
||||
(gpt/point (.-x v) (.-y v)))
|
||||
|
@ -369,15 +383,6 @@
|
|||
(.setType "jsonb")
|
||||
(.setValue (json/encode-str data))))
|
||||
|
||||
(defn pgarray->set
|
||||
[v]
|
||||
(set (.getArray ^PgArray v)))
|
||||
|
||||
(defn pgarray->vector
|
||||
[v]
|
||||
(vec (.getArray ^PgArray v)))
|
||||
|
||||
|
||||
;; --- Locks
|
||||
|
||||
(defn- xact-check-param
|
||||
|
|
|
@ -114,9 +114,14 @@
|
|||
(s/def ::storage map?)
|
||||
(s/def ::assets map?)
|
||||
(s/def ::feedback fn?)
|
||||
(s/def ::error-report-handler fn?)
|
||||
(s/def ::audit-http-handler fn?)
|
||||
|
||||
(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
|
||||
[_ {:keys [session rpc oauth metrics assets feedback] :as cfg}]
|
||||
|
@ -136,9 +141,7 @@
|
|||
["/webhooks"
|
||||
["/sns" {:post (:sns-webhook cfg)}]]
|
||||
|
||||
["/api" {:middleware [
|
||||
;; Temporary disabled
|
||||
#_[middleware/etag]
|
||||
["/api" {:middleware [[middleware/etag]
|
||||
[middleware/format-response-body]
|
||||
[middleware/params]
|
||||
[middleware/multipart-params]
|
||||
|
@ -149,10 +152,12 @@
|
|||
|
||||
["/feedback" {:middleware [(:middleware session)]
|
||||
:post feedback}]
|
||||
|
||||
["/auth/oauth/:provider" {:post (:handler oauth)}]
|
||||
["/auth/oauth/:provider/callback" {:get (:callback-handler oauth)}]
|
||||
|
||||
["/audit/events" {:middleware [(:middleware session)]
|
||||
:post (:audit-http-handler cfg)}]
|
||||
|
||||
["/rpc" {:middleware [(:middleware session)]}
|
||||
["/query/:type" {:get (:query-handler rpc)
|
||||
:post (:query-handler rpc)}]
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.hash :as bh]
|
||||
[clojure.java.io :as io]
|
||||
[ring.core.protocols :as rp]
|
||||
[ring.middleware.cookies :refer [wrap-cookies]]
|
||||
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
|
||||
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
|
||||
|
@ -74,33 +73,15 @@
|
|||
{:name ::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
|
||||
[response _request]
|
||||
(let [body (:body response)
|
||||
opts {:type :json-verbose}]
|
||||
opts {:type :json}]
|
||||
(cond
|
||||
(coll? body)
|
||||
(-> response
|
||||
(update :headers assoc "content-type" "application/transit+json")
|
||||
(assoc :body (transit-streamable-body 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))))
|
||||
(assoc :body (t/encode body opts)))
|
||||
|
||||
(nil? body)
|
||||
(assoc response :status 204 :body "")
|
||||
|
|
|
@ -23,7 +23,8 @@
|
|||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[lambdaisland.uri :as u]))
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(defn parse-client-ip
|
||||
[{:keys [headers] :as request}]
|
||||
|
@ -67,6 +68,65 @@
|
|||
|
||||
(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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -103,7 +163,9 @@
|
|||
(recur)))
|
||||
|
||||
(fn [& {:keys [cmd] :as params}]
|
||||
(let [params (dissoc params :cmd)]
|
||||
(let [params (-> params
|
||||
(dissoc :cmd)
|
||||
(assoc :tracked-at (dt/now)))]
|
||||
(case cmd
|
||||
:stop (a/close! input)
|
||||
:submit (when-not (a/offer! input params)
|
||||
|
@ -117,13 +179,14 @@
|
|||
(:name event)
|
||||
(:type event)
|
||||
(:profile-id event)
|
||||
(:tracked-at event)
|
||||
(some-> (:ip-addr event) db/inet)
|
||||
(db/tjson (:props event))])]
|
||||
|
||||
(db/tjson (:props event))
|
||||
"backend"])]
|
||||
(aa/with-thread executor
|
||||
(db/with-atomic [conn pool]
|
||||
(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))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -144,16 +207,22 @@
|
|||
|
||||
(defmethod ig/init-key ::archive-task
|
||||
[_ {:keys [uri enabled] :as cfg}]
|
||||
(fn [_]
|
||||
(fn [props]
|
||||
;; NOTE: this let allows overwrite default configured values from
|
||||
;; the repl, when manually invoking the task.
|
||||
(let [enabled (or enabled (:enabled props false))
|
||||
uri (or uri (:uri props))
|
||||
cfg (assoc cfg :uri uri)]
|
||||
(when (and enabled (not uri))
|
||||
(ex/raise :type :internal
|
||||
:code :task-not-configured
|
||||
:hint "archive task not configured, missing uri"))
|
||||
(when enabled
|
||||
(loop []
|
||||
(let [res (archive-events cfg)]
|
||||
(when (= res :continue)
|
||||
(aa/thread-sleep 200)
|
||||
(recur))))))
|
||||
(recur))))))))
|
||||
|
||||
(def sql:retrieve-batch-of-audit-log
|
||||
"select * from audit_log
|
||||
|
@ -164,22 +233,27 @@
|
|||
|
||||
(defn archive-events
|
||||
[{: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
|
||||
(db/pgobject? props)
|
||||
(assoc :props (db/decode-transit-pgobject props))
|
||||
|
||||
(db/pgobject? context)
|
||||
(assoc :context (db/decode-transit-pgobject context))
|
||||
|
||||
(db/pgobject? ip-addr "inet")
|
||||
(assoc :ip-addr (db/decode-inet ip-addr))))
|
||||
|
||||
(row->event [{:keys [name type created-at profile-id props ip-addr]}]
|
||||
(cond-> {:type type
|
||||
:name name
|
||||
:timestamp created-at
|
||||
:profile-id profile-id
|
||||
:props props}
|
||||
(some? ip-addr)
|
||||
(update :context assoc :source-ip ip-addr)))
|
||||
(row->event [row]
|
||||
(select-keys row [:type
|
||||
:name
|
||||
:source
|
||||
:created-at
|
||||
:tracked-at
|
||||
:profile-id
|
||||
:ip-addr
|
||||
:props
|
||||
:context]))
|
||||
|
||||
(send [events]
|
||||
(let [token (tokens :generate {:iss "authentication"
|
||||
|
|
|
@ -28,11 +28,24 @@
|
|||
{:name "actions_profile_register_count"
|
||||
:help "A global counter of user registrations."
|
||||
:type :counter}
|
||||
|
||||
:profile-activation
|
||||
{:name "actions_profile_activation_count"
|
||||
: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}}}
|
||||
|
||||
|
||||
|
||||
:app.migrations/all
|
||||
{:main (ig/ref :app.migrations/migrations)}
|
||||
|
||||
|
@ -95,6 +108,7 @@
|
|||
:storage (ig/ref :app.storage/storage)
|
||||
:sns-webhook (ig/ref :app.http.awsns/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)}
|
||||
|
||||
:app.http.assets/handlers
|
||||
|
@ -289,6 +303,11 @@
|
|||
:app.loggers.zmq/receiver
|
||||
{: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
|
||||
{:enabled (cf/get :audit-enabled false)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
|
|
|
@ -92,18 +92,14 @@
|
|||
_ (when (seq labels)
|
||||
(.labelNames instance (into-array String labels)))
|
||||
instance (.register instance registry)]
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] instance)
|
||||
|
||||
clojure.lang.IFn
|
||||
(invoke [_ cmd]
|
||||
(.inc ^Counter instance))
|
||||
|
||||
(invoke [_ cmd labels]
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [by labels] :or {by 1}}]
|
||||
(if labels
|
||||
(.. ^Counter instance
|
||||
(labels (into-array String labels))
|
||||
(inc))))))
|
||||
(inc by))
|
||||
(.inc ^Counter instance by)))}))
|
||||
|
||||
(defn make-gauge
|
||||
[{:keys [name help registry reg labels] :as props}]
|
||||
|
@ -115,21 +111,16 @@
|
|||
(.labelNames instance (into-array String labels)))
|
||||
instance (.register instance registry)]
|
||||
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] instance)
|
||||
|
||||
clojure.lang.IFn
|
||||
(invoke [_ cmd]
|
||||
(case cmd
|
||||
:inc (.inc ^Gauge instance)
|
||||
:dec (.dec ^Gauge instance)))
|
||||
|
||||
(invoke [_ cmd labels]
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [cmd by labels] :or {by 1}}]
|
||||
(if labels
|
||||
(let [labels (into-array String [labels])]
|
||||
(case cmd
|
||||
:inc (.. ^Gauge instance (labels labels) (inc))
|
||||
:dec (.. ^Gauge instance (labels labels) (dec))))))))
|
||||
:inc (.. ^Gauge instance (labels labels) (inc by))
|
||||
:dec (.. ^Gauge instance (labels labels) (dec by))))
|
||||
(case cmd
|
||||
:inc (.inc ^Gauge instance by)
|
||||
:dec (.dec ^Gauge instance by))))}))
|
||||
|
||||
(def default-quantiles
|
||||
[[0.75 0.02]
|
||||
|
@ -150,18 +141,14 @@
|
|||
_ (when (seq labels)
|
||||
(.labelNames instance (into-array String labels)))
|
||||
instance (.register instance registry)]
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] instance)
|
||||
|
||||
clojure.lang.IFn
|
||||
(invoke [_ cmd val]
|
||||
(.observe ^Summary instance val))
|
||||
|
||||
(invoke [_ cmd val labels]
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [val labels]}]
|
||||
(if labels
|
||||
(.. ^Summary instance
|
||||
(labels (into-array String labels))
|
||||
(observe val))))))
|
||||
(observe val))
|
||||
(.observe ^Summary instance val)))}))
|
||||
|
||||
(def default-histogram-buckets
|
||||
[1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
|
||||
|
@ -177,18 +164,14 @@
|
|||
_ (when (seq labels)
|
||||
(.labelNames instance (into-array String labels)))
|
||||
instance (.register instance registry)]
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] instance)
|
||||
|
||||
clojure.lang.IFn
|
||||
(invoke [_ cmd val]
|
||||
(.observe ^Histogram instance val))
|
||||
|
||||
(invoke [_ cmd val labels]
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [val labels]}]
|
||||
(if labels
|
||||
(.. ^Histogram instance
|
||||
(labels (into-array String labels))
|
||||
(observe val))))))
|
||||
(observe val))
|
||||
(.observe ^Histogram instance val)))}))
|
||||
|
||||
(defn create
|
||||
[{:keys [type] :as props}]
|
||||
|
@ -205,19 +188,19 @@
|
|||
(with-meta
|
||||
(fn
|
||||
([a]
|
||||
(mobj :inc)
|
||||
((::fn mobj) nil)
|
||||
(origf a))
|
||||
([a b]
|
||||
(mobj :inc)
|
||||
((::fn mobj) nil)
|
||||
(origf a b))
|
||||
([a b c]
|
||||
(mobj :inc)
|
||||
((::fn mobj) nil)
|
||||
(origf a b c))
|
||||
([a b c d]
|
||||
(mobj :inc)
|
||||
((::fn mobj) nil)
|
||||
(origf a b c d))
|
||||
([a b c d & more]
|
||||
(mobj :inc)
|
||||
((::fn mobj) nil)
|
||||
(apply origf a b c d more)))
|
||||
(assoc mdata ::original origf))))
|
||||
([rootf mobj labels]
|
||||
|
@ -226,13 +209,13 @@
|
|||
(with-meta
|
||||
(fn
|
||||
([a]
|
||||
(mobj :inc labels)
|
||||
((::fn mobj) {:labels labels})
|
||||
(origf a))
|
||||
([a b]
|
||||
(mobj :inc labels)
|
||||
((::fn mobj) {:labels labels})
|
||||
(origf a b))
|
||||
([a b & more]
|
||||
(mobj :inc labels)
|
||||
((::fn mobj) {:labels labels})
|
||||
(apply origf a b more)))
|
||||
(assoc mdata ::original origf)))))
|
||||
|
||||
|
@ -245,15 +228,15 @@
|
|||
([a]
|
||||
(with-measure
|
||||
:expr (origf a)
|
||||
:cb #(mobj :observe %)))
|
||||
:cb #((::fn mobj) {:val %})))
|
||||
([a b]
|
||||
(with-measure
|
||||
:expr (origf a b)
|
||||
:cb #(mobj :observe %)))
|
||||
:cb #((::fn mobj) {:val %})))
|
||||
([a b & more]
|
||||
(with-measure
|
||||
:expr (apply origf a b more)
|
||||
:cb #(mobj :observe %))))
|
||||
:cb #((::fn mobj) {:val %}))))
|
||||
(assoc mdata ::original origf))))
|
||||
|
||||
([rootf mobj labels]
|
||||
|
@ -264,26 +247,26 @@
|
|||
([a]
|
||||
(with-measure
|
||||
:expr (origf a)
|
||||
:cb #(mobj :observe % labels)))
|
||||
:cb #((::fn mobj) {:val % :labels labels})))
|
||||
([a b]
|
||||
(with-measure
|
||||
:expr (origf a b)
|
||||
:cb #(mobj :observe % labels)))
|
||||
:cb #((::fn mobj) {:val % :labels labels})))
|
||||
([a b & more]
|
||||
(with-measure
|
||||
:expr (apply origf a b more)
|
||||
:cb #(mobj :observe % labels))))
|
||||
:cb #((::fn mobj) {:val % :labels labels}))))
|
||||
(assoc mdata ::original origf)))))
|
||||
|
||||
(defn instrument-vars!
|
||||
[vars {:keys [wrap] :as props}]
|
||||
(let [obj (create props)]
|
||||
(cond
|
||||
(instance? Counter @obj)
|
||||
(instance? Counter (::instance obj))
|
||||
(doseq [var vars]
|
||||
(alter-var-root var (or wrap wrap-counter) obj))
|
||||
|
||||
(instance? Summary @obj)
|
||||
(instance? Summary (::instance obj))
|
||||
(doseq [var vars]
|
||||
(alter-var-root var (or wrap wrap-summary) obj))
|
||||
|
||||
|
@ -294,13 +277,13 @@
|
|||
[f {:keys [wrap] :as props}]
|
||||
(let [obj (create props)]
|
||||
(cond
|
||||
(instance? Counter @obj)
|
||||
(instance? Counter (::instance obj))
|
||||
((or wrap wrap-counter) f obj)
|
||||
|
||||
(instance? Summary @obj)
|
||||
(instance? Summary (::instance obj))
|
||||
((or wrap wrap-summary) f obj)
|
||||
|
||||
(instance? Histogram @obj)
|
||||
(instance? Histogram (::instance obj))
|
||||
((or wrap wrap-summary) f obj)
|
||||
|
||||
:else
|
||||
|
|
|
@ -193,6 +193,15 @@
|
|||
|
||||
{:name "0061-mod-file-table"
|
||||
: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")}
|
||||
])
|
||||
|
||||
|
||||
|
|
12
backend/src/app/migrations/sql/0063-add-share-link-table.sql
Normal 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);
|
13
backend/src/app/migrations/sql/0064-mod-audit-log-table.sql
Normal 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;
|
|
@ -117,14 +117,14 @@
|
|||
profile-id (or (:profile-id params')
|
||||
(:profile-id result)
|
||||
(::audit/profile-id resultm))
|
||||
props (d/merge params (::audit/props resultm))]
|
||||
props (d/merge params' (::audit/props resultm))]
|
||||
(audit :cmd :submit
|
||||
:type (::type cfg)
|
||||
:name (or (::audit/name resultm)
|
||||
(::sv/name mdata))
|
||||
:profile-id profile-id
|
||||
:ip-addr (audit/parse-client-ip request)
|
||||
:props (audit/profile->props props))))
|
||||
:props props)))
|
||||
|
||||
result))))
|
||||
|
||||
|
@ -175,6 +175,7 @@
|
|||
'app.rpc.mutations.management
|
||||
'app.rpc.mutations.ldap
|
||||
'app.rpc.mutations.fonts
|
||||
'app.rpc.mutations.share-link
|
||||
'app.rpc.mutations.verify-token)
|
||||
(map (partial process-method cfg))
|
||||
(into {}))))
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.files :as files]
|
||||
[app.rpc.queries.projects :as proj]
|
||||
|
@ -291,7 +292,7 @@
|
|||
(simpl/del-object backend 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)
|
||||
(:revn file))
|
||||
|
||||
|
@ -301,14 +302,22 @@
|
|||
:context {:incoming-revn (:revn params)
|
||||
: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)
|
||||
changes)
|
||||
|
||||
;; Trace the number of changes processed
|
||||
_ ((::mtx/fn mtx1) {:by (count changes)})
|
||||
|
||||
ts (dt/now)
|
||||
file (-> (files/retrieve-data cfg file)
|
||||
(update :revn inc)
|
||||
(update :data (fn [data]
|
||||
;; Trace the length of bytes of processed data
|
||||
((::mtx/fn mtx2) {:by (alength data)})
|
||||
(-> data
|
||||
(blob/decode)
|
||||
(assoc :id (:id file))
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
[app.http.oauth :refer [extract-props]]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.media :as media]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc.mutations.projects :as projects]
|
||||
[app.rpc.mutations.teams :as teams]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
|
@ -150,7 +151,8 @@
|
|||
transaction is completed."
|
||||
[metrics]
|
||||
(fn []
|
||||
((get-in metrics [:definitions :profile-register]) :inc)))
|
||||
(let [mobj (get-in metrics [:definitions :profile-register])]
|
||||
((::mtx/fn mobj) {:by 1}))))
|
||||
|
||||
(defn register-profile
|
||||
[{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]
|
||||
|
|
67
backend/src/app/rpc/mutations/share_link.clj
Normal 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)))
|
|
@ -9,6 +9,7 @@
|
|||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc.mutations.teams :as teams]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.util.services :as sv]
|
||||
|
@ -42,7 +43,8 @@
|
|||
transaction is completed."
|
||||
[metrics]
|
||||
(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
|
||||
[{:keys [conn session metrics] :as cfg} _ {:keys [profile-id] :as claims}]
|
||||
|
|
|
@ -37,6 +37,41 @@
|
|||
:is-admin 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
|
||||
"A simple factory for edition permission check functions."
|
||||
[qfn]
|
||||
|
|
|
@ -61,16 +61,23 @@
|
|||
|
||||
(defn- retrieve-file-permissions
|
||||
[conn profile-id file-id]
|
||||
(when (and profile-id file-id)
|
||||
(db/exec! conn [sql:file-permissions
|
||||
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!
|
||||
(perms/make-edition-check-fn retrieve-file-permissions))
|
||||
(perms/make-check-fn has-edit-permissions?))
|
||||
|
||||
(def check-read-permissions!
|
||||
(perms/make-read-check-fn retrieve-file-permissions))
|
||||
(perms/make-check-fn has-read-permissions?))
|
||||
|
||||
|
||||
;; --- Query: Files search
|
||||
|
|
|
@ -14,24 +14,98 @@
|
|||
[app.util.services :as sv]
|
||||
[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)
|
||||
|
||||
;; DEPRECATED: should be removed in 1.9.x
|
||||
|
||||
(declare check-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 ::file-id ::us/uuid)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::token ::us/string)
|
||||
|
||||
|
@ -81,6 +155,3 @@
|
|||
[conn file-id page-id]
|
||||
(let [sql "select * from file_share_token where file_id=? and page_id=?"]
|
||||
(db/exec-one! conn [sql file-id page-id])))
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
|
||||
(defmethod handle-deletion :team-font-variant
|
||||
[{: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)]
|
||||
(when (:deleted-at font)
|
||||
(db/delete! conn :team-font-variant {:id id})
|
||||
|
|
|
@ -24,10 +24,10 @@
|
|||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
:project-id proj-id
|
||||
:is-shared false})
|
||||
token (atom nil)]
|
||||
share-id (atom nil)]
|
||||
|
||||
(t/testing "authenticated with page-id"
|
||||
(let [data {::th/type :viewer-bundle
|
||||
(let [data {::th/type :view-only-bundle
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:page-id (get-in file [:data :pages 0])}
|
||||
|
@ -38,26 +38,28 @@
|
|||
(t/is (nil? (:error out)))
|
||||
|
||||
(let [result (:result out)]
|
||||
(t/is (contains? result :token))
|
||||
(t/is (contains? result :page))
|
||||
(t/is (contains? result :share-links))
|
||||
(t/is (contains? result :permissions))
|
||||
(t/is (contains? result :libraries))
|
||||
(t/is (contains? result :file))
|
||||
(t/is (contains? result :project)))))
|
||||
|
||||
(t/testing "generate share token"
|
||||
(let [data {::th/type :create-file-share-token
|
||||
(let [data {::th/type :create-share-link
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:page-id (get-in file [:data :pages 0])}
|
||||
:pages #{(get-in file [:data :pages 0])}
|
||||
:flags #{}}
|
||||
out (th/mutation! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
(t/is (string? (:token result)))
|
||||
(reset! token (:token result)))))
|
||||
(t/is (uuid? (:id result)))
|
||||
(reset! share-id (:id result)))))
|
||||
|
||||
(t/testing "not authenticated with page-id"
|
||||
(let [data {::th/type :viewer-bundle
|
||||
(let [data {::th/type :view-only-bundle
|
||||
:profile-id (:id prof2)
|
||||
:file-id (:id file)
|
||||
:page-id (get-in file [:data :pages 0])}
|
||||
|
@ -70,32 +72,33 @@
|
|||
(t/is (= (:type error-data) :not-found))
|
||||
(t/is (= (:code error-data) :object-not-found)))))
|
||||
|
||||
;; (t/testing "authenticated with token & profile"
|
||||
;; (let [data {::sq/type :viewer-bundle
|
||||
;; :profile-id (:id prof2)
|
||||
;; :token @token
|
||||
;; :file-id (:id file)
|
||||
;; :page-id (get-in file [:data :pages 0])}
|
||||
;; out (th/try-on! (sq/handle data))]
|
||||
(t/testing "authenticated with token & profile"
|
||||
(let [data {::th/type :view-only-bundle
|
||||
:profile-id (:id prof2)
|
||||
:share-id @share-id
|
||||
:file-id (:id file)
|
||||
:page-id (get-in file [:data :pages 0])}
|
||||
out (th/query! data)]
|
||||
|
||||
;; ;; (th/print-result! out)
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
|
||||
;; (let [result (:result out)]
|
||||
;; (t/is (contains? result :page))
|
||||
;; (t/is (contains? result :file))
|
||||
;; (t/is (contains? result :project)))))
|
||||
(let [result (:result out)]
|
||||
(t/is (contains? result :share))
|
||||
(t/is (contains? result :file))
|
||||
(t/is (contains? result :project)))))
|
||||
|
||||
;; (t/testing "authenticated with token"
|
||||
;; (let [data {::sq/type :viewer-bundle
|
||||
;; :token @token
|
||||
;; :file-id (:id file)
|
||||
;; :page-id (get-in file [:data :pages 0])}
|
||||
;; out (th/try-on! (sq/handle data))]
|
||||
(t/testing "authenticated with token"
|
||||
(let [data {::th/type :view-only-bundle
|
||||
:share-id @share-id
|
||||
:file-id (:id file)
|
||||
:page-id (get-in file [:data :pages 0])}
|
||||
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)))))
|
||||
))
|
||||
|
|
|
@ -229,8 +229,11 @@
|
|||
([conn {:keys [file-id changes session-id profile-id revn]
|
||||
:or {session-id (uuid/next) revn 0}}]
|
||||
(let [file (db/get-by-id conn :file file-id)
|
||||
msgbus (:app.msgbus/msgbus *system*)]
|
||||
(#'files/update-file {:conn conn :msgbus msgbus}
|
||||
msgbus (:app.msgbus/msgbus *system*)
|
||||
metrics (:app.metrics/metrics *system*)]
|
||||
(#'files/update-file {:conn conn
|
||||
:msgbus msgbus
|
||||
:metrics metrics}
|
||||
{:file file
|
||||
:revn revn
|
||||
:changes changes
|
||||
|
|
32
common/src/app/common/flags.cljc
Normal 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)))))))
|
||||
|
||||
|
|
@ -487,6 +487,7 @@
|
|||
(d/parse-double)
|
||||
(* (get-in modifiers [:resize-vector :x] 1))
|
||||
(* (get-in modifiers [:resize-vector-2 :x] 1))
|
||||
(mth/precision 2)
|
||||
(str))]
|
||||
(attrs/merge attrs {:font-size font-size})))]
|
||||
(update shape :content #(txt/transform-nodes
|
||||
|
|
|
@ -37,6 +37,8 @@
|
|||
:stroke-style :stroke-group
|
||||
:stroke-width :stroke-group
|
||||
:stroke-alignment :stroke-group
|
||||
:stroke-cap-start :stroke-group
|
||||
:stroke-cap-end :stroke-group
|
||||
:rx :radius-group
|
||||
:ry :radius-group
|
||||
:r1 :radius-group
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
(def empty-page-data
|
||||
{:options {}
|
||||
:name "Page"
|
||||
:name "Page-1"
|
||||
:objects
|
||||
{root
|
||||
{:id root
|
||||
|
@ -38,7 +38,7 @@
|
|||
|
||||
(def ^:private minimal-shapes
|
||||
[{:type :rect
|
||||
:name "Rect"
|
||||
:name "Rect-1"
|
||||
:fill-color default-color
|
||||
:fill-opacity 1
|
||||
:stroke-style :none
|
||||
|
@ -52,7 +52,7 @@
|
|||
{:type :image}
|
||||
|
||||
{:type :circle
|
||||
:name "Circle"
|
||||
:name "Circle-1"
|
||||
:fill-color default-color
|
||||
:fill-opacity 1
|
||||
:stroke-style :none
|
||||
|
@ -62,7 +62,7 @@
|
|||
:stroke-opacity 0}
|
||||
|
||||
{:type :path
|
||||
:name "Path"
|
||||
:name "Path-1"
|
||||
:stroke-style :solid
|
||||
:stroke-alignment :center
|
||||
:stroke-width 2
|
||||
|
@ -70,7 +70,7 @@
|
|||
:stroke-opacity 1}
|
||||
|
||||
{:type :frame
|
||||
:name "Artboard"
|
||||
:name "Artboard-1"
|
||||
:fill-color "#ffffff"
|
||||
:fill-opacity 1
|
||||
:stroke-style :none
|
||||
|
@ -80,7 +80,7 @@
|
|||
:stroke-opacity 0}
|
||||
|
||||
{:type :text
|
||||
:name "Text"
|
||||
:name "Text-1"
|
||||
:content nil}
|
||||
|
||||
{:type :svg-raw}])
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[app.common.geom.point :as gpt]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Specs
|
||||
|
@ -254,6 +255,17 @@
|
|||
(s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?))
|
||||
(s/def :internal.shape/stroke-opacity ::safe-number)
|
||||
(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-alignment #{:center :inner :outer})
|
||||
(s/def :internal.shape/text-align #{"left" "right" "center" "justify"})
|
||||
|
@ -342,6 +354,8 @@
|
|||
:internal.shape/stroke-style
|
||||
:internal.shape/stroke-width
|
||||
:internal.shape/stroke-alignment
|
||||
:internal.shape/stroke-cap-start
|
||||
:internal.shape/stroke-cap-end
|
||||
:internal.shape/text-align
|
||||
:internal.shape/transform
|
||||
:internal.shape/transform-inverse
|
||||
|
|
|
@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
|
|||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV NODE_VERSION=v14.17.3 \
|
||||
CLOJURE_VERSION=1.10.3.929 \
|
||||
CLJKONDO_VERSION=2021.06.18 \
|
||||
BABASHKA_VERSION=0.5.0 \
|
||||
ENV NODE_VERSION=v14.17.5 \
|
||||
CLOJURE_VERSION=1.10.3.933 \
|
||||
CLJKONDO_VERSION=2021.07.28 \
|
||||
BABASHKA_VERSION=0.5.1 \
|
||||
LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8
|
||||
|
||||
|
@ -44,6 +44,7 @@ RUN set -ex; \
|
|||
python \
|
||||
build-essential \
|
||||
imagemagick \
|
||||
ghostscript \
|
||||
netpbm \
|
||||
potrace \
|
||||
webp \
|
||||
|
@ -97,6 +98,15 @@ RUN set -ex; \
|
|||
; \
|
||||
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; \
|
||||
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; \
|
||||
|
|
|
@ -9,6 +9,7 @@ FROM gitpod/workspace-postgres
|
|||
RUN set -ex; \
|
||||
brew install redis; \
|
||||
brew install imagemagick; \
|
||||
brew install ghostscript; \
|
||||
brew install mailhog; \
|
||||
brew install openldap; \
|
||||
sudo mkdir -p /var/log/nginx; \
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
FROM ubuntu:20.04
|
||||
FROM debian:bullseye
|
||||
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
NODE_VERSION=v14.16.0
|
||||
NODE_VERSION=v14.17.5
|
||||
|
||||
RUN set -ex; \
|
||||
mkdir -p /etc/resolvconf/resolv.conf.d; \
|
||||
|
@ -20,6 +20,7 @@ RUN set -ex; \
|
|||
apt-get -qq update; \
|
||||
apt-get -qqy install \
|
||||
imagemagick \
|
||||
ghostscript \
|
||||
netpbm \
|
||||
potrace \
|
||||
gconf-service \
|
||||
|
@ -55,9 +56,9 @@ RUN set -ex; \
|
|||
libxss1 \
|
||||
libxtst6 \
|
||||
fonts-liberation \
|
||||
libappindicator1 \
|
||||
libnss3 \
|
||||
libgbm1 \
|
||||
chromium \
|
||||
; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
binaryage/devtools {:mvn/version "RELEASE"}
|
||||
metosin/reitit-core {:mvn/version "0.5.13"}
|
||||
lambdaisland/glogi {:mvn/version "1.0.106"}
|
||||
funcool/beicon {:mvn/version "2021.04.29-0"}
|
||||
funcool/beicon {:mvn/version "2021.07.05-1"}
|
||||
}
|
||||
:aliases
|
||||
{:outdated
|
||||
|
@ -14,7 +14,7 @@
|
|||
|
||||
:dev
|
||||
{:extra-deps
|
||||
{thheller/shadow-cljs {:mvn/version "2.14.1"}}}
|
||||
{thheller/shadow-cljs {:mvn/version "2.15.2"}}}
|
||||
|
||||
:shadow-cljs
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}
|
||||
|
|
|
@ -9,18 +9,18 @@
|
|||
"author": "UXBOX LABS SL",
|
||||
"license": "SEE LICENSE IN <LICENSE>",
|
||||
"dependencies": {
|
||||
"generic-pool": "^3.8.2",
|
||||
"inflation": "^2.0.0",
|
||||
"jszip": "^3.6.0",
|
||||
"jszip": "^3.7.0",
|
||||
"koa": "^2.13.0",
|
||||
"luxon": "^1.27.0",
|
||||
"puppeteer": "^10.0.0",
|
||||
"puppeteer-cluster": "^0.22.0",
|
||||
"luxon": "^2.0.1",
|
||||
"puppeteer-core": "^10.1.0",
|
||||
"raw-body": "^2.4.1",
|
||||
"xml-js": "^1.6.11",
|
||||
"xregexp": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shadow-cljs": "^2.14.2",
|
||||
"shadow-cljs": "^2.15.2",
|
||||
"source-map-support": "^0.5.19"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
|
||||
(ns app.browser
|
||||
(:require
|
||||
["puppeteer-cluster" :as ppc]
|
||||
["puppeteer-core" :as pp]
|
||||
["generic-pool" :as gp]
|
||||
[app.common.data :as d]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[lambdaisland.glogi :as log]
|
||||
[promesa.core :as p]))
|
||||
|
@ -20,12 +22,6 @@
|
|||
(str "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/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!
|
||||
[page {:keys [key value domain]}]
|
||||
(.setCookie ^js page #js {:name key
|
||||
|
@ -73,12 +69,14 @@
|
|||
|
||||
(defn pdf
|
||||
([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 {}
|
||||
omit-background? true
|
||||
prefer-css-page-size? true}}]
|
||||
prefer-css-page-size? true
|
||||
save-path nil}}]
|
||||
(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)
|
||||
:scale (:scale viewport)
|
||||
:omitBackground omit-background?
|
||||
|
@ -100,36 +98,76 @@
|
|||
|
||||
;; --- BROWSER STATE
|
||||
|
||||
(def instance (atom nil))
|
||||
(defonce pool (atom nil))
|
||||
(defonce pool-browser-id (atom 1))
|
||||
|
||||
(defn- create-browser
|
||||
[concurrency strategy]
|
||||
(let [strategy (case strategy
|
||||
:browser (.-CONCURRENCY_BROWSER ^js ppc/Cluster)
|
||||
:incognito (.-CONCURRENCY_CONTEXT ^js ppc/Cluster)
|
||||
:page (.-CONCURRENCY_PAGE ^js ppc/Cluster))
|
||||
opts #js {:concurrency strategy
|
||||
:maxConcurrency concurrency
|
||||
:puppeteerOptions #js {:args #js ["--no-sandbox"]}}]
|
||||
(.launch ^js ppc/Cluster opts)))
|
||||
(def browser-pool-factory
|
||||
(letfn [(create []
|
||||
(let [path (cf/get :browser-executable-path "/usr/bin/google-chrome")]
|
||||
(-> (pp/launch #js {:executablePath path :args #js ["--no-sandbox"]})
|
||||
(p/then (fn [browser]
|
||||
(let [id (deref pool-browser-id)]
|
||||
(log/info :origin "factory" :action "create" :browser-id id)
|
||||
(unchecked-set browser "__num_use" 0)
|
||||
(unchecked-set browser "__id" id)
|
||||
(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
|
||||
[]
|
||||
(let [concurrency (cf/get :browser-concurrency)
|
||||
strategy (cf/get :browser-strategy)]
|
||||
(-> (create-browser concurrency strategy)
|
||||
(p/then #(reset! instance %))
|
||||
(p/catch (fn [error]
|
||||
(log/error :msg "failed to initialize browser")
|
||||
(js/console.error error))))))
|
||||
(log/info :msg "initializing browser pool")
|
||||
(let [opts #js {:max (cf/get :browser-pool-max 3)
|
||||
:min (cf/get :browser-pool-min 0)
|
||||
:testOnBorrow true
|
||||
:evictionRunIntervalMillis 30000
|
||||
:numTestsPerEvictionRun 5
|
||||
:acquireTimeoutMillis 120000 ; 2min
|
||||
:idleTimeoutMillis 30000}]
|
||||
|
||||
(reset! pool (gp/createPool browser-pool-factory opts))
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn stop
|
||||
[]
|
||||
(if-let [instance @instance]
|
||||
(p/do!
|
||||
(.idle ^js instance)
|
||||
(.close ^js instance)
|
||||
(log/info :msg "shutdown headless browser"))
|
||||
(p/resolved nil)))
|
||||
(when-let [pool (deref pool)]
|
||||
(log/info :msg "finalizing browser pool")
|
||||
(-> (.drain ^js pool)
|
||||
(p/then (fn [] (.clear ^js pool))))))
|
||||
|
||||
(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))))))
|
||||
|
|
|
@ -8,13 +8,15 @@
|
|||
(:require
|
||||
[app.config :as cf]
|
||||
[app.http.export :refer [export-handler]]
|
||||
[app.http.export-frames :refer [export-frames-handler]]
|
||||
[app.http.impl :as impl]
|
||||
[lambdaisland.glogi :as log]
|
||||
[promesa.core :as p]
|
||||
[reitit.core :as r]))
|
||||
|
||||
(def routes
|
||||
[["/export" {:handler export-handler}]])
|
||||
[["/export-frames" {:handler export-frames-handler}]
|
||||
["/export" {:handler export-handler}]])
|
||||
|
||||
(def instance (atom nil))
|
||||
|
||||
|
|
69
exporter/src/app/http/export_frames.cljs
Normal 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)}})))
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
:value token}))
|
||||
|
||||
(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]
|
||||
(let [path (str "/render-object/" file-id "/" page-id "/" object-id)
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
|
@ -55,7 +55,7 @@
|
|||
:png (bw/screenshot dom {:omit-background? true :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 ::suffix ::us/string)
|
||||
|
@ -74,13 +74,7 @@
|
|||
(defn render
|
||||
[params]
|
||||
(us/assert ::render-params params)
|
||||
(let [browser @bw/instance]
|
||||
(when-not browser
|
||||
(ex/raise :type :internal
|
||||
:code :browser-not-ready
|
||||
:hint "browser cluster is not initialized yet"))
|
||||
|
||||
(p/let [content (screenshot-object browser params)]
|
||||
(p/let [content (screenshot-object params)]
|
||||
{:content content
|
||||
:filename (or (:filename params)
|
||||
(str (:name params)
|
||||
|
@ -91,5 +85,5 @@
|
|||
:length (alength content)
|
||||
:mime-type (case (:type params)
|
||||
:png "image/png"
|
||||
:jpeg "image/jpeg")})))
|
||||
:jpeg "image/jpeg")}))
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
:value token}))
|
||||
|
||||
(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]
|
||||
(let [path (str "/render-object/" file-id "/" page-id "/" object-id)
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
|
@ -42,9 +42,11 @@
|
|||
(bw/configure-page! page options)
|
||||
(bw/navigate! page uri)
|
||||
(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 ::suffix ::us/string)
|
||||
|
@ -54,26 +56,21 @@
|
|||
(s/def ::scale ::us/number)
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::filename ::us/string)
|
||||
(s/def ::save-path ::us/string)
|
||||
|
||||
(s/def ::render-params
|
||||
(s/keys :req-un [::name ::suffix ::object-id ::page-id ::scale ::token ::file-id]
|
||||
:opt-un [::filename]))
|
||||
:opt-un [::filename ::save-path]))
|
||||
|
||||
(defn render
|
||||
[params]
|
||||
(us/assert ::render-params params)
|
||||
(let [browser @bw/instance]
|
||||
(when-not browser
|
||||
(ex/raise :type :internal
|
||||
:code :browser-not-ready
|
||||
:hint "browser cluster is not initialized yet"))
|
||||
|
||||
(p/let [content (pdf-from-object browser params)]
|
||||
(p/let [content (pdf-from-object params)]
|
||||
{:content content
|
||||
:filename (or (:filename params)
|
||||
(str (:name params)
|
||||
(:suffix params "")
|
||||
".pdf"))
|
||||
:length (alength content)
|
||||
:mime-type "application/pdf"})))
|
||||
:mime-type "application/pdf"}))
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
|
||||
|
||||
(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]
|
||||
(log/trace :fn :convert-to-ppm)
|
||||
(let [basepath (path/dirname pngpath)
|
||||
|
@ -279,7 +279,7 @@
|
|||
rctx {:cookie cookie
|
||||
:uri (str uri)}]
|
||||
(log/info :uri (:uri rctx))
|
||||
(bw/exec! browser (partial handle rctx)))))
|
||||
(bw/exec! (partial handle rctx)))))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::suffix ::us/string)
|
||||
|
@ -298,18 +298,11 @@
|
|||
(defn render
|
||||
[params]
|
||||
(us/assert ::render-params params)
|
||||
(let [browser @bw/instance]
|
||||
(when-not browser
|
||||
(ex/raise :type :internal
|
||||
:code :browser-not-ready
|
||||
:hint "browser cluster is not initialized yet"))
|
||||
|
||||
|
||||
(p/let [content (render-object browser params)]
|
||||
(p/let [content (render-object params)]
|
||||
{:content content
|
||||
:filename (or (:filename params)
|
||||
(str (:name params)
|
||||
(:suffix params "")
|
||||
".svg"))
|
||||
:length (alength content)
|
||||
:mime-type "image/svg+xml"})))
|
||||
:mime-type "image/svg+xml"}))
|
||||
|
|
|
@ -2,23 +2,23 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/runtime-corejs3@^7.12.1":
|
||||
version "7.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.0.tgz#6bf5fbc0b961f8e3202888cb2cd0fb7a0a9a3f66"
|
||||
integrity sha512-0R0HTZWHLk6G8jIk0FtoX+AatCtKnswS98VhXwGImFc759PJRp4Tru0PQYZofyijTFUr+gT8Mu7sgXVJLQ0ceg==
|
||||
"@babel/runtime-corejs3@^7.14.9":
|
||||
version "7.15.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.15.3.tgz#28754263988198f2a928c09733ade2fb4d28089d"
|
||||
integrity sha512-30A3lP+sRL6ml8uhoJSs+8jwpKzbw8CqBvDc1laeptxPm5FahumJxirigcbD2qTs71Sonvj1cyZB0OKGAmxQ+A==
|
||||
dependencies:
|
||||
core-js-pure "^3.0.0"
|
||||
core-js-pure "^3.16.0"
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@types/node@*":
|
||||
version "15.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.2.tgz#c61d49f38af70da32424b5322eee21f97e627175"
|
||||
integrity sha512-dxcOx8801kMo3KlU+C+/ctWrzREAH7YvoF3aoVpRdqgs+Kf7flp+PJDN/EX5bME3suDUZHsxes9hpvBmzYlWbA==
|
||||
version "16.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50"
|
||||
integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA==
|
||||
|
||||
"@types/yauzl@^2.9.1":
|
||||
version "2.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af"
|
||||
integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a"
|
||||
integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
|
@ -60,11 +60,6 @@ assert@^1.1.1:
|
|||
object-assign "^4.1.1"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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=
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
buffer-xor@^1.0.3:
|
||||
version "1.0.3"
|
||||
|
@ -271,10 +266,10 @@ cookies@~0.8.0:
|
|||
depd "~2.0.0"
|
||||
keygrip "~1.1.0"
|
||||
|
||||
core-js-pure@^3.0.0:
|
||||
version "3.13.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.13.1.tgz#5d139d346780f015f67225f45ee2362a6bed6ba1"
|
||||
integrity sha512-wVlh0IAi2t1iOEh16y4u1TRk6ubd4KvLE8dlMi+3QUI6SfKphQUh7tAwihGGSQ8affxEXpVIPpOdf9kjR4v4Pw==
|
||||
core-js-pure@^3.16.0:
|
||||
version "3.16.2"
|
||||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.16.2.tgz#0ef4b79cabafb251ea86eb7d139b42bd98c533e8"
|
||||
integrity sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw==
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
|
@ -329,7 +324,14 @@ crypto-browserify@^3.11.0:
|
|||
randombytes "^2.0.0"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
|
||||
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
|
||||
|
@ -376,10 +378,10 @@ destroy@^1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
||||
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
|
||||
|
||||
devtools-protocol@0.0.883894:
|
||||
version "0.0.883894"
|
||||
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.883894.tgz#d403f2c75cd6d71c916aee8dde9258da988a4da9"
|
||||
integrity sha512-33idhm54QJzf3Q7QofMgCvIVSd2o9H3kQPWaKT/fhoZh+digc+WSiMhbkeG3iN79WY4Hwr9G05NpbhEVrsOYAg==
|
||||
devtools-protocol@0.0.901419:
|
||||
version "0.0.901419"
|
||||
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd"
|
||||
integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==
|
||||
|
||||
diffie-hellman@^5.0.0:
|
||||
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"
|
||||
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:
|
||||
version "5.2.0"
|
||||
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"
|
||||
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:
|
||||
version "3.1.0"
|
||||
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=
|
||||
|
||||
is-generator-function@^1.0.7:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.9.tgz#e5f82c2323673e7fcad3d12858c83c4039f6399c"
|
||||
integrity sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A==
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
|
||||
integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
|
||||
dependencies:
|
||||
has-tostringtag "^1.0.0"
|
||||
|
||||
isarray@^1.0.0, isarray@~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"
|
||||
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
||||
|
||||
jszip@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.6.0.tgz#839b72812e3f97819cc13ac4134ffced95dd6af9"
|
||||
integrity sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==
|
||||
jszip@^3.7.0:
|
||||
version "3.7.1"
|
||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9"
|
||||
integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
|
||||
dependencies:
|
||||
lie "~3.3.0"
|
||||
pako "~1.0.2"
|
||||
|
@ -712,10 +733,10 @@ locate-path@^5.0.0:
|
|||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
luxon@^1.27.0:
|
||||
version "1.27.0"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.27.0.tgz#ae10c69113d85dab8f15f5e8390d0cbeddf4f00f"
|
||||
integrity sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA==
|
||||
luxon@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133"
|
||||
integrity sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg==
|
||||
|
||||
md5.js@^1.3.4:
|
||||
version "1.3.5"
|
||||
|
@ -739,17 +760,17 @@ miller-rabin@^4.0.0:
|
|||
bn.js "^4.0.0"
|
||||
brorand "^1.0.1"
|
||||
|
||||
mime-db@1.48.0:
|
||||
version "1.48.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"
|
||||
integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==
|
||||
mime-db@1.49.0:
|
||||
version "1.49.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
|
||||
integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==
|
||||
|
||||
mime-types@^2.1.18, mime-types@~2.1.24:
|
||||
version "2.1.31"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b"
|
||||
integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==
|
||||
version "2.1.32"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5"
|
||||
integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==
|
||||
dependencies:
|
||||
mime-db "1.48.0"
|
||||
mime-db "1.49.0"
|
||||
|
||||
minimalistic-assert@^1.0.0, minimalistic-assert@^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"
|
||||
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
||||
|
||||
puppeteer-cluster@^0.22.0:
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/puppeteer-cluster/-/puppeteer-cluster-0.22.0.tgz#4ab214671f414f15ad6a94a4b61ed0b4172e86e6"
|
||||
integrity sha512-hmydtMwfVM+idFIDzS8OXetnujHGre7RY3BGL+3njy9+r8Dcu3VALkZHfuBEPf6byKssTCgzxU1BvLczifXd5w==
|
||||
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==
|
||||
puppeteer-core@^10.1.0:
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-10.2.0.tgz#8d6606cf345fc0e421bc0612055579ea53234111"
|
||||
integrity sha512-c1COxSnfynsE6Mtt+dW0t3TITjF9Ku4dnJbFMDDVhLQuMTYSpz4rkSP37qvzcSo3k02/Ac3GYWk0/ncp6DKZNA==
|
||||
dependencies:
|
||||
debug "4.3.1"
|
||||
devtools-protocol "0.0.883894"
|
||||
devtools-protocol "0.0.901419"
|
||||
extract-zip "2.0.1"
|
||||
https-proxy-agent "5.0.0"
|
||||
node-fetch "2.6.1"
|
||||
|
@ -1074,9 +1088,9 @@ readline-sync@^1.4.7:
|
|||
integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==
|
||||
|
||||
regenerator-runtime@^0.13.4:
|
||||
version "0.13.7"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
|
||||
version "0.13.9"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
||||
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
|
||||
|
||||
rimraf@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"
|
||||
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
|
||||
|
||||
shadow-cljs@^2.14.2:
|
||||
version "2.14.2"
|
||||
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.14.2.tgz#dba651ea124028064aea6fa9a390f257cb6eede4"
|
||||
integrity sha512-ficaYfBAATzJ6OGt/GbIl393+cqLchzNkdTrM2PY4ttbsAOyBfWd39t+PZcYpCqemXjkgfBdZt9DJda7WaHJGA==
|
||||
shadow-cljs@^2.15.2:
|
||||
version "2.15.4"
|
||||
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.4.tgz#0d657fc8ab9a02d8980db5c49cb1622e8fc6fa52"
|
||||
integrity sha512-xn8UsiVpOf2LTsQZLsCa910CcMCYdMRT6STAsgveOEIncC9cunGdqE7cTq69vTmIijVQmzf0A1nALidyzO3Hcw==
|
||||
dependencies:
|
||||
node-libs-browser "^2.2.1"
|
||||
readline-sync "^1.4.7"
|
||||
shadow-cljs-jar "1.3.2"
|
||||
source-map-support "^0.4.15"
|
||||
which "^1.3.1"
|
||||
ws "^3.0.0"
|
||||
ws "^7.4.6"
|
||||
|
||||
source-map-support@^0.4.15:
|
||||
version "0.4.18"
|
||||
|
@ -1282,11 +1296,6 @@ type-is@^1.6.16:
|
|||
media-typer "0.3.0"
|
||||
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:
|
||||
version "1.3.3"
|
||||
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"
|
||||
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
|
||||
|
||||
ws@^3.0.0:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
|
||||
integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
safe-buffer "~5.1.0"
|
||||
ultron "~1.1.0"
|
||||
ws@^7.4.6:
|
||||
version "7.5.3"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
|
||||
integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
|
||||
|
||||
xml-js@^1.6.11:
|
||||
version "1.6.11"
|
||||
|
@ -1371,11 +1376,11 @@ xml-js@^1.6.11:
|
|||
sax "^1.2.4"
|
||||
|
||||
xregexp@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.0.2.tgz#798aa7757836f39cdbdeeba3daf94d75f7a9dcc1"
|
||||
integrity sha512-JPNfN40YMNSDxZrahMrmtNH1QqPJp0/qNeEJM2nnOlhcBdfCCjekPYFV2OnwKxwvpEYglH1RBotbpRRaEuCG8Q==
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.1.0.tgz#c87e7ae5ffa5fdc520f898a467dcba02b0d391e9"
|
||||
integrity sha512-PynwUWtXnSZr8tpQlDPMZfPTyv78EYuA4oI959ukxcQ0a9O/lvndLVKy5wpImzzA26eMxpZmnAXJYiQA13AtWA==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.12.1"
|
||||
"@babel/runtime-corejs3" "^7.14.9"
|
||||
|
||||
xtend@^4.0.0:
|
||||
version "4.0.2"
|
||||
|
|
1
frontend/resources/images/cap-circle-marker.svg
Normal 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 |
1
frontend/resources/images/cap-diamond-marker.svg
Normal 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 |
1
frontend/resources/images/cap-line-arrow.svg
Normal 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 |
1
frontend/resources/images/cap-round.svg
Normal 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 |
1
frontend/resources/images/cap-square-marker.svg
Normal 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 |
1
frontend/resources/images/cap-square.svg
Normal 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 |
1
frontend/resources/images/cap-triangle-arrow.svg
Normal 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 |
BIN
frontend/resources/images/features/export-artboards.gif
Normal file
After Width: | Height: | Size: 366 KiB |
BIN
frontend/resources/images/features/navigate-history.gif
Normal file
After Width: | Height: | Size: 250 KiB |
BIN
frontend/resources/images/features/share-viewer.gif
Normal file
After Width: | Height: | Size: 705 KiB |
BIN
frontend/resources/images/features/stroke-caps.gif
Normal file
After Width: | Height: | Size: 206 KiB |
5
frontend/resources/images/icons/switch.svg
Normal 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 |
|
@ -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 {
|
||||
@extend %btn;
|
||||
background: $color-gray-30;
|
||||
|
@ -588,7 +606,6 @@ input.element-name {
|
|||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.column {
|
||||
|
@ -975,6 +992,14 @@ input[type=range]:focus::-ms-fill-upper {
|
|||
}
|
||||
}
|
||||
|
||||
&.tooltip-expand {
|
||||
&:hover {
|
||||
&::after {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tooltip-bottom-left {
|
||||
&:hover {
|
||||
&::after {
|
||||
|
@ -1130,7 +1155,7 @@ input[type=range]:focus::-ms-fill-upper {
|
|||
padding-left: 16px;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 13;
|
||||
z-index: 1005;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -88,3 +88,4 @@
|
|||
@import "main/partials/color-bullet";
|
||||
@import "main/partials/handoff";
|
||||
@import "main/partials/exception-page";
|
||||
@import "main/partials/share-link";
|
||||
|
|
|
@ -53,8 +53,8 @@
|
|||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -154,6 +154,10 @@
|
|||
.modal-footer .action-buttons {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.fields-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-dialog {
|
||||
|
@ -807,7 +811,7 @@
|
|||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
height: 100%;
|
||||
width: 106%;
|
||||
width: 115%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
141
frontend/resources/styles/main/partials/share-link.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1316,7 +1316,7 @@
|
|||
|
||||
&::after {
|
||||
content: ' ';
|
||||
background-color: $color-gray-20;
|
||||
background-color: $color-gray-30;
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,17 +42,50 @@
|
|||
}
|
||||
}
|
||||
|
||||
.options-zone {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
// width: 384px;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
|
||||
> * {
|
||||
margin-left: $big;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.zoom-widget {
|
||||
.dropdown {
|
||||
top: 45px;
|
||||
left: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-options {
|
||||
.icon {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
width: 90px;
|
||||
|
||||
> span {
|
||||
color: $color-gray-10;
|
||||
font-size: $fs13;
|
||||
margin-right: $x-small;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
fill: $color-gray-30;
|
||||
height: 30px;
|
||||
width: 28px;
|
||||
fill: $color-gray-10;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -64,34 +97,9 @@
|
|||
|
||||
.dropdown {
|
||||
min-width: 260px;
|
||||
left: 0px;
|
||||
top: 40px;
|
||||
top: 45px;
|
||||
left: -25px;
|
||||
}
|
||||
|
||||
.view-options-dropdown {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
color: $color-gray-10;
|
||||
font-size: $fs13;
|
||||
margin-right: $x-small;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $color-gray-10;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-menu {
|
||||
.dropdown {
|
||||
min-width: 100px;
|
||||
right: 0px;
|
||||
top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,39 +108,50 @@
|
|||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: $x-small;
|
||||
position: relative;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
fill: $color-gray-20;
|
||||
height: 20px;
|
||||
height: 12px;
|
||||
margin-right: $small;
|
||||
width: 20px;
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
.breadcrumb, .current-frame {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
> span {
|
||||
color: $color-gray-20;
|
||||
margin-right: $x-small;
|
||||
font-size: $fs14;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.frame-name {
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.show-thumbnails-button svg {
|
||||
fill: $color-white;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.page-name {
|
||||
> .dropdown {
|
||||
top: 45px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.current-frame {
|
||||
display: flex;
|
||||
span {
|
||||
color: $color-white;
|
||||
margin-right: $x-small;
|
||||
}
|
||||
|
||||
.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 {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
.viewer-thumbnails {
|
||||
grid-row: 1 / span 1;
|
||||
grid-column: 1 / span 1;
|
||||
|
@ -9,6 +8,11 @@
|
|||
flex-direction: column;
|
||||
z-index: 12;
|
||||
|
||||
&.invisible {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
grid-row: 1 / span 2;
|
||||
|
||||
|
@ -159,7 +163,7 @@
|
|||
|
||||
&:hover {
|
||||
border-color: $color-primary;
|
||||
border-width: 2px;
|
||||
outline: 2px solid $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
margin-left: $x-small;
|
||||
}
|
||||
|
||||
.dropdown-button svg {
|
||||
.icon svg {
|
||||
fill: $color-gray-10;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.zoom-dropdown {
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
z-index: 12;
|
||||
width: 210px;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.config
|
||||
(:require
|
||||
[app.common.flags :as flags]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.common.version :as v]
|
||||
|
@ -53,10 +54,14 @@
|
|||
:browser
|
||||
:webworker))
|
||||
|
||||
(def default-flags
|
||||
#{:registration :demo-users})
|
||||
|
||||
(defn- parse-flags
|
||||
[global]
|
||||
(let [flags (obj/get global "penpotFlags" "")]
|
||||
(into #{} (map keyword) (str/words flags))))
|
||||
(let [flags (obj/get global "penpotFlags" "")
|
||||
flags (into #{} (map keyword) (str/words flags))]
|
||||
(flags/parse default-flags flags)))
|
||||
|
||||
(defn- parse-version
|
||||
[global]
|
||||
|
@ -68,26 +73,27 @@
|
|||
(def default-theme "default")
|
||||
(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 gitlab-client-id (obj/get global "penpotGitlabClientID" nil))
|
||||
(def github-client-id (obj/get global "penpotGithubClientID" 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 translations (obj/get global "penpotTranslations"))
|
||||
(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)))
|
||||
(def target (delay (parse-target global)))
|
||||
(def browser (delay (parse-browser)))
|
||||
(def platform (delay (parse-platform)))
|
||||
;; mantain for backward compatibility
|
||||
(let [login-with-ldap (obj/get global "penpotLoginWithLDAP" false)
|
||||
registration (obj/get global "penpotRegistrationEnabled" true)]
|
||||
(when login-with-ldap
|
||||
(swap! flags conj :login-with-ldap))
|
||||
(when (false? registration)
|
||||
(swap! flags disj :registration)))
|
||||
|
||||
(def public-uri
|
||||
(let [uri (u/uri (or (obj/get global "penpotPublicURI")
|
||||
|
|
|
@ -42,9 +42,12 @@
|
|||
(if-let [conform (get-in match [:data :conform])]
|
||||
(let [spath (get conform :path-params ::any)
|
||||
squery (get conform :query-params ::any)]
|
||||
(try
|
||||
(-> (dissoc match :params)
|
||||
(assoc :path-params (us/conform spath (get match :path-params))
|
||||
:query-params (us/conform squery (get match :query-params)))))
|
||||
:query-params (us/conform squery (get match :query-params))))
|
||||
(catch :default _
|
||||
nil)))
|
||||
match)))
|
||||
|
||||
(defn on-navigate
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
(update :workspace-drawing dissoc :comment)
|
||||
(update-in [:comments id] assoc (:id comment) comment)))]
|
||||
|
||||
(ptk/reify ::create-thread
|
||||
(ptk/reify ::create-comment-thread
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/mutation :create-comment-thread params)
|
||||
|
@ -94,6 +94,8 @@
|
|||
[{:keys [id is-resolved] :as thread}]
|
||||
(us/assert ::comment-thread thread)
|
||||
(ptk/reify ::update-comment-thread
|
||||
IDeref
|
||||
(-deref [_] {:is-resolved is-resolved})
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
@ -122,7 +124,7 @@
|
|||
(defn update-comment
|
||||
[{:keys [id content thread-id] :as comment}]
|
||||
(us/assert ::comment comment)
|
||||
(ptk/reify :update-comment
|
||||
(ptk/reify ::update-comment
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(d/update-in-when state [:comments thread-id id] assoc :content content))
|
||||
|
@ -135,7 +137,7 @@
|
|||
(defn delete-comment-thread
|
||||
[{:keys [id] :as thread}]
|
||||
(us/assert ::comment-thread thread)
|
||||
(ptk/reify :delete-comment-thread
|
||||
(ptk/reify ::delete-comment-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
|
@ -150,7 +152,7 @@
|
|||
(defn delete-comment
|
||||
[{:keys [id thread-id] :as comment}]
|
||||
(us/assert ::comment comment)
|
||||
(ptk/reify :delete-comment
|
||||
(ptk/reify ::delete-comment
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(d/update-in-when state [:comments thread-id] dissoc id))
|
||||
|
@ -212,7 +214,7 @@
|
|||
(defn open-thread
|
||||
[{:keys [id] :as thread}]
|
||||
(us/assert ::comment-thread thread)
|
||||
(ptk/reify ::open-thread
|
||||
(ptk/reify ::open-comment-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
|
@ -221,7 +223,7 @@
|
|||
|
||||
(defn close-thread
|
||||
[]
|
||||
(ptk/reify ::close-thread
|
||||
(ptk/reify ::close-comment-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
|
|
46
frontend/src/app/main/data/common.cljs
Normal 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)))))
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.data.media :as di]
|
||||
[app.main.data.users :as du]
|
||||
|
@ -386,6 +387,9 @@
|
|||
(us/assert ::us/email email)
|
||||
(us/assert ::us/keyword role)
|
||||
(ptk/reify ::invite-team-member
|
||||
IDeref
|
||||
(-deref [_] {:role role})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [{:keys [on-success on-error]
|
||||
|
@ -475,6 +479,10 @@
|
|||
(us/assert ::us/uuid id)
|
||||
(us/assert ::us/uuid team-id)
|
||||
(ptk/reify ::move-project
|
||||
IDeref
|
||||
(-deref [_]
|
||||
{:id id :team-id team-id})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
|
@ -566,6 +574,10 @@
|
|||
[{:keys [id name] :as params}]
|
||||
(us/assert ::file params)
|
||||
(ptk/reify ::rename-file
|
||||
IDeref
|
||||
(-deref [_]
|
||||
{::ev/origin "dashboard" :id id :name name})
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
|
@ -585,6 +597,10 @@
|
|||
[{:keys [id is-shared] :as params}]
|
||||
(us/assert ::file params)
|
||||
(ptk/reify ::set-file-shared
|
||||
IDeref
|
||||
(-deref [_]
|
||||
{::ev/origin "dashboard" :id id :shared is-shared})
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
|
@ -663,12 +679,16 @@
|
|||
(us/assert ::set-of-uuid ids)
|
||||
(us/assert ::us/uuid project-id)
|
||||
(ptk/reify ::move-files
|
||||
IDeref
|
||||
(-deref [_]
|
||||
{:num-files (count ids)
|
||||
:project-id project-id})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)]
|
||||
|
||||
(->> (rp/mutation! :move-files {:ids ids :project-id project-id})
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
@ -690,14 +710,14 @@
|
|||
|
||||
(defn go-to-files
|
||||
([project-id]
|
||||
(ptk/reify ::go-to-files
|
||||
(ptk/reify ::go-to-files-1
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(rx/of (rt/nav :dashboard-files {:team-id team-id
|
||||
:project-id project-id}))))))
|
||||
([team-id project-id]
|
||||
(ptk/reify ::go-to-files
|
||||
(ptk/reify ::go-to-files-2
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (rt/nav :dashboard-files {:team-id team-id
|
||||
|
@ -719,13 +739,13 @@
|
|||
|
||||
(defn go-to-projects
|
||||
([]
|
||||
(ptk/reify ::go-to-projects
|
||||
(ptk/reify ::go-to-projects-0
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
|
||||
([team-id]
|
||||
(ptk/reify ::go-to-projects
|
||||
(ptk/reify ::go-to-projects-1
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(du/set-current-team! team-id)
|
||||
|
|
|
@ -71,18 +71,84 @@
|
|||
|
||||
;; --- 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 ::event
|
||||
[event]
|
||||
(let [data (deref event)]
|
||||
(let [data (deref event)
|
||||
origin (::origin data)]
|
||||
(when (::name data)
|
||||
(d/without-nils
|
||||
{:type (::type data "action")
|
||||
:name (::name 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
|
||||
[event]
|
||||
|
@ -113,42 +179,6 @@
|
|||
:profile-id (:id data)
|
||||
: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
|
||||
|
||||
(defn- append-to-buffer
|
||||
|
@ -164,7 +194,7 @@
|
|||
(defn- persist-events
|
||||
[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}]
|
||||
(->> (http/send! {:uri uri
|
||||
:method :post
|
||||
|
@ -203,8 +233,7 @@
|
|||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ stream]
|
||||
(let [events (methods process-event)
|
||||
session (atom nil)
|
||||
(let [session (atom nil)
|
||||
|
||||
profile (->> (rx/from-atom storage {:emit-current-value? true})
|
||||
(rx/map :profile)
|
||||
|
@ -215,12 +244,9 @@
|
|||
(rx/with-latest-from profile)
|
||||
(rx/map (fn [result]
|
||||
(let [event (aget result 0)
|
||||
profile-id (aget result 1)
|
||||
type (ptk/type event)
|
||||
impl-fn (get events type)]
|
||||
(when (fn? impl-fn)
|
||||
(some-> (impl-fn event)
|
||||
(update :profile-id #(or % profile-id)))))))
|
||||
profile-id (aget result 1)]
|
||||
(some-> (process-event event)
|
||||
(update :profile-id #(or % profile-id))))))
|
||||
(rx/filter :profile-id)
|
||||
(rx/map (fn [event]
|
||||
(let [session* (or @session (dt/now))
|
||||
|
@ -242,6 +268,6 @@
|
|||
|
||||
(defmethod ptk/resolve ::initialize
|
||||
[_ params]
|
||||
(if cf/analytics
|
||||
(if (contains? @cf/flags :audit-log)
|
||||
(initialize)
|
||||
(ptk/data-event ::initialize params)))
|
||||
|
|
|
@ -187,6 +187,9 @@
|
|||
(defn add-font
|
||||
[font]
|
||||
(ptk/reify ::add-font
|
||||
IDeref
|
||||
(-deref [_] (select-keys font [:font-family :font-style :font-weight]))
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :dashboard-fonts assoc (:id font) font))))
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
|
||||
;; --- COMMON SPECS
|
||||
|
||||
(defn is-authenticated?
|
||||
[{:keys [id]}]
|
||||
(and (uuid? id) (not= id uuid/zero)))
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::fullname ::us/string)
|
||||
(s/def ::email ::us/email)
|
||||
|
|
|
@ -14,24 +14,12 @@
|
|||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[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
|
||||
|
||||
(def ^:private
|
||||
|
@ -49,25 +37,24 @@
|
|||
(declare fetch-bundle)
|
||||
(declare bundle-fetched)
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(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 ::initialize-params
|
||||
(s/keys :req-un [::page-id ::file-id]
|
||||
:opt-un [::token]))
|
||||
(s/keys :req-un [::file-id]
|
||||
:opt-un [::share-id ::page-id]))
|
||||
|
||||
(defn initialize
|
||||
[{:keys [page-id file-id] :as params}]
|
||||
[{:keys [file-id] :as params}]
|
||||
(us/assert ::initialize-params params)
|
||||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc :current-file-id file-id)
|
||||
(assoc :current-page-id page-id)
|
||||
(update :viewer-local
|
||||
(fn [lstate]
|
||||
(if (nil? lstate)
|
||||
|
@ -77,55 +64,72 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(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
|
||||
(s/keys :req-un [::page-id ::file-id]
|
||||
:opt-un [::token]))
|
||||
(defn finalize
|
||||
[_]
|
||||
(ptk/reify ::finalize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(dissoc state :viewer))))
|
||||
|
||||
(defn fetch-bundle
|
||||
[{:keys [page-id file-id token] :as params}]
|
||||
(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]
|
||||
(defn select-frames
|
||||
[{:keys [objects] :as page}]
|
||||
(let [root (get objects uuid/zero)]
|
||||
(into [] (comp (map #(get objects %))
|
||||
(filter #(= :frame (:type %))))
|
||||
(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
|
||||
[{:keys [project file page share-token token libraries users] :as bundle}]
|
||||
(us/verify ::bundle bundle)
|
||||
[{:keys [project file share-links libraries users permissions] :as bundle}]
|
||||
(let [pages (->> (get-in file [:data :pages])
|
||||
(map (fn [page-id]
|
||||
(let [data (get-in file [:data :pages-index page-id])]
|
||||
[page-id (assoc data :frames (select-frames data))])))
|
||||
(into {}))]
|
||||
|
||||
(ptk/reify ::bundle-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (:objects page)
|
||||
frames (extract-frames objects)]
|
||||
(-> state
|
||||
(assoc :viewer-libraries (d/index-by :id libraries))
|
||||
(update :viewer-data assoc
|
||||
:project project
|
||||
:objects objects
|
||||
(assoc :share-links share-links)
|
||||
(assoc :viewer {:libraries (d/index-by :id libraries)
|
||||
:users (d/index-by :id users)
|
||||
:file file
|
||||
:page page
|
||||
:frames frames
|
||||
:token token
|
||||
:share-token share-token))))))
|
||||
:permissions permissions
|
||||
:project project
|
||||
:pages pages
|
||||
:file file}))))))
|
||||
|
||||
(defn fetch-comment-threads
|
||||
[{:keys [file-id page-id] :as params}]
|
||||
|
@ -168,32 +172,6 @@
|
|||
(->> (rp/query :comments {:thread-id thread-id})
|
||||
(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
|
||||
|
||||
(def increase-zoom
|
||||
|
@ -245,29 +223,32 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
screen (-> route :data :name keyword)
|
||||
qparams (:query-params route)
|
||||
pparams (:path-params route)
|
||||
index (:index qparams)]
|
||||
(when (pos? index)
|
||||
(rx/of
|
||||
(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
|
||||
(ptk/reify ::select-prev-frame
|
||||
(ptk/reify ::select-next-frame
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(prn "select-next-frame")
|
||||
(let [route (:route state)
|
||||
screen (-> route :data :name keyword)
|
||||
qparams (:query-params route)
|
||||
pparams (:path-params route)
|
||||
qparams (:query-params route)
|
||||
|
||||
page-id (:page-id 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))
|
||||
(rx/of
|
||||
(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})
|
||||
|
||||
|
@ -309,7 +290,7 @@
|
|||
|
||||
(defn go-to-frame-by-index
|
||||
[index]
|
||||
(ptk/reify ::go-to-frame
|
||||
(ptk/reify ::go-to-frame-by-index
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
|
@ -324,12 +305,15 @@
|
|||
(ptk/reify ::go-to-frame
|
||||
ptk/WatchEvent
|
||||
(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))]
|
||||
(when index
|
||||
(rx/of (go-to-frame-by-index index)))))))
|
||||
|
||||
|
||||
(defn go-to-section
|
||||
[section]
|
||||
(ptk/reify ::go-to-section
|
||||
|
@ -340,13 +324,6 @@
|
|||
qparams (:query-params route)]
|
||||
(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 []
|
||||
(ptk/reify ::deselect-all
|
||||
ptk/UpdateEvent
|
||||
|
@ -376,7 +353,10 @@
|
|||
(ptk/reify ::shift-select-to
|
||||
ptk/UpdateEvent
|
||||
(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
|
||||
(get-in [:viewer-local :selected] #{})
|
||||
(conj id))]
|
||||
|
@ -389,8 +369,13 @@
|
|||
(ptk/reify ::select-all
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (get-in state [:viewer-data :objects])
|
||||
frame-id (get-in state [:viewer-data :current-frame-id])
|
||||
(let [route (:route state)
|
||||
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
|
||||
(filter #(= (:frame-id (second %)) frame-id))
|
||||
(map first)
|
||||
|
@ -405,18 +390,50 @@
|
|||
(let [toggled? (contains? (get-in state [:viewer-local :collapsed]) 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/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :hover] (when hover? id)))))
|
||||
|
||||
;; --- NAV
|
||||
|
||||
(defn go-to-dashboard
|
||||
([] (go-to-dashboard nil))
|
||||
([{:keys [team-id]}]
|
||||
[]
|
||||
(ptk/reify ::go-to-dashboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (or team-id (get-in state [:viewer-data :project :team-id]))]
|
||||
(rx/of (rt/nav :dashboard-projects {:team-id team-id})))))))
|
||||
(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 _]
|
||||
(let [route (:route state)
|
||||
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)}))))))
|
||||
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
|
@ -37,6 +38,7 @@
|
|||
[app.main.repo :as rp]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.router :as rt]
|
||||
|
@ -48,7 +50,6 @@
|
|||
[potok.core :as ptk]))
|
||||
|
||||
;; (log/set-level! :trace)
|
||||
;; --- Specs
|
||||
|
||||
(s/def ::shape-attrs ::cp/shape-attrs)
|
||||
(s/def ::set-of-string
|
||||
|
@ -87,7 +88,7 @@
|
|||
:snap-grid
|
||||
:dynamic-alignment})
|
||||
|
||||
(def layout-names
|
||||
(def layout-presets
|
||||
{:assets
|
||||
{:del #{:sitemap :layers :document-history }
|
||||
:add #{:assets}}
|
||||
|
@ -121,22 +122,31 @@
|
|||
:picked-color nil
|
||||
:picked-color-select false})
|
||||
|
||||
(declare ensure-layout)
|
||||
|
||||
(defn initialize-layout
|
||||
[layout-name]
|
||||
(us/verify (s/nilable ::us/keyword) layout-name)
|
||||
(ptk/reify ::initialize-layout
|
||||
(defn ensure-layout
|
||||
[lname]
|
||||
(ptk/reify ::ensure-layout
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-layout
|
||||
(fn [layout]
|
||||
(or layout default-layout))))
|
||||
(fn [stored]
|
||||
(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
|
||||
(watch [_ _ _]
|
||||
(if (and layout-name (contains? layout-names layout-name))
|
||||
(rx/of (ensure-layout layout-name))
|
||||
(if (and lname (contains? layout-presets lname))
|
||||
(rx/of (ensure-layout lname))
|
||||
(rx/of (ensure-layout :layers))))))
|
||||
|
||||
(defn initialize-file
|
||||
|
@ -171,7 +181,12 @@
|
|||
(->> stream
|
||||
(rx/filter #(= ::dwc/index-initialized %))
|
||||
(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
|
||||
[{:keys [file users project libraries] :as bundle}]
|
||||
|
@ -219,8 +234,10 @@
|
|||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (dwn/finalize file-id)
|
||||
::dwp/finalize))))
|
||||
(rx/merge
|
||||
(rx/of (dwn/finalize file-id))
|
||||
(->> (rx/of ::dwp/finalize)
|
||||
(rx/observe-on :async))))))
|
||||
|
||||
(defn initialize-page
|
||||
[page-id]
|
||||
|
@ -274,7 +291,7 @@
|
|||
(watch [it state _]
|
||||
(let [pages (get-in state [:workspace-data :pages-index])
|
||||
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
|
||||
:id id
|
||||
|
@ -348,7 +365,6 @@
|
|||
(when (= id (:current-page-id state))
|
||||
go-to-file))))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; WORKSPACE File Actions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -357,6 +373,10 @@
|
|||
[id name]
|
||||
{:pre [(uuid? id) (string? name)]}
|
||||
(ptk/reify ::rename-file
|
||||
IDeref
|
||||
(-deref [_]
|
||||
{::ev/origin "workspace" :id id :name name})
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-file :name] name))
|
||||
|
@ -373,6 +393,9 @@
|
|||
|
||||
;; --- Viewport Sizing
|
||||
|
||||
(declare increase-zoom)
|
||||
(declare decrease-zoom)
|
||||
(declare set-zoom)
|
||||
(declare zoom-to-fit-all)
|
||||
|
||||
(defn initialize-viewport
|
||||
|
@ -457,7 +480,6 @@
|
|||
(update :height #(/ % hprop))
|
||||
(assoc :left-offset left-offset))))))))))))
|
||||
|
||||
|
||||
(defn start-panning []
|
||||
(ptk/reify ::start-panning
|
||||
ptk/WatchEvent
|
||||
|
@ -484,23 +506,32 @@
|
|||
(-> state
|
||||
(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 ensure-layout
|
||||
[layout-name]
|
||||
(assert (contains? layout-names layout-name)
|
||||
(str "unexpected layout name: " layout-name))
|
||||
(ptk/reify ::ensure-layout
|
||||
(defn finish-zooming []
|
||||
(ptk/reify ::finish-zooming
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-layout
|
||||
(fn [stored]
|
||||
(let [todel (get-in layout-names [layout-name :del] #{})
|
||||
toadd (get-in layout-names [layout-name :add] #{})]
|
||||
(-> stored
|
||||
(set/difference todel)
|
||||
(set/union toadd))))))))
|
||||
(-> state
|
||||
(update :workspace-local dissoc :zooming)))))
|
||||
|
||||
|
||||
;; --- Toggle layout flag
|
||||
|
||||
(defn toggle-layout-flags
|
||||
[& flags]
|
||||
|
@ -569,6 +600,16 @@
|
|||
(update state :workspace-local
|
||||
#(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
|
||||
(ptk/reify ::reset-zoom
|
||||
ptk/UpdateEvent
|
||||
|
@ -1058,6 +1099,9 @@
|
|||
:group
|
||||
(rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)])))
|
||||
|
||||
:svg-raw
|
||||
nil
|
||||
|
||||
(rx/of (dwc/start-edition-mode id)
|
||||
(dwdp/start-path-edit id)))))))))
|
||||
|
||||
|
@ -1089,7 +1133,7 @@
|
|||
(defn align-objects
|
||||
[axis]
|
||||
(us/verify ::gal/align-axis axis)
|
||||
(ptk/reify :align-objects
|
||||
(ptk/reify ::align-objects
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
|
@ -1120,7 +1164,7 @@
|
|||
(defn distribute-objects
|
||||
[axis]
|
||||
(us/verify ::gal/dist-axis axis)
|
||||
(ptk/reify :align-objects
|
||||
(ptk/reify ::distribute-objects
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
|
@ -1195,7 +1239,7 @@
|
|||
(rx/of (rt/nav' :workspace pparams qparams))))))
|
||||
([page-id]
|
||||
(us/verify ::us/uuid page-id)
|
||||
(ptk/reify ::go-to-page
|
||||
(ptk/reify ::go-to-page-2
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [project-id (:current-project-id state)
|
||||
|
@ -1207,7 +1251,10 @@
|
|||
(defn go-to-layout
|
||||
[layout]
|
||||
(us/verify ::layout-flag layout)
|
||||
(ptk/reify ::go-to-layout
|
||||
(ptk/reify ::set-workspace-layout
|
||||
IDeref
|
||||
(-deref [_] {:layout layout})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [project-id (get-in state [:workspace-project :id])
|
||||
|
@ -1234,10 +1281,14 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [{:keys [current-file-id current-page-id]} state
|
||||
params {:file-id (or file-id current-file-id)
|
||||
:page-id (or page-id current-page-id)}]
|
||||
pparams {:file-id (or file-id current-file-id)}
|
||||
qparams {:page-id (or page-id current-page-id)
|
||||
:index 0}]
|
||||
(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
|
||||
([] (go-to-dashboard nil))
|
||||
|
@ -1251,7 +1302,7 @@
|
|||
|
||||
(defn go-to-dashboard-fonts
|
||||
[]
|
||||
(ptk/reify ::go-to-dashboard
|
||||
(ptk/reify ::go-to-dashboard-fonts
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
(defn show-palette
|
||||
"Show the palette tool and change the library it uses"
|
||||
[selected]
|
||||
(ptk/reify ::change-palette-selected
|
||||
(ptk/reify ::show-palette
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
(defn center-to-comment-thread
|
||||
[{:keys [position] :as thread}]
|
||||
(us/assert ::dcm/comment-thread thread)
|
||||
(ptk/reify :center-to-comment-thread
|
||||
(ptk/reify ::center-to-comment-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-local
|
||||
|
@ -89,7 +89,7 @@
|
|||
(defn navigate
|
||||
[thread]
|
||||
(us/assert ::dcm/comment-thread thread)
|
||||
(ptk/reify ::navigate
|
||||
(ptk/reify ::open-comment-thread
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [pparams {:project-id (:project-id thread)
|
||||
|
|
|
@ -68,16 +68,14 @@
|
|||
|
||||
(defn generate-unique-name
|
||||
"A unique name generator"
|
||||
([used basename]
|
||||
(generate-unique-name used basename false))
|
||||
([used basename prefix-first?]
|
||||
[used basename]
|
||||
(s/assert ::set-of-string used)
|
||||
(s/assert ::us/string basename)
|
||||
(if-not (contains? used basename)
|
||||
basename
|
||||
(let [[prefix initial] (extract-numeric-suffix basename)]
|
||||
(loop [counter initial]
|
||||
(let [candidate (if (and (= 1 counter) prefix-first?)
|
||||
(str prefix)
|
||||
(str prefix "-" counter))]
|
||||
(let [candidate (str prefix "-" counter)]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc counter))
|
||||
candidate))))))
|
||||
|
@ -144,6 +142,38 @@
|
|||
:origin it
|
||||
: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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
|
||||
(defn remove-frame-grid
|
||||
[frame-id index]
|
||||
(ptk/reify ::set-frame-grid
|
||||
(ptk/reify ::remove-frame-grid
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (dch/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) []))))))))
|
||||
|
|
|
@ -182,7 +182,7 @@
|
|||
shapes (shapes-for-grouping objects selected)]
|
||||
(when-not (empty? shapes)
|
||||
(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
|
||||
:undo-changes uchanges
|
||||
:origin it})
|
||||
|
@ -221,7 +221,7 @@
|
|||
(if (and (= (count shapes) 1)
|
||||
(= (:type (first shapes)) :group))
|
||||
[(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
|
||||
[{:type :mod-obj
|
||||
|
|
|
@ -84,11 +84,14 @@
|
|||
(defn add-color
|
||||
[color]
|
||||
(let [id (uuid/next)
|
||||
color (assoc color
|
||||
:id id
|
||||
:name (default-color-name color))]
|
||||
color (-> color
|
||||
(assoc :id id)
|
||||
(assoc :name (default-color-name color)))]
|
||||
(us/assert ::cp/color color)
|
||||
(ptk/reify ::add-color
|
||||
IDeref
|
||||
(-deref [_] color)
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it _ _]
|
||||
(let [rchg {:type :add-color
|
||||
|
@ -211,6 +214,9 @@
|
|||
(let [typography (update typography :id #(or % (uuid/next)))]
|
||||
(us/assert ::cp/typography typography)
|
||||
(ptk/reify ::add-typography
|
||||
IDeref
|
||||
(-deref [_] typography)
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it _ _]
|
||||
(let [rchg {:type :add-typography
|
||||
|
@ -258,16 +264,19 @@
|
|||
:undo-changes [uchg]
|
||||
:origin it}))))))
|
||||
|
||||
(def add-component
|
||||
"Add a new component to current file library, from the currently selected shapes."
|
||||
(ptk/reify ::add-component
|
||||
|
||||
(defn- add-component2
|
||||
"This is the second step of the component creation."
|
||||
[selected]
|
||||
(ptk/reify ::add-component2
|
||||
IDeref
|
||||
(-deref [_] {:num-shapes (count selected)})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
selected (wsh/lookup-selected state)
|
||||
selected (cp/clean-loops objects selected)
|
||||
shapes (dwg/shapes-for-grouping objects selected)]
|
||||
(when-not (empty? shapes)
|
||||
(let [[group rchanges uchanges]
|
||||
|
@ -278,6 +287,20 @@
|
|||
:origin it})
|
||||
(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
|
||||
"Rename the component with the given id, in the current file library."
|
||||
[id new-name]
|
||||
|
@ -462,6 +485,31 @@
|
|||
:undo-changes uchanges
|
||||
: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
|
||||
[file-id]
|
||||
(us/assert ::us/uuid file-id)
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
(if (and (= (count shapes) 1)
|
||||
(= (:type (first shapes)) :group))
|
||||
[(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]
|
||||
(make-component-shape group objects file-id)
|
||||
|
|
|
@ -59,7 +59,8 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [wsession (get-in state [:ws file-id])
|
||||
stoper (rx/filter #(= ::finalize %) stream)
|
||||
stoper (->> stream
|
||||
(rx/filter (ptk/type? ::finalize)))
|
||||
interval (* 1000 60)]
|
||||
(->> (rx/merge
|
||||
;; Each 60 seconds send a keepalive message for maintain
|
||||
|
@ -106,7 +107,7 @@
|
|||
|
||||
(defn- handle-pointer-send
|
||||
[file-id point]
|
||||
(ptk/reify ::handle-pointer-update
|
||||
(ptk/reify ::handle-pointer-send
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [ws (get-in state [:ws file-id])
|
||||
|
@ -122,11 +123,10 @@
|
|||
(defn finalize
|
||||
[file-id]
|
||||
(ptk/reify ::finalize
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(when-let [ws (get-in state [:ws file-id])]
|
||||
(ws/-close ws))
|
||||
(rx/of ::finalize))))
|
||||
(ws/-close ws)))))
|
||||
|
||||
;; --- Handle: Presence
|
||||
|
||||
|
|
|
@ -179,7 +179,7 @@
|
|||
:right (gpt/point 1 0)))
|
||||
|
||||
(defn finish-move-selected []
|
||||
(ptk/reify ::move-selected
|
||||
(ptk/reify ::finish-move-selected
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (get-in state [:workspace-local :edition])]
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
(defn end-path-event? [event]
|
||||
(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/finalize-page (ptk/type event))
|
||||
(= event :interrupt) ;; ESC
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
;; Shortcuts format https://github.com/ccampbell/mousetrap
|
||||
|
||||
(defn esc-pressed []
|
||||
(ptk/reify :esc-pressed
|
||||
(ptk/reify ::esc-pressed
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
;; Not interrupt when we're editing a path
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
"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"
|
||||
[]
|
||||
(ptk/reify ::add-undo-entry
|
||||
(ptk/reify ::merge-head
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (st/get-path-id state)
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.data.media :as di]
|
||||
[app.main.data.messages :as dm]
|
||||
|
@ -275,6 +276,10 @@
|
|||
[id is-shared]
|
||||
{:pre [(uuid? id) (boolean? is-shared)]}
|
||||
(ptk/reify ::set-file-shared
|
||||
IDeref
|
||||
(-deref [_]
|
||||
{::ev/origin "workspace" :id id :shared is-shared})
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-file :is-shared] is-shared))
|
||||
|
@ -313,7 +318,7 @@
|
|||
|
||||
(defn link-file-to-library
|
||||
[file-id library-id]
|
||||
(ptk/reify ::link-file-to-library
|
||||
(ptk/reify ::attach-library
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1)
|
||||
|
@ -325,7 +330,7 @@
|
|||
|
||||
(defn unlink-file-from-library
|
||||
[file-id library-id]
|
||||
(ptk/reify ::unlink-file-from-library
|
||||
(ptk/reify ::detach-library
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(d/dissoc-in state [:workspace-libraries library-id]))
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
(defn deselect-shape
|
||||
[id]
|
||||
(us/verify ::us/uuid id)
|
||||
(ptk/reify ::select-shape
|
||||
(ptk/reify ::deselect-shape
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :selected] disj id))))
|
||||
|
@ -219,8 +219,6 @@
|
|||
lks/empty-linked-set)
|
||||
selrect (get-in state [:workspace-local :selrect])
|
||||
blocked? (fn [id] (get-in objects [id :blocked] false))]
|
||||
(rx/merge
|
||||
|
||||
(when selrect
|
||||
(->> (uw/ask! {:cmd :selection/query
|
||||
:page-id page-id
|
||||
|
@ -229,7 +227,7 @@
|
|||
:full-frame? true})
|
||||
(rx/map #(cp/clean-loops objects %))
|
||||
(rx/map #(into initial-set (filter (comp not blocked?)) %))
|
||||
(rx/map select-shapes))))))))
|
||||
(rx/map select-shapes)))))))
|
||||
|
||||
(defn select-inside-group
|
||||
[group-id position]
|
||||
|
@ -383,6 +381,53 @@
|
|||
|
||||
(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
|
||||
(ptk/reify ::duplicate-selected
|
||||
ptk/WatchEvent
|
||||
|
@ -390,7 +435,10 @@
|
|||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
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)
|
||||
|
||||
|
@ -400,15 +448,20 @@
|
|||
uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %))
|
||||
(reverse rchanges))
|
||||
|
||||
id-original (when (= (count selected) 1) (first selected))
|
||||
|
||||
selected (->> rchanges
|
||||
(filter #(selected (:old-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
|
||||
:undo-changes uchanges
|
||||
:origin it})
|
||||
(select-shapes selected))))))
|
||||
(select-shapes selected)
|
||||
(memorize-duplicated id-original id-duplicated))))))
|
||||
|
||||
(defn change-hover-state
|
||||
[id value]
|
||||
|
|
|
@ -91,7 +91,11 @@
|
|||
|
||||
:create-component {:tooltip (ds/meta "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")
|
||||
:command "shift+v"
|
||||
|
|
|
@ -95,7 +95,11 @@
|
|||
(d/parse-double))))))
|
||||
|
||||
(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
|
||||
(uc/color? (get-in shape [:svg-attrs :stroke]))
|
||||
(-> (update :svg-attrs dissoc :stroke)
|
||||
|
@ -113,8 +117,16 @@
|
|||
(get-in shape [:svg-attrs :style :stroke-width])
|
||||
(-> (update-in [:svg-attrs :style] dissoc :stroke-width)
|
||||
(assoc :stroke-width (-> (get-in shape [:svg-attrs :style :stroke-width])
|
||||
(d/parse-double)))))]
|
||||
(if (d/any-key? shape :stroke-color :stroke-opacity :stroke-width)
|
||||
(d/parse-double))))
|
||||
|
||||
(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)
|
||||
shape)))
|
||||
|
||||
|
@ -331,7 +343,7 @@
|
|||
(let [{:keys [tag attrs]} element-data
|
||||
attrs (usvg/format-styles 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)
|
||||
references (usvg/find-def-references (:defs svg-data) att-refs)
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
|
@ -222,7 +223,7 @@
|
|||
root
|
||||
transformed-root)))]
|
||||
(reduce set-child
|
||||
(update-in modif-tree [(:id shape) :modifiers] #(merge % modifiers))
|
||||
(assoc-in modif-tree [(:id shape) :modifiers] modifiers)
|
||||
children)))
|
||||
|
||||
(defn- check-delta
|
||||
|
@ -281,7 +282,7 @@
|
|||
(defn start-resize
|
||||
"Enter mouse resize mode, until mouse button is released."
|
||||
[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)
|
||||
{:keys [rotation]} shape
|
||||
rotation (or rotation 0)
|
||||
|
@ -315,17 +316,34 @@
|
|||
|
||||
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-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 (-> (handler-resize-origin (:selrect shape) handler)
|
||||
(gsh/transform-point-center shape-center shape-transform))]
|
||||
origin (cond-> (gsh/transform-point-center origin shape-center shape-transform)
|
||||
(some? displacement)
|
||||
(gpt/add displacement))
|
||||
|
||||
displacement (when (some? displacement)
|
||||
(gmt/translate-matrix displacement))]
|
||||
|
||||
(rx/of (set-modifiers ids
|
||||
{:resize-vector scalev
|
||||
{:displacement displacement
|
||||
:resize-vector scalev
|
||||
:resize-origin origin
|
||||
:resize-transform shape-transform
|
||||
:resize-scale-text scale-text
|
||||
|
@ -334,9 +352,9 @@
|
|||
;; Unifies the instantaneous proportion lock modifier
|
||||
;; activated by Shift key and the shapes own proportion
|
||||
;; 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)]
|
||||
[point (or proportion-lock? shift?)]))]
|
||||
[point (or proportion-lock? shift?) alt?]))]
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
@ -358,9 +376,9 @@
|
|||
(rx/concat
|
||||
(rx/of (dch/update-shapes text-shapes-ids #(assoc % :grow-type :fixed)))
|
||||
(->> 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/switch-map (fn [[point :as current]]
|
||||
(rx/switch-map (fn [[point _ _ :as current]]
|
||||
(->> (snap/closest-snap-point page-id resizing-shapes layout zoom point)
|
||||
(rx/map #(conj current %)))))
|
||||
(rx/mapcat (partial resize shape initial-position layout))
|
||||
|
@ -493,7 +511,7 @@
|
|||
|
||||
(defn- start-move-duplicate
|
||||
[from-position]
|
||||
(ptk/reify ::start-move-selected
|
||||
(ptk/reify ::start-move-duplicate
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(->> stream
|
||||
|
@ -521,10 +539,18 @@
|
|||
layout (get state :workspace-layout)
|
||||
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
|
||||
(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
|
||||
;; We send the nil first so the stream is not waiting for the first value
|
||||
|
|
|
@ -44,7 +44,8 @@
|
|||
|
||||
(defn- calculate-dimensions
|
||||
[{: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))
|
||||
rect (cond->> (gsh/selection-rect shapes)
|
||||
(some? vport)
|
||||
|
@ -131,7 +132,8 @@
|
|||
|
||||
(mf/defc page-svg
|
||||
{::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)
|
||||
root (get objects uuid/zero)
|
||||
shapes
|
||||
|
@ -158,11 +160,12 @@
|
|||
(mf/deps objects)
|
||||
#(shape-wrapper-factory objects))]
|
||||
[:& (mf/provider embed/context) {:value embed?}
|
||||
[:& (mf/provider use/include-metadata-ctx) {:value include-metadata?}
|
||||
[:svg {:view-box vbox
|
||||
: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"
|
||||
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
|
||||
:style {:width "100%"
|
||||
:height "100%"
|
||||
:background background-color}}
|
||||
|
@ -186,7 +189,7 @@
|
|||
:key (:id item)}]
|
||||
:else
|
||||
[:& shape-wrapper {:shape item
|
||||
:key (:id item)}])))]]))
|
||||
:key (:id item)}])))]]]))
|
||||
|
||||
(mf/defc frame-svg
|
||||
{::mf/wrap [mf/memo]}
|
||||
|
@ -197,6 +200,8 @@
|
|||
|
||||
frame-id (:id frame)
|
||||
|
||||
include-metadata? (mf/use-ctx use/include-metadata-ctx)
|
||||
|
||||
modifier-ids (concat [frame-id] (cp/get-children frame-id objects))
|
||||
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
|
||||
objects (reduce update-fn objects modifier-ids)
|
||||
|
@ -214,9 +219,9 @@
|
|||
:width width
|
||||
:height height
|
||||
: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"
|
||||
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")}
|
||||
[:& wrapper {:shape frame :view-box vbox}]]))
|
||||
|
||||
(mf/defc component-svg
|
||||
|
@ -229,6 +234,8 @@
|
|||
|
||||
group-id (:id group)
|
||||
|
||||
include-metadata? (mf/use-ctx use/include-metadata-ctx)
|
||||
|
||||
modifier-ids (concat [group-id] (cp/get-children group-id objects))
|
||||
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
|
||||
objects (reduce update-fn objects modifier-ids)
|
||||
|
@ -246,10 +253,11 @@
|
|||
:width width
|
||||
:height height
|
||||
:version "1.1"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlns:penpot "https://penpot.app/xmlns"}
|
||||
[:& wrapper {:shape group :view-box vbox}]]))
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")}
|
||||
[:> shape-container {:shape group}
|
||||
[:& wrapper {:shape group :view-box vbox}]]]))
|
||||
|
||||
(mf/defc component-symbol
|
||||
[{:keys [id data] :as props}]
|
||||
|
@ -287,20 +295,21 @@
|
|||
|
||||
(let [data (obj/get props "data")
|
||||
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 use/include-metadata-ctx) {:value include-metadata?}
|
||||
[:svg {:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns:penpot "https://penpot.app/xmlns"
|
||||
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
|
||||
:style {:width "100vw"
|
||||
: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}])]
|
||||
|
||||
children]]))
|
||||
children]]]))
|
||||
|
|
|
@ -38,6 +38,9 @@
|
|||
(def threads-ref
|
||||
(l/derived :comment-threads st/state))
|
||||
|
||||
(def share-links
|
||||
(l/derived :share-links st/state))
|
||||
|
||||
;; ---- Dashboard refs
|
||||
|
||||
(def dashboard-local
|
||||
|
@ -110,6 +113,7 @@
|
|||
:edit-path
|
||||
:tooltip
|
||||
:panning
|
||||
:zooming
|
||||
:picking-color?
|
||||
:transform
|
||||
:hover
|
||||
|
@ -286,8 +290,17 @@
|
|||
|
||||
;; ---- Viewer refs
|
||||
|
||||
(def viewer-file
|
||||
(l/derived :viewer-file st/state))
|
||||
|
||||
(def viewer-project
|
||||
(l/derived :viewer-file st/state))
|
||||
|
||||
(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
|
||||
(l/derived :viewer-local st/state))
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
(->> (rx/of data)
|
||||
(rx/map
|
||||
(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)))))))
|
||||
|
||||
(defn render-components
|
||||
|
@ -82,5 +82,5 @@
|
|||
(->> (rx/of data)
|
||||
(rx/map
|
||||
(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))))))))
|
||||
|
|
|
@ -107,6 +107,14 @@
|
|||
:response-type :blob})
|
||||
(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 :update-profile-photo ::multipart-upload)
|
||||
(derive :update-team-photo ::multipart-upload)
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
;; --- User Events
|
||||
|
||||
(defrecord KeyboardEvent [type key shift ctrl alt meta])
|
||||
(defrecord KeyboardEvent [type key shift ctrl alt meta editing])
|
||||
|
||||
(defn keyboard-event?
|
||||
[v]
|
||||
|
@ -137,3 +137,14 @@
|
|||
(rx/dedupe))]
|
||||
(rx/subscribe-with ob 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))
|
||||
|
|
|
@ -20,14 +20,13 @@
|
|||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.cursors :as c]
|
||||
[app.main.ui.dashboard :refer [dashboard]]
|
||||
[app.main.ui.handoff :refer [handoff]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.messages :as msgs]
|
||||
[app.main.ui.onboarding]
|
||||
[app.main.ui.render :as render]
|
||||
[app.main.ui.settings :as settings]
|
||||
[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.util.timers :as ts]
|
||||
[cljs.pprint :refer [pprint]]
|
||||
|
@ -41,25 +40,26 @@
|
|||
|
||||
(s/def ::page-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 ::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/keys :req-un [::index]
|
||||
:opt-un [::token ::section]))
|
||||
:opt-un [::share-id ::section ::page-id]))
|
||||
|
||||
(def routes
|
||||
[["/auth"
|
||||
["/login" :auth-login]
|
||||
(when cf/registration-enabled
|
||||
(when (contains? @cf/flags :registration)
|
||||
["/register" :auth-register])
|
||||
(when cf/registration-enabled
|
||||
(when (contains? @cf/flags :registration)
|
||||
["/register/validate" :auth-register-validate])
|
||||
(when cf/registration-enabled
|
||||
(when (contains? @cf/flags :registration)
|
||||
["/register/success" :auth-register-success])
|
||||
["/recovery/request" :auth-recovery-request]
|
||||
["/recovery" :auth-recovery]
|
||||
|
@ -71,7 +71,7 @@
|
|||
["/feedback" :settings-feedback]
|
||||
["/options" :settings-options]]
|
||||
|
||||
["/view/:file-id/:page-id"
|
||||
["/view/:file-id"
|
||||
{:name :viewer
|
||||
:conform
|
||||
{:path-params ::viewer-path-params
|
||||
|
@ -143,26 +143,21 @@
|
|||
:dashboard-team-settings)
|
||||
[:*
|
||||
#_[: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}]]
|
||||
|
||||
:viewer
|
||||
(let [index (get-in route [:query-params :index])
|
||||
token (get-in route [:query-params :token])
|
||||
section (get-in route [:query-params :section] :interactions)
|
||||
file-id (get-in route [:path-params :file-id])
|
||||
page-id (get-in route [:path-params :page-id])]
|
||||
(let [{:keys [query-params path-params]} route
|
||||
{:keys [index share-id section page-id] :or {section :interactions}} query-params
|
||||
{:keys [file-id]} path-params]
|
||||
[:& fs/fullscreen-wrapper {}
|
||||
(if (= section :handoff)
|
||||
[:& handoff {:page-id page-id
|
||||
:file-id file-id
|
||||
:index index
|
||||
:token token}]
|
||||
[:& viewer-page {:page-id page-id
|
||||
(if (:token query-params)
|
||||
[:& viewer/breaking-change-notice]
|
||||
[:& viewer/viewer-page {:page-id page-id
|
||||
:file-id file-id
|
||||
:section section
|
||||
:index index
|
||||
:token token}])])
|
||||
:share-id share-id}])])
|
||||
|
||||
:render-object
|
||||
(do
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
(ns app.main.ui.auth.login
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.config :as cf]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.repo :as rp]
|
||||
|
@ -23,10 +23,10 @@
|
|||
[rumext.alpha :as mf]))
|
||||
|
||||
(def show-alt-login-buttons?
|
||||
(or cfg/google-client-id
|
||||
cfg/gitlab-client-id
|
||||
cfg/github-client-id
|
||||
cfg/oidc-client-id))
|
||||
(or cf/google-client-id
|
||||
cf/gitlab-client-id
|
||||
cf/github-client-id
|
||||
cf/oidc-client-id))
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
|
@ -97,7 +97,7 @@
|
|||
[:div.fields-row
|
||||
[:& fm/input
|
||||
{:name :email
|
||||
:type "text"
|
||||
:type "email"
|
||||
:tab-index "2"
|
||||
:help-icon i/at
|
||||
:label (tr "auth.email")}]]
|
||||
|
@ -113,7 +113,7 @@
|
|||
[:& fm/submit-button
|
||||
{:label (tr "auth.login-submit")}]
|
||||
|
||||
(when cfg/login-with-ldap
|
||||
(when (contains? @cf/flags :login-with-ldap)
|
||||
[:& fm/submit-button
|
||||
{:label (tr "auth.login-with-ldap-submit")
|
||||
:on-click on-submit-ldap}])]]]))
|
||||
|
@ -121,26 +121,26 @@
|
|||
(mf/defc login-buttons
|
||||
[{:keys [params] :as props}]
|
||||
[:div.auth-buttons
|
||||
(when cfg/google-client-id
|
||||
(when cf/google-client-id
|
||||
[:a.btn-ocean.btn-large.btn-google-auth
|
||||
{:on-click #(login-with-oauth % :google params)}
|
||||
(tr "auth.login-with-google-submit")])
|
||||
|
||||
(when cfg/gitlab-client-id
|
||||
(when cf/gitlab-client-id
|
||||
[:a.btn-ocean.btn-large.btn-gitlab-auth
|
||||
{:on-click #(login-with-oauth % :gitlab params)}
|
||||
[:img.logo
|
||||
{:src "/images/icons/brand-gitlab.svg"}]
|
||||
(tr "auth.login-with-gitlab-submit")])
|
||||
|
||||
(when cfg/github-client-id
|
||||
(when cf/github-client-id
|
||||
[:a.btn-ocean.btn-large.btn-github-auth
|
||||
{:on-click #(login-with-oauth % :github params)}
|
||||
[:img.logo
|
||||
{:src "/images/icons/brand-github.svg"}]
|
||||
(tr "auth.login-with-github-submit")])
|
||||
|
||||
(when cfg/oidc-client-id
|
||||
(when cf/oidc-client-id
|
||||
[:a.btn-ocean.btn-large.btn-github-auth
|
||||
{:on-click #(login-with-oauth % :oidc params)}
|
||||
(tr "auth.login-with-oidc-submit")])])
|
||||
|
@ -166,14 +166,13 @@
|
|||
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))}
|
||||
(tr "auth.forgot-password")]]
|
||||
|
||||
(when cfg/registration-enabled
|
||||
(when (contains? @cf/flags :registration)
|
||||
[:div.link-entry
|
||||
[:span (tr "auth.register") " "]
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth-register {} params))}
|
||||
(tr "auth.register-submit")]])]
|
||||
|
||||
|
||||
(when cfg/allow-demo-users
|
||||
(when (contains? @cf/flags :demo-users)
|
||||
[:div.links.demo
|
||||
[:div.link-entry
|
||||
[:span (tr "auth.create-demo-profile") " "]
|
||||
|
|
|
@ -116,7 +116,7 @@
|
|||
[:h1 (tr "auth.register-title")]
|
||||
[:div.subtitle (tr "auth.register-subtitle")]
|
||||
|
||||
(when cf/demo-warning
|
||||
(when (contains? @cf/flags :demo-warning)
|
||||
[:& demo-warning])
|
||||
|
||||
[:& register-form {:params params}]
|
||||
|
@ -135,7 +135,7 @@
|
|||
:tab-index "4"}
|
||||
(tr "auth.login-here")]]
|
||||
|
||||
(when cf/allow-demo-users
|
||||
(when (contains? @cf/flags :demo-users)
|
||||
[:div.link-entry
|
||||
[:span (tr "auth.create-demo-profile") " "]
|
||||
[:a {:on-click #(st/emit! (du/create-demo-profile))
|
||||
|
@ -216,7 +216,7 @@
|
|||
:label (tr "auth.terms-privacy-agreement")
|
||||
:type "checkbox"}]]
|
||||
|
||||
(when (contains? @cf/flags :show-newsletter-check-on-register-validation)
|
||||
(when (contains? @cf/flags :newsletter-registration-check)
|
||||
[:div.fields-row
|
||||
[:& fm/input {:name :accept-newsletter-subscription
|
||||
:class "check-primary"
|
||||
|
|
|
@ -12,12 +12,14 @@
|
|||
[beicon.core :as rx]
|
||||
[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)]
|
||||
(mf/use-effect
|
||||
(mf/deps @just-copied)
|
||||
(fn []
|
||||
(when @just-copied
|
||||
(when (fn? on-copied)
|
||||
(on-copied))
|
||||
(let [sub (timers/schedule 1000 #(reset! just-copied false))]
|
||||
;; On unmount we dispose the timer
|
||||
#(rx/-dispose sub)))))
|
||||
|
|