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

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

View file

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

View file

@ -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]}]

View file

@ -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

View file

@ -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}}">

View file

@ -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>

View file

@ -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}}

View file

@ -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

View file

@ -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)}]

View file

@ -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 "")

View file

@ -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"

View file

@ -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)

View file

@ -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

View file

@ -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")}
])

View file

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

View file

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

View file

@ -117,14 +117,14 @@
profile-id (or (:profile-id params')
(:profile-id 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 {}))))

View file

@ -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))

View 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}]

View file

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

View file

@ -9,6 +9,7 @@
[app.common.exceptions :as ex]
[app.common.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}]

View file

@ -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]

View file

@ -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

View file

@ -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])))

View file

@ -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})

View file

@ -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)))))
))

View file

@ -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

View file

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

View file

@ -487,6 +487,7 @@
(d/parse-double)
(* (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

View file

@ -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

View file

@ -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}])

View file

@ -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

View file

@ -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; \

View file

@ -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; \

View file

@ -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/*;

View file

@ -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"]}

View file

@ -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"
}
}

View file

@ -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))))))

View file

@ -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))

View file

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

View file

@ -29,7 +29,7 @@
:value token}))
(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")}))

View file

@ -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"}))

View file

@ -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"}))

View file

@ -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"

View file

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

After

Width:  |  Height:  |  Size: 165 B

View file

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

After

Width:  |  Height:  |  Size: 199 B

View file

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

After

Width:  |  Height:  |  Size: 139 B

View file

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

After

Width:  |  Height:  |  Size: 166 B

View file

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

After

Width:  |  Height:  |  Size: 180 B

View file

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

After

Width:  |  Height:  |  Size: 151 B

View file

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

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View file

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

After

Width:  |  Height:  |  Size: 285 B

View file

@ -131,6 +131,24 @@
}
}
.btn-text-dark {
@extend %btn;
background: $color-gray-60;
color: $color-gray-20;
svg {
fill: $color-gray-20;
}
&:hover {
background: $color-primary;
color: $color-gray-60;
svg {
fill: $color-gray-60;
}
}
}
.btn-gray {
@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;

View file

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

View file

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

View file

@ -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%;
}
}
}

View file

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

View file

@ -1316,7 +1316,7 @@
&::after {
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;
}
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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")

View file

@ -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

View file

@ -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

View file

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

View file

@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.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)

View file

@ -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)))

View file

@ -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))))

View file

@ -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)

View file

@ -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)}))))))

View file

@ -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)]

View file

@ -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

View file

@ -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)

View file

@ -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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -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) []))))))))

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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])]

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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]))

View file

@ -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]

View file

@ -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"

View file

@ -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)

View file

@ -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

View file

@ -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]]]))

View file

@ -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))

View file

@ -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))))))))

View file

@ -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)

View file

@ -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))

View file

@ -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

View file

@ -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") " "]

View file

@ -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"

View file

@ -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)))))

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