mirror of
https://github.com/penpot/penpot.git
synced 2025-03-15 17:21:17 -05:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
58905f0b99
95 changed files with 2088 additions and 1836 deletions
|
@ -47,6 +47,11 @@
|
|||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- "Show in exports" is showing in multiselections [Taiga 3194](https://tree.taiga.io/project/penpot/issue/3194)
|
||||
- Fix line gap between shapes [Taiga #3181](https://tree.taiga.io/project/penpot/issue/3181)
|
||||
- Edit file name navigates to the file workspace [Taiga #3183](https://tree.taiga.io/project/penpot/issue/3183)
|
||||
- Fix scroll into view behind fixed element [Taiga #3170](https://tree.taiga.io/project/penpot/issue/3170)
|
||||
- Fix sidebar icon in viewer mode [Taiga #3184](https://tree.taiga.io/project/penpot/issue/3184)
|
||||
- Fix send to back several shapes at a time [Taiga #3077](https://tree.taiga.io/project/penpot/issue/3077)
|
||||
- Fix duplicate multi selected elements [Taiga #3155](https://tree.taiga.io/project/penpot/issue/3155)
|
||||
- Fix add fills to artboard modify children [Taiga #3151](https://tree.taiga.io/project/penpot/issue/3151)
|
||||
|
@ -81,6 +86,8 @@
|
|||
- Fix drag guides to delete target area [#1679](https://github.com/penpot/penpot/issues/1679)
|
||||
- Fix undo when rotating groups [Taiga #3136](https://tree.taiga.io/project/penpot/issue/3136)
|
||||
- Fix component name in sidebar widget [Taiga #3144](https://tree.taiga.io/project/penpot/issue/3144)
|
||||
- Fix resize rotated shape with top&down constraints [Taiga #3167](https://tree.taiga.io/project/penpot/issue/3167)
|
||||
- Fix multi user not working [Taiga #3195](https://tree.taiga.io/project/penpot/issue/3195)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
io.lettuce/lettuce-core {:mvn/version "6.1.6.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/yetti {:git/tag "v9.0" :git/sha "e09e46c"
|
||||
funcool/yetti {:git/tag "v9.1" :git/sha "63f35d9"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
|
@ -42,6 +42,9 @@
|
|||
|
||||
io.sentry/sentry {:mvn/version "5.6.1"}
|
||||
|
||||
dawran6/emoji {:mvn/version "0.1.5"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.11.0"}
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.17.136"}}
|
||||
|
|
|
@ -8,6 +8,7 @@ rm -rf target;
|
|||
mkdir -p target/classes;
|
||||
mkdir -p target/dist;
|
||||
echo "$CURRENT_VERSION" > target/classes/version.txt;
|
||||
cp ../CHANGES.md target/classes/changelog.md;
|
||||
|
||||
clojure -T:build jar;
|
||||
mv target/penpot.jar target/dist/penpot.jar
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
export PENPOT_HOST=devenv
|
||||
export PENPOT_TENANT=dev
|
||||
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-secure-session-cookies enable-audit-log enable-cors enable-transit-readable-response enable-demo-users"
|
||||
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-cors enable-transit-readable-response enable-demo-users"
|
||||
|
||||
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot"
|
||||
# export PENPOT_DATABASE_USERNAME="penpot"
|
||||
|
@ -13,11 +13,15 @@ export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-secure-session-
|
|||
# export PENPOT_DATABASE_USERNAME="penpot_pre"
|
||||
# export PENPOT_DATABASE_PASSWORD="penpot_pre"
|
||||
|
||||
# export PENPOT_LOGGERS_LOKI_URI="http://172.17.0.1:3100/loki/api/v1/push"
|
||||
# export PENPOT_AUDIT_LOG_ARCHIVE_URI="http://localhost:6070/api/audit"
|
||||
|
||||
# Initialize MINIO config
|
||||
# mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin
|
||||
# mc admin user add penpot-s3 penpot-devenv penpot-devenv
|
||||
# mc admin policy set penpot-s3 readwrite user=penpot-devenv
|
||||
# mc mb penpot-s3/penpot -p
|
||||
mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin
|
||||
mc admin user add penpot-s3 penpot-devenv penpot-devenv
|
||||
mc admin policy set penpot-s3 readwrite user=penpot-devenv
|
||||
mc mb penpot-s3/penpot -p
|
||||
|
||||
export AWS_ACCESS_KEY_ID=penpot-devenv
|
||||
export AWS_SECRET_ACCESS_KEY=penpot-devenv
|
||||
export PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
|
||||
|
|
|
@ -41,8 +41,7 @@
|
|||
data))
|
||||
|
||||
(def defaults
|
||||
{:host "devenv"
|
||||
:tenant "dev"
|
||||
{
|
||||
:database-uri "postgresql://postgres/penpot"
|
||||
:database-username "penpot"
|
||||
:database-password "penpot"
|
||||
|
@ -54,8 +53,10 @@
|
|||
:file-change-snapshot-timeout "3h"
|
||||
|
||||
:public-uri "http://localhost:3449"
|
||||
:redis-uri "redis://redis/0"
|
||||
:host "localhost"
|
||||
:tenant "main"
|
||||
|
||||
:redis-uri "redis://redis/0"
|
||||
:srepl-host "127.0.0.1"
|
||||
:srepl-port 6062
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"Main api for send emails."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
|
@ -165,19 +166,25 @@
|
|||
(let [enabled? (or (contains? cf/flags :smtp)
|
||||
(cf/get :smtp-enabled)
|
||||
(:enabled task))]
|
||||
(if enabled?
|
||||
(emails/send! cfg props)
|
||||
(when enabled?
|
||||
(emails/send! cfg props))
|
||||
|
||||
(when (contains? cf/flags :log-emails)
|
||||
(send-console! cfg props)))))
|
||||
|
||||
(defn- send-console!
|
||||
[cfg email]
|
||||
(let [baos (java.io.ByteArrayOutputStream.)
|
||||
mesg (emails/smtp-message cfg email)]
|
||||
(.writeTo mesg baos)
|
||||
(let [out (with-out-str
|
||||
(println "email console dump:")
|
||||
(println "******** start email" (:id email) "**********")
|
||||
(println (.toString baos))
|
||||
(println "******** end email "(:id email) "**********"))]
|
||||
(l/info :email out))))
|
||||
[_ email]
|
||||
(let [body (:body email)
|
||||
out (with-out-str
|
||||
(println "email console dump:")
|
||||
(println "******** start email" (:id email) "**********")
|
||||
(pp/pprint (dissoc email :body))
|
||||
(if (string? body)
|
||||
(println body)
|
||||
(println (->> body
|
||||
(filter #(= "text/plain" (:type %)))
|
||||
(map :content)
|
||||
first)))
|
||||
(println "******** end email" (:id email) "**********"))]
|
||||
(l/info ::l/raw out)))
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
(merge {:name "http"
|
||||
:port 6060
|
||||
:host "0.0.0.0"
|
||||
:max-body-size (* 1024 1024 24) ; 24 MiB
|
||||
:max-body-size (* 1024 1024 30) ; 30 MiB
|
||||
:max-multipart-body-size (* 1024 1024 120)} ; 120 MiB
|
||||
(d/without-nils cfg)))
|
||||
|
||||
|
@ -145,6 +145,7 @@
|
|||
|
||||
["/dbg" {:middleware [(:middleware session)]}
|
||||
["" {:handler (:index debug)}]
|
||||
["/changelog" {:handler (:changelog debug)}]
|
||||
["/error-by-id/:id" {:handler (:retrieve-error debug)}]
|
||||
["/error/:id" {:handler (:retrieve-error debug)}]
|
||||
["/error" {:handler (:retrieve-error-list debug)}]
|
||||
|
|
|
@ -22,8 +22,11 @@
|
|||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs]
|
||||
[emoji.core :as emj]
|
||||
[fipp.edn :as fpp]
|
||||
[integrant.core :as ig]
|
||||
[markdown.core :as md]
|
||||
[markdown.transformers :as mdt]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[yetti.request :as yrq]
|
||||
|
@ -213,6 +216,18 @@
|
|||
(db/exec-one! conn ["select count(*) as count from server_prop;"])
|
||||
(yrs/response 200 "OK")))
|
||||
|
||||
(defn changelog
|
||||
[_ _]
|
||||
(letfn [(transform-emoji [text state]
|
||||
[(emj/emojify text) state])
|
||||
(md->html [text]
|
||||
(md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))]
|
||||
(if-let [clog (io/resource "changelog.md")]
|
||||
(yrs/response :status 200
|
||||
:headers {"content-type" "text/html; charset=utf-8"}
|
||||
:body (-> clog slurp md->html))
|
||||
(yrs/response :status 404 :body "NOT FOUND"))))
|
||||
|
||||
(defn- wrap-async
|
||||
[{:keys [executor] :as cfg} f]
|
||||
(fn [request respond raise]
|
||||
|
@ -230,4 +245,5 @@
|
|||
:retrieve-file-changes (wrap-async cfg retrieve-file-changes)
|
||||
:retrieve-error (wrap-async cfg retrieve-error)
|
||||
:retrieve-error-list (wrap-async cfg retrieve-error-list)
|
||||
:file-data (wrap-async cfg file-data)})
|
||||
:file-data (wrap-async cfg file-data)
|
||||
:changelog (wrap-async cfg changelog)})
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
[yetti.request :as yrq]
|
||||
[yetti.response :as yrs]))
|
||||
|
||||
(def ^:dynamic *context* {})
|
||||
|
||||
(defn- parse-client-ip
|
||||
[request]
|
||||
(or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first)
|
||||
|
@ -24,6 +26,7 @@
|
|||
(defn get-context
|
||||
[request]
|
||||
(merge
|
||||
*context*
|
||||
{:path (:path request)
|
||||
:method (:method request)
|
||||
:params (:params request)
|
||||
|
@ -49,12 +52,20 @@
|
|||
|
||||
(defmethod handle-exception :validation
|
||||
[err _]
|
||||
(let [data (ex-data err)
|
||||
explain (us/pretty-explain data)]
|
||||
(yrs/response :status 400
|
||||
:body (-> data
|
||||
(dissoc ::s/problems ::s/value)
|
||||
(cond-> explain (assoc :explain explain))))))
|
||||
(let [{:keys [code] :as data} (ex-data err)]
|
||||
(cond
|
||||
(= code :spec-validation)
|
||||
(let [explain (us/pretty-explain data)]
|
||||
(yrs/response :status 400
|
||||
:body (-> data
|
||||
(dissoc ::s/problems ::s/value)
|
||||
(cond-> explain (assoc :explain explain)))))
|
||||
|
||||
(= code :request-body-too-large)
|
||||
(yrs/response :status 413 :body data)
|
||||
|
||||
:else
|
||||
(yrs/response :status 400 :body data))))
|
||||
|
||||
(defmethod handle-exception :assertion
|
||||
[error request]
|
||||
|
@ -129,9 +140,21 @@
|
|||
:code :unhandled
|
||||
:hint (ex-message error)
|
||||
:data edata})))))
|
||||
|
||||
(defn handle
|
||||
[error request]
|
||||
(if (or (instance? java.util.concurrent.CompletionException error)
|
||||
(instance? java.util.concurrent.ExecutionException error))
|
||||
(handle-exception (.getCause ^Throwable error) request)
|
||||
(handle-exception error request)))
|
||||
[cause request]
|
||||
|
||||
(cond
|
||||
(or (instance? java.util.concurrent.CompletionException cause)
|
||||
(instance? java.util.concurrent.ExecutionException cause))
|
||||
(handle-exception (.getCause ^Throwable cause) request)
|
||||
|
||||
|
||||
(ex/wrapped? cause)
|
||||
(let [context (meta cause)
|
||||
cause (deref cause)]
|
||||
(binding [*context* context]
|
||||
(handle-exception cause request)))
|
||||
|
||||
:else
|
||||
(handle-exception cause request)))
|
||||
|
|
|
@ -16,7 +16,10 @@
|
|||
[yetti.middleware :as ymw]
|
||||
[yetti.request :as yrq]
|
||||
[yetti.response :as yrs])
|
||||
(:import java.io.OutputStream))
|
||||
(:import
|
||||
com.fasterxml.jackson.core.io.JsonEOFException
|
||||
io.undertow.server.RequestTooBigException
|
||||
java.io.OutputStream))
|
||||
|
||||
(def server-timing
|
||||
{:name ::server-timing
|
||||
|
@ -46,16 +49,29 @@
|
|||
(update :params merge params))))
|
||||
|
||||
:else
|
||||
request)))]
|
||||
request)))
|
||||
|
||||
(handle-error [raise cause]
|
||||
(cond
|
||||
(instance? RequestTooBigException cause)
|
||||
(raise (ex/error :type :validation
|
||||
:code :request-body-too-large
|
||||
:hint (ex-message cause)))
|
||||
|
||||
(instance? JsonEOFException cause)
|
||||
(raise (ex/error :type :validation
|
||||
:code :malformed-json
|
||||
:hint (ex-message cause)))
|
||||
:else
|
||||
(raise cause)))]
|
||||
|
||||
(fn [request respond raise]
|
||||
(when-let [request (try
|
||||
(process-request request)
|
||||
(catch Exception cause
|
||||
(raise (ex/error :type :validation
|
||||
:code :malformed-params
|
||||
:hint (ex-message cause)
|
||||
:cause cause))))]
|
||||
(catch RuntimeException cause
|
||||
(handle-error raise (or (.getCause cause) cause)))
|
||||
(catch Throwable cause
|
||||
(handle-error raise cause)))]
|
||||
(handler request respond raise)))))
|
||||
|
||||
(def parse-request
|
||||
|
@ -99,7 +115,10 @@
|
|||
(let [body (yrs/body response)]
|
||||
(if (coll? body)
|
||||
(let [qs (yrq/query request)
|
||||
opts {:type (if (str/includes? qs "verbose") :json-verbose :json)}]
|
||||
opts (if (or (contains? cf/flags :transit-readable-response)
|
||||
(str/includes? qs "transit_verbose"))
|
||||
{:type :json-verbose}
|
||||
{:type :json})]
|
||||
(-> response
|
||||
(update :headers assoc "content-type" "application/transit+json")
|
||||
(assoc :body (transit-streamable-body body opts))))
|
||||
|
|
|
@ -166,11 +166,9 @@
|
|||
(a/go
|
||||
;; Only allow receive pointer updates when active subscription
|
||||
(when-let [{:keys [topic]} (get-in @wsp [::subscriptions subs-id])]
|
||||
(l/trace :fn "handle-message" :event :pointer-update :message message)
|
||||
(let [msgbus-fn (:msgbus @wsp)
|
||||
profile-id (::profile-id @wsp)
|
||||
session-id (::session-id @wsp)
|
||||
|
||||
message (-> message
|
||||
(dissoc :subs-id)
|
||||
(assoc :profile-id profile-id)
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
:read-only (cf/get :database-readonly false)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:migrations (ig/ref :app.migrations/all)
|
||||
:name :main
|
||||
:min-size (cf/get :database-min-pool-size 0)
|
||||
:max-size (cf/get :database-max-pool-size 30)}
|
||||
:name :main
|
||||
:min-size (cf/get :database-min-pool-size 0)
|
||||
:max-size (cf/get :database-max-pool-size 30)}
|
||||
|
||||
;; Default thread pool for IO operations
|
||||
[::default :app.worker/executor]
|
||||
|
@ -115,7 +115,9 @@
|
|||
:router (ig/ref :app.http/router)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:executor (ig/ref [::default :app.worker/executor])
|
||||
:io-threads (cf/get :http-server-io-threads)}
|
||||
:io-threads (cf/get :http-server-io-threads)
|
||||
:max-body-size (cf/get :http-server-max-body-size)
|
||||
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
||||
|
||||
:app.http/router
|
||||
{:assets (ig/ref :app.http.assets/handlers)
|
||||
|
|
|
@ -217,6 +217,12 @@
|
|||
|
||||
{:name "0069-add-file-thumbnail-table"
|
||||
:fn (mg/resource "app/migrations/sql/0069-add-file-thumbnail-table.sql")}
|
||||
|
||||
{:name "0070-del-frame-thumbnail-table"
|
||||
:fn (mg/resource "app/migrations/sql/0070-del-frame-thumbnail-table.sql")}
|
||||
|
||||
{:name "0071-add-file-object-thumbnail-table"
|
||||
:fn (mg/resource "app/migrations/sql/0071-add-file-object-thumbnail-table.sql")}
|
||||
])
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE file_frame_thumbnail;
|
|
@ -0,0 +1,11 @@
|
|||
CREATE TABLE file_object_thumbnail (
|
||||
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
|
||||
object_id uuid NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
data text NULL,
|
||||
|
||||
PRIMARY KEY(file_id, object_id)
|
||||
);
|
||||
|
||||
ALTER TABLE file_object_thumbnail
|
||||
ALTER COLUMN data SET STORAGE external;
|
|
@ -59,7 +59,9 @@
|
|||
(-> (method data)
|
||||
(p/then handle-response)
|
||||
(p/then respond)
|
||||
(p/catch raise)))))
|
||||
(p/catch (fn [cause]
|
||||
(let [context {:profile-id profile-id}]
|
||||
(raise (ex/wrap-with-context cause context)))))))))
|
||||
|
||||
(defn- rpc-mutation-handler
|
||||
"Ring handler that dispatches mutation requests and convert between
|
||||
|
@ -81,7 +83,9 @@
|
|||
(-> (method data)
|
||||
(p/then handle-response)
|
||||
(p/then respond)
|
||||
(p/catch raise)))))
|
||||
(p/catch (fn [cause]
|
||||
(let [context {:profile-id profile-id}]
|
||||
(raise (ex/wrap-with-context cause context)))))))))
|
||||
|
||||
(defn- wrap-metrics
|
||||
"Wrap service method with metrics measurement."
|
||||
|
|
|
@ -476,30 +476,31 @@
|
|||
:revn revn
|
||||
:data (blob/encode data)}
|
||||
{:id id})))
|
||||
|
||||
nil)))
|
||||
|
||||
;; --- Mutation: upsert object thumbnail
|
||||
|
||||
;; --- Mutation: Upsert frame thumbnail
|
||||
|
||||
(def sql:upsert-frame-thumbnail
|
||||
"insert into file_frame_thumbnail(file_id, frame_id, data)
|
||||
(def sql:upsert-object-thumbnail
|
||||
"insert into file_object_thumbnail(file_id, object_id, data)
|
||||
values (?, ?, ?)
|
||||
on conflict(file_id, frame_id) do
|
||||
on conflict(file_id, object_id) do
|
||||
update set data = ?;")
|
||||
|
||||
(s/def ::data ::us/string)
|
||||
(s/def ::upsert-file-frame-thumbnail
|
||||
(s/keys :req-un [::profile-id ::file-id ::frame-id ::data]))
|
||||
(s/def ::data (s/nilable ::us/string))
|
||||
(s/def ::object-id ::us/uuid)
|
||||
(s/def ::upsert-file-object-thumbnail
|
||||
(s/keys :req-un [::profile-id ::file-id ::object-id ::data]))
|
||||
|
||||
(sv/defmethod ::upsert-file-frame-thumbnail
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id frame-id data]}]
|
||||
(sv/defmethod ::upsert-file-object-thumbnail
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id object-id data]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(db/exec-one! conn [sql:upsert-frame-thumbnail file-id frame-id data data])
|
||||
(if data
|
||||
(db/exec-one! conn [sql:upsert-object-thumbnail file-id object-id data data])
|
||||
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id}))
|
||||
nil))
|
||||
|
||||
;; --- Mutation: Upsert file thumbnail
|
||||
;; --- Mutation: upsert file thumbnail
|
||||
|
||||
(def sql:upsert-file-thumbnail
|
||||
"insert into file_thumbnail (file_id, revn, data, props)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
|
@ -412,6 +413,9 @@
|
|||
{:iss :profile-identity
|
||||
:profile-id (:id profile)})]
|
||||
|
||||
(when (contains? cf/flags :log-invitation-tokens)
|
||||
(l/trace :hint "invitation token" :token itoken))
|
||||
|
||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||
(ex/raise :type :validation
|
||||
:code :member-is-muted
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
(ns app.rpc.queries.files
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.rpc.helpers :as rpch]
|
||||
|
@ -21,7 +21,8 @@
|
|||
[app.rpc.queries.teams :as teams]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(declare decode-row)
|
||||
(declare decode-row-xf)
|
||||
|
@ -187,12 +188,30 @@
|
|||
|
||||
;; --- Query: File (By ID)
|
||||
|
||||
(defn retrieve-object-thumbnails
|
||||
([{:keys [pool]} file-id]
|
||||
(let [sql (str/concat
|
||||
"select object_id, data "
|
||||
" from file_object_thumbnail"
|
||||
" where file_id=?")]
|
||||
(->> (db/exec! pool [sql file-id])
|
||||
(d/index-by :object-id :data))))
|
||||
|
||||
([{:keys [pool]} file-id frame-ids]
|
||||
(with-open [conn (db/open pool)]
|
||||
(let [sql (str/concat
|
||||
"select object_id, data "
|
||||
" from file_object_thumbnail"
|
||||
" where file_id=? and object_id = ANY(?)")
|
||||
ids (db/create-array conn "uuid" (seq frame-ids))]
|
||||
(->> (db/exec! conn [sql file-id ids])
|
||||
(d/index-by :object-id :data))))))
|
||||
|
||||
(defn retrieve-file
|
||||
[{:keys [pool] :as cfg} id]
|
||||
(let [item (db/get-by-id pool :file id)]
|
||||
(->> item
|
||||
(decode-row)
|
||||
(pmg/migrate-file))))
|
||||
(->> (db/get-by-id pool :file id)
|
||||
(decode-row)
|
||||
(pmg/migrate-file)))
|
||||
|
||||
(s/def ::file
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
@ -202,133 +221,135 @@
|
|||
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
|
||||
(let [perms (get-permissions pool profile-id id)]
|
||||
(check-read-permissions! perms)
|
||||
(-> (retrieve-file cfg id)
|
||||
(assoc :permissions perms))))
|
||||
(let [file (retrieve-file cfg id)
|
||||
thumbs (retrieve-object-thumbnails cfg id)]
|
||||
(-> file
|
||||
(assoc :thumbnails thumbs)
|
||||
(assoc :permissions perms)))))
|
||||
|
||||
(declare trim-file-data)
|
||||
|
||||
;; --- QUERY: page
|
||||
|
||||
(defn- prune-objects
|
||||
"Given the page data and the object-id returns the page data with all
|
||||
other not needed objects removed from the `:objects` data
|
||||
structure."
|
||||
[{:keys [objects] :as page} object-id]
|
||||
(let [objects (cph/get-children-with-self objects object-id)]
|
||||
(assoc page :objects (d/index-by :id objects))))
|
||||
|
||||
(defn- prune-thumbnails
|
||||
"Given the page data, removes the `:thumbnail` prop from all
|
||||
shapes."
|
||||
[page]
|
||||
(update page :objects d/update-vals #(dissoc % :thumbnail)))
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::object-id ::us/uuid)
|
||||
|
||||
(s/def ::trimmed-file
|
||||
(s/keys :req-un [::profile-id ::id ::object-id ::page-id]))
|
||||
|
||||
(sv/defmethod ::trimmed-file
|
||||
"Retrieve a file by its ID and trims all unnecesary content from
|
||||
it. It is mainly used for rendering a concrete object, so we don't
|
||||
need force download all shapes when only a small subset is
|
||||
necesseary."
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
|
||||
(let [perms (get-permissions pool profile-id id)]
|
||||
(check-read-permissions! perms)
|
||||
(-> (retrieve-file cfg id)
|
||||
(trim-file-data params)
|
||||
(assoc :permissions perms))))
|
||||
|
||||
(defn- trim-file-data
|
||||
[file {:keys [page-id object-id]}]
|
||||
(let [page (get-in file [:data :pages-index page-id])
|
||||
objects (->> (cph/get-children-with-self (:objects page) object-id)
|
||||
(map #(dissoc % :thumbnail))
|
||||
(d/index-by :id))
|
||||
page (assoc page :objects objects)]
|
||||
(-> file
|
||||
(update :data assoc :pages-index {page-id page})
|
||||
(update :data assoc :pages [page-id]))))
|
||||
|
||||
;; --- FILE THUMBNAIL
|
||||
|
||||
(declare strip-frames-with-thumbnails)
|
||||
(declare extract-file-thumbnail)
|
||||
(declare get-first-page-data)
|
||||
(declare get-thumbnail-data)
|
||||
|
||||
(s/def ::strip-frames-with-thumbnails ::us/boolean)
|
||||
|
||||
(s/def ::page
|
||||
(s/keys :req-un [::profile-id ::file-id]
|
||||
:opt-un [::strip-frames-with-thumbnails]))
|
||||
(s/and
|
||||
(s/keys :req-un [::profile-id ::file-id]
|
||||
:opt-un [::page-id ::object-id])
|
||||
(fn [obj]
|
||||
(if (contains? obj :object-id)
|
||||
(contains? obj :page-id)
|
||||
true))))
|
||||
|
||||
(sv/defmethod ::page
|
||||
"Retrieves the first page of the file. Used mainly for render
|
||||
thumbnails on dashboard.
|
||||
"Retrieves the page data from file and returns it. If no page-id is
|
||||
specified, the first page will be returned. If object-id is
|
||||
specified, only that object and its children will be returned in the
|
||||
page objects data structure.
|
||||
|
||||
DEPRECATED: still here for backward compatibility."
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}]
|
||||
If you specify the object-id, the page-id parameter becomes
|
||||
mandatory.
|
||||
|
||||
Mainly used for rendering purposes."
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id page-id object-id] :as props}]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(let [file (retrieve-file cfg file-id)
|
||||
data (get-first-page-data file props)]
|
||||
data))
|
||||
(let [file (retrieve-file cfg file-id)
|
||||
page-id (or page-id (-> file :data :pages first))
|
||||
page (get-in file [:data :pages-index page-id])]
|
||||
|
||||
(cond-> (prune-thumbnails page)
|
||||
(uuid? object-id)
|
||||
(prune-objects object-id))))
|
||||
|
||||
;; --- QUERY: file-data-for-thumbnail
|
||||
|
||||
(defn- get-file-thumbnail-data
|
||||
[cfg {:keys [data id] :as file}]
|
||||
(letfn [;; function responsible on finding the frame marked to be
|
||||
;; used as thumbnail; the returned frame always have
|
||||
;; the :page-id set to the page that it belongs.
|
||||
(get-thumbnail-frame [data]
|
||||
(d/seek :use-for-thumbnail?
|
||||
(for [page (-> data :pages-index vals)
|
||||
frame (-> page :objects cph/get-frames)]
|
||||
(assoc frame :page-id (:id page)))))
|
||||
|
||||
;; function responsible to filter objects data strucuture of
|
||||
;; all unneded shapes if a concrete frame is provided. If no
|
||||
;; frame, the objects is returned untouched.
|
||||
(filter-objects [objects frame-id]
|
||||
(d/index-by :id (cph/get-children-with-self objects frame-id)))
|
||||
|
||||
;; function responsible of assoc available thumbnails
|
||||
;; to frames and remove all children shapes from objects if
|
||||
;; thumbnails is available
|
||||
(assoc-thumbnails [objects thumbnails]
|
||||
(loop [objects objects
|
||||
frames (filter cph/frame-shape? (vals objects))]
|
||||
|
||||
(if-let [{:keys [id] :as frame} (first frames)]
|
||||
(let [frame (if-let [thumb (get thumbnails id)]
|
||||
(assoc frame :thumbnail thumb :shapes [])
|
||||
(dissoc frame :thumbnail))]
|
||||
(if (:thumbnail frame)
|
||||
(recur (-> (assoc objects id frame)
|
||||
(d/without-keys (cph/get-children-ids objects id)))
|
||||
(rest frames))
|
||||
(recur (assoc objects id frame)
|
||||
(rest frames))))
|
||||
|
||||
objects)))]
|
||||
|
||||
(let [frame (get-thumbnail-frame data)
|
||||
frame-id (:id frame)
|
||||
page-id (or (:page-id frame)
|
||||
(-> data :pages first))
|
||||
page (dm/get-in data [:pages-index page-id])
|
||||
|
||||
obj-ids (or (some-> frame-id list)
|
||||
(map :id (cph/get-frames page)))
|
||||
thumbs (retrieve-object-thumbnails cfg id obj-ids)]
|
||||
|
||||
(cond-> page
|
||||
;; If we have frame, we need to specify it on the page level
|
||||
;; and remove the all other unrelated objects.
|
||||
(some? frame-id)
|
||||
(-> (assoc :thumbnail-frame-id frame-id)
|
||||
(update :objects filter-objects frame-id))
|
||||
|
||||
;; Assoc the available thumbnails and prune not visible shapes
|
||||
;; for avoid transfer unnecesary data.
|
||||
:always
|
||||
(update :objects assoc-thumbnails thumbs)))))
|
||||
|
||||
(s/def ::file-data-for-thumbnail
|
||||
(s/keys :req-un [::profile-id ::file-id]
|
||||
:opt-un [::strip-frames-with-thumbnails]))
|
||||
(s/keys :req-un [::profile-id ::file-id]))
|
||||
|
||||
(sv/defmethod ::file-data-for-thumbnail
|
||||
"Retrieves the data for generate the thumbnail of the file. Used mainly for render
|
||||
thumbnails on dashboard."
|
||||
"Retrieves the data for generate the thumbnail of the file. Used
|
||||
mainly for render thumbnails on dashboard."
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(let [file (retrieve-file cfg file-id)]
|
||||
{:data (get-thumbnail-data file props)
|
||||
:file-id file-id
|
||||
:revn (:revn file)}))
|
||||
{:file-id file-id
|
||||
:revn (:revn file)
|
||||
:page (get-file-thumbnail-data cfg file)}))
|
||||
|
||||
(defn get-thumbnail-data
|
||||
[{:keys [data] :as file} props]
|
||||
(if-let [[page frame] (first
|
||||
(for [page (-> data :pages-index vals)
|
||||
frame (-> page :objects cph/get-frames)
|
||||
:when (:file-thumbnail frame)]
|
||||
[page frame]))]
|
||||
(let [objects (->> (cph/get-children-with-self (:objects page) (:id frame))
|
||||
(d/index-by :id))]
|
||||
(cond-> (assoc page :objects objects)
|
||||
(:strip-frames-with-thumbnails props)
|
||||
(strip-frames-with-thumbnails)
|
||||
|
||||
:always
|
||||
(assoc :thumbnail-frame frame)))
|
||||
|
||||
(let [page-id (-> data :pages first)]
|
||||
(cond-> (get-in data [:pages-index page-id])
|
||||
(:strip-frames-with-thumbnails props)
|
||||
(strip-frames-with-thumbnails)))))
|
||||
|
||||
(defn get-first-page-data
|
||||
[file props]
|
||||
(let [page-id (get-in file [:data :pages 0])
|
||||
data (cond-> (get-in file [:data :pages-index page-id])
|
||||
(true? (:strip-frames-with-thumbnails props))
|
||||
(strip-frames-with-thumbnails))]
|
||||
data))
|
||||
|
||||
(defn strip-frames-with-thumbnails
|
||||
"Remove unnecesary shapes from frames that have thumbnail."
|
||||
[data]
|
||||
(let [filter-shape?
|
||||
(fn [objects [id shape]]
|
||||
(let [frame-id (:frame-id shape)]
|
||||
(or (= id uuid/zero)
|
||||
(= frame-id uuid/zero)
|
||||
(not (some? (get-in objects [frame-id :thumbnail]))))))
|
||||
|
||||
;; We need to remove from the attribute :shapes its children because
|
||||
;; they will not be sent in the data
|
||||
remove-frame-children
|
||||
(fn [[id shape]]
|
||||
[id (cond-> shape
|
||||
(some? (:thumbnail shape))
|
||||
(assoc :shapes []))])
|
||||
|
||||
update-objects
|
||||
(fn [objects]
|
||||
(into {}
|
||||
(comp (map remove-frame-children)
|
||||
(filter (partial filter-shape? objects)))
|
||||
objects))]
|
||||
|
||||
(update data :objects update-objects)))
|
||||
|
||||
;; --- Query: Shared Library Files
|
||||
|
||||
|
@ -428,20 +449,6 @@
|
|||
(teams/check-read-permissions! pool profile-id team-id)
|
||||
(db/exec! pool [sql:team-recent-files team-id]))
|
||||
|
||||
;; --- QUERY: get all file frame thumbnails
|
||||
|
||||
(s/def ::file-frame-thumbnails
|
||||
(s/keys :req-un [::profile-id ::file-id]
|
||||
:opt-un [::frame-id]))
|
||||
|
||||
(sv/defmethod ::file-frame-thumbnails
|
||||
[{:keys [pool]} {:keys [profile-id file-id frame-id]}]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(let [params (cond-> {:file-id file-id}
|
||||
frame-id (assoc :frame-id frame-id))
|
||||
rows (db/query pool :file-frame-thumbnail params)]
|
||||
(d/index-by :frame-id :data rows)))
|
||||
|
||||
;; --- QUERY: get file thumbnail
|
||||
|
||||
(s/def ::revn ::us/integer)
|
||||
|
|
|
@ -6,18 +6,19 @@
|
|||
|
||||
(ns app.tasks.file-gc
|
||||
"A maintenance task that is responsible of: purge unused file media,
|
||||
clean unused frame thumbnails and remove old file thumbnails. The
|
||||
clean unused object thumbnails and remove old file thumbnails. The
|
||||
file is eligible to be garbage collected after some period of
|
||||
inactivity (the default threshold is 72h)."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.db :as db]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.time :as dt]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(declare ^:private retrieve-candidates)
|
||||
|
@ -117,26 +118,26 @@
|
|||
;; them.
|
||||
(db/delete! conn :file-media-object {:id (:id mobj)}))))
|
||||
|
||||
(defn- collect-frames
|
||||
[data]
|
||||
(let [xform (comp
|
||||
(map :objects)
|
||||
(mapcat vals)
|
||||
(filter cph/frame-shape?)
|
||||
(keep :id))
|
||||
pages (concat
|
||||
(vals (:pages-index data))
|
||||
(vals (:components data)))]
|
||||
(into #{} xform pages)))
|
||||
|
||||
(defn- clean-file-frame-thumbnails!
|
||||
[conn file-id data]
|
||||
(let [sql (str "delete from file_frame_thumbnail "
|
||||
" where file_id=? and not (frame_id=ANY(?))")
|
||||
ids (->> (collect-frames data)
|
||||
(db/create-array conn "uuid"))
|
||||
res (db/exec-one! conn [sql file-id ids])]
|
||||
(l/debug :hint "delete frame thumbnails" :total (:next.jdbc/update-count res))))
|
||||
(let [stored (->> (db/query conn :file-object-thumbnail
|
||||
{:file-id file-id}
|
||||
{:columns [:object-id]})
|
||||
(into #{} (map :object-id)))
|
||||
|
||||
using (->> (concat (vals (:pages-index data))
|
||||
(vals (:components data)))
|
||||
(into #{} (comp (map :objects)
|
||||
(mapcat keys))))
|
||||
|
||||
unused (set/difference stored using)]
|
||||
|
||||
(when (seq unused)
|
||||
(let [sql (str/concat
|
||||
"delete from file_object_thumbnail "
|
||||
" where file_id=? and object_id=ANY(?)")
|
||||
res (db/exec-one! conn [sql file-id (db/create-array conn "uuid" unused)])]
|
||||
(l/debug :hint "delete object thumbnails" :total (:next.jdbc/update-count res))))))
|
||||
|
||||
(defn- clean-file-thumbnails!
|
||||
[conn file-id revn]
|
||||
|
|
|
@ -413,75 +413,217 @@
|
|||
(t/is (= (:type error-data) :not-found))))
|
||||
))
|
||||
|
||||
(t/deftest query-frame-thumbnails
|
||||
|
||||
(t/deftest object-thumbnails-ops
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
:project-id (:default-project-id prof)
|
||||
:is-shared false})
|
||||
data {::th/type :file-frame-thumbnails
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:frame-id (uuid/next)}]
|
||||
page-id (get-in file [:data :pages 0])
|
||||
frame1-id (uuid/next)
|
||||
shape1-id (uuid/next)
|
||||
frame2-id (uuid/next)
|
||||
shape2-id (uuid/next)
|
||||
|
||||
;; insert an entry on the database with a test value for the thumbnail of this frame
|
||||
(th/db-insert! :file-frame-thumbnail
|
||||
{:file-id (:file-id data)
|
||||
:frame-id (:frame-id data)
|
||||
:data "testvalue"})
|
||||
changes [{:type :add-obj
|
||||
:page-id page-id
|
||||
:id frame1-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj {:id frame1-id
|
||||
:use-for-thumbnail? true
|
||||
:name "test-frame1"
|
||||
:type :frame}}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape1-id
|
||||
:parent-id frame1-id
|
||||
:frame-id frame1-id
|
||||
:obj {:id shape1-id
|
||||
:name "test-shape1"
|
||||
:type :rect}}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id frame2-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj {:id frame2-id
|
||||
:name "test-frame2"
|
||||
:type :frame}}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape2-id
|
||||
:parent-id frame2-id
|
||||
:frame-id frame2-id
|
||||
:obj {:id shape2-id
|
||||
:name "test-shape2"
|
||||
:type :rect}}]]
|
||||
;; Update the file
|
||||
(th/update-file* {:file-id (:id file)
|
||||
:profile-id (:id prof)
|
||||
:revn 0
|
||||
:changes changes})
|
||||
|
||||
(let [{:keys [result error] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (= 1 (count result)))
|
||||
(t/is (= "testvalue" (get result (:frame-id data)))))))
|
||||
(t/testing "RPC page query (rendering purposes)"
|
||||
|
||||
(t/deftest insert-frame-thumbnails
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
:project-id (:default-project-id prof)
|
||||
:is-shared false})
|
||||
data {::th/type :upsert-file-frame-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:frame-id (uuid/next)
|
||||
:data "test insert new value"}]
|
||||
;; Query :page RPC method without passing page-id
|
||||
(let [data {::th/type :page
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)}
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
|
||||
(let [out (th/mutation! data)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:result out)))
|
||||
(let [[result] (th/db-query :file-frame-thumbnail
|
||||
{:file-id (:file-id data)
|
||||
:frame-id (:frame-id data)})]
|
||||
(t/is (= "test insert new value" (:data result)))))))
|
||||
;; (th/print-result! out)
|
||||
(t/is (map? result))
|
||||
(t/is (contains? result :objects))
|
||||
(t/is (contains? (:objects result) frame1-id))
|
||||
(t/is (contains? (:objects result) shape1-id))
|
||||
(t/is (contains? (:objects result) frame2-id))
|
||||
(t/is (contains? (:objects result) shape2-id))
|
||||
(t/is (contains? (:objects result) uuid/zero)))
|
||||
|
||||
(t/deftest upsert-frame-thumbnails
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
:project-id (:default-project-id prof)
|
||||
:is-shared false})
|
||||
data {::th/type :upsert-file-frame-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:frame-id (uuid/next)
|
||||
:data "updated value"}]
|
||||
;; Query :page RPC method with page-id
|
||||
(let [data {::th/type :page
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:page-id page-id}
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (map? result))
|
||||
(t/is (contains? result :objects))
|
||||
(t/is (contains? (:objects result) frame1-id))
|
||||
(t/is (contains? (:objects result) shape1-id))
|
||||
(t/is (contains? (:objects result) frame2-id))
|
||||
(t/is (contains? (:objects result) shape2-id))
|
||||
(t/is (contains? (:objects result) uuid/zero)))
|
||||
|
||||
;; insert an entry on the database with and old value for the thumbnail of this frame
|
||||
(th/db-insert! :file-frame-thumbnail
|
||||
{:file-id (:file-id data)
|
||||
:frame-id (:frame-id data)
|
||||
:data "old value"})
|
||||
;; Query :page RPC method with page-id and object-id
|
||||
(let [data {::th/type :page
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:page-id page-id
|
||||
:object-id frame1-id}
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (map? result))
|
||||
(t/is (contains? result :objects))
|
||||
(t/is (contains? (:objects result) frame1-id))
|
||||
(t/is (contains? (:objects result) shape1-id))
|
||||
(t/is (not (contains? (:objects result) uuid/zero)))
|
||||
(t/is (not (contains? (:objects result) frame2-id)))
|
||||
(t/is (not (contains? (:objects result) shape2-id))))
|
||||
|
||||
(let [out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
;; Query :page RPC method with wrong params
|
||||
(let [data {::th/type :page
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:object-id frame1-id}
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (= :validation (th/ex-type error)))
|
||||
(t/is (= :spec-validation (th/ex-code error)))))
|
||||
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:result out)))
|
||||
(t/testing "RPC :file-data-for-thumbnail"
|
||||
;; Insert a thumbnail data for the frame-id
|
||||
(let [data {::th/type :upsert-file-object-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:object-id frame1-id
|
||||
:data "random-data-1"}
|
||||
|
||||
;; retrieve the value from the database and check its content
|
||||
(let [[result] (th/db-query :file-frame-thumbnail
|
||||
{:file-id (:file-id data)
|
||||
:frame-id (:frame-id data)})]
|
||||
(t/is (= "updated value" (:data result)))))))
|
||||
{:keys [error result] :as out} (th/mutation! data)]
|
||||
(t/is (nil? error))
|
||||
(t/is (nil? result)))
|
||||
|
||||
;; Check the result
|
||||
(let [data {::th/type :file-data-for-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)}
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (map? result))
|
||||
(t/is (contains? result :page))
|
||||
(t/is (contains? result :revn))
|
||||
(t/is (contains? result :file-id))
|
||||
|
||||
(t/is (= (:id file) (:file-id result)))
|
||||
(t/is (= "random-data-1" (get-in result [:page :objects frame1-id :thumbnail])))
|
||||
(t/is (= [] (get-in result [:page :objects frame1-id :shapes]))))
|
||||
|
||||
;; Delete thumbnail data
|
||||
(let [data {::th/type :upsert-file-object-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:object-id frame1-id
|
||||
:data nil}
|
||||
{:keys [error result] :as out} (th/mutation! data)]
|
||||
(t/is (nil? error))
|
||||
(t/is (nil? result)))
|
||||
|
||||
;; Check the result
|
||||
(let [data {::th/type :file-data-for-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)}
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (map? result))
|
||||
(t/is (contains? result :page))
|
||||
(t/is (contains? result :revn))
|
||||
(t/is (contains? result :file-id))
|
||||
(t/is (= (:id file) (:file-id result)))
|
||||
(t/is (nil? (get-in result [:page :objects frame1-id :thumbnail])))
|
||||
(t/is (not= [] (get-in result [:page :objects frame1-id :shapes])))))
|
||||
|
||||
(t/testing "TASK :file-gc"
|
||||
|
||||
;; insert object snapshot for known frame
|
||||
(let [data {::th/type :upsert-file-object-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:object-id frame1-id
|
||||
:data "new-data"}
|
||||
{:keys [error result] :as out} (th/mutation! data)]
|
||||
(t/is (nil? error))
|
||||
(t/is (nil? result)))
|
||||
|
||||
;; Wait to file be ellegible for GC
|
||||
(th/sleep 300)
|
||||
|
||||
;; run the task again
|
||||
(let [task (:app.tasks.file-gc/handler th/*system*)
|
||||
res (task {})]
|
||||
(t/is (= 1 (:processed res))))
|
||||
|
||||
;; check that object thumbnails are still here
|
||||
(let [res (th/db-exec! ["select * from file_object_thumbnail"])]
|
||||
(t/is (= 1 (count res)))
|
||||
(t/is (= "new-data" (get-in res [0 :data]))))
|
||||
|
||||
;; insert object snapshot for for unknown frame
|
||||
(let [data {::th/type :upsert-file-object-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:object-id (uuid/next)
|
||||
:data "new-data-2"}
|
||||
{:keys [error result] :as out} (th/mutation! data)]
|
||||
(t/is (nil? error))
|
||||
(t/is (nil? result)))
|
||||
|
||||
;; Mark file as modified
|
||||
(th/db-exec! ["update file set has_media_trimmed=false where id=?" (:id file)])
|
||||
|
||||
;; check that we have all object thumbnails
|
||||
(let [res (th/db-exec! ["select * from file_object_thumbnail"])]
|
||||
(t/is (= 2 (count res))))
|
||||
|
||||
;; run the task again
|
||||
(let [task (:app.tasks.file-gc/handler th/*system*)
|
||||
res (task {})]
|
||||
(t/is (= 1 (:processed res))))
|
||||
|
||||
;; check that the unknown frame thumbnail is deleted
|
||||
(let [res (th/db-exec! ["select * from file_object_thumbnail"])]
|
||||
(t/is (= 1 (count res)))
|
||||
(t/is (= "new-data" (get-in res [0 :data])))))))
|
||||
|
||||
|
||||
(t/deftest file-thumbnail-ops
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.pprint :as pp]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.main :as main]
|
||||
|
@ -303,7 +304,7 @@
|
|||
(println "====> END ERROR"))
|
||||
(do
|
||||
(println "====> START RESPONSE")
|
||||
(fipp.edn/pprint result)
|
||||
(pp/pprint result)
|
||||
(println "====> END RESPONSE"))))
|
||||
|
||||
(defn exception?
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/promesa {:mvn/version "8.0.450"}
|
||||
funcool/cuerdas {:mvn/version "2022.01.14-391"}
|
||||
funcool/cuerdas {:mvn/version "2022.03.27-397"}
|
||||
|
||||
lambdaisland/uri {:mvn/version "1.13.95"
|
||||
:exclusions [org.clojure/data.json]}
|
||||
|
@ -42,7 +42,7 @@
|
|||
{:extra-deps
|
||||
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||
org.clojure/test.check {:mvn/version "RELEASE"}
|
||||
thheller/shadow-cljs {:mvn/version "2.17.5"}
|
||||
thheller/shadow-cljs {:mvn/version "2.17.8"}
|
||||
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||
criterium/criterium {:mvn/version "RELEASE"}
|
||||
mockery/mockery {:mvn/version "RELEASE"}}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"test": "yarn run compile-test && yarn run run-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shadow-cljs": "2.17.5",
|
||||
"shadow-cljs": "2.17.8",
|
||||
"source-map-support": "^0.5.19",
|
||||
"ws": "^7.4.6"
|
||||
}
|
||||
|
|
|
@ -23,9 +23,9 @@
|
|||
#?(:clj
|
||||
(:import linked.set.LinkedSet)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Structures
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn ordered-set
|
||||
([] lks/empty-linked-set)
|
||||
|
@ -49,9 +49,14 @@
|
|||
([a] (into (queue) [a]))
|
||||
([a & more] (into (queue) (cons a more))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Structures Manipulation
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn editable-collection?
|
||||
[m]
|
||||
#?(:clj (instance? clojure.lang.IEditableCollection m)
|
||||
:cljs (implements? core/IEditableCollection m)))
|
||||
|
||||
(defn deep-merge
|
||||
([a b]
|
||||
|
@ -101,7 +106,6 @@
|
|||
|
||||
(defn preconj
|
||||
[coll elem]
|
||||
(assert (or (vector? coll) (nil? coll)))
|
||||
(into [elem] coll))
|
||||
|
||||
(defn enumerate
|
||||
|
@ -174,9 +178,12 @@
|
|||
"Return a map without the keys provided
|
||||
in the `keys` parameter."
|
||||
[data keys]
|
||||
(when (map? data)
|
||||
(persistent!
|
||||
(reduce #(dissoc! %1 %2) (transient data) keys))))
|
||||
(persistent!
|
||||
(reduce dissoc!
|
||||
(if (editable-collection? data)
|
||||
(transient data)
|
||||
(transient {}))
|
||||
keys)))
|
||||
|
||||
(defn remove-at-index
|
||||
"Takes a vector and returns a vector with an element in the
|
||||
|
@ -209,8 +216,7 @@
|
|||
(with-meta
|
||||
(persistent!
|
||||
(reduce-kv (fn [acc k v] (assoc! acc k (f v)))
|
||||
(if #?(:clj (instance? clojure.lang.IEditableCollection m)
|
||||
:cljs (implements? core/IEditableCollection m))
|
||||
(if (editable-collection? m)
|
||||
(transient m)
|
||||
(transient {}))
|
||||
m))
|
||||
|
@ -344,13 +350,14 @@
|
|||
(do (vswap! seen conj input*)
|
||||
(rf result input)))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Parsing / Conversion
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn nan?
|
||||
[v]
|
||||
(not= v v))
|
||||
#?(:cljs (js/isNaN v)
|
||||
:clj (not= v v)))
|
||||
|
||||
(defn- impl-parse-integer
|
||||
[v]
|
||||
|
@ -408,9 +415,9 @@
|
|||
[val default]
|
||||
(or val default))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Parsing / Conversion
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(defn nilf
|
||||
"Returns a new function that if you pass nil as any argument will
|
||||
return nil"
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#?(:clj [clojure.core :as c]
|
||||
:cljs [cljs.core :as c])
|
||||
[app.common.data :as d]
|
||||
[cuerdas.core :as str]
|
||||
[cljs.analyzer.api :as aapi]))
|
||||
|
||||
(defmacro select-keys
|
||||
|
@ -36,61 +37,9 @@
|
|||
`(let [v# (-> ~target ~@(map (fn [key] (list `c/get key)) keys))]
|
||||
(if (some? v#) v# ~default))))
|
||||
|
||||
|
||||
;; => benchmarking: clojure.core/str
|
||||
;; --> WARM: 100000
|
||||
;; --> BENCH: 500000
|
||||
;; --> TOTAL: 197.82ms
|
||||
;; --> MEAN: 395.64ns
|
||||
;; => benchmarking: app.commons.data.macros/str
|
||||
;; --> WARM: 100000
|
||||
;; --> BENCH: 500000
|
||||
;; --> TOTAL: 20.31ms
|
||||
;; --> MEAN: 40.63ns
|
||||
|
||||
(defmacro str
|
||||
"CLJS only macro variant of `str` function that performs string concat much faster."
|
||||
([a]
|
||||
(if (:ns &env)
|
||||
(list 'js* "\"\"+~{}" a)
|
||||
(list `c/str a)))
|
||||
([a b]
|
||||
(if (:ns &env)
|
||||
(list 'js* "\"\"+~{}+~{}" a b)
|
||||
(list `c/str a b)))
|
||||
([a b c]
|
||||
(if (:ns &env)
|
||||
(list 'js* "\"\"+~{}+~{}+~{}" a b c)
|
||||
(list `c/str a b c)))
|
||||
([a b c d]
|
||||
(if (:ns &env)
|
||||
(list 'js* "\"\"+~{}+~{}+~{}+~{}" a b c d)
|
||||
(list `c/str a b c d)))
|
||||
([a b c d e]
|
||||
(if (:ns &env)
|
||||
(list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}" a b c d e)
|
||||
(list `c/str a b c d e)))
|
||||
([a b c d e f]
|
||||
(if (:ns &env)
|
||||
(list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}+~{}" a b c d e f)
|
||||
(list `c/str a b c d e f)))
|
||||
([a b c d e f g]
|
||||
(if (:ns &env)
|
||||
(list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}+~{}+~{}" a b c d e f g)
|
||||
(list `c/str a b c d e f g)))
|
||||
([a b c d e f g h]
|
||||
(if (:ns &env)
|
||||
(list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}+~{}+~{}+~{}" a b c d e f g h)
|
||||
(list `c/str a b c d e f g h)))
|
||||
([a b c d e f g h & rest]
|
||||
(let [all (into [a b c d e f g h] rest)]
|
||||
(if (:ns &env)
|
||||
(let [xf (map (fn [items] `(str ~@items)))
|
||||
pall (partition-all 8 all)]
|
||||
(if (<= (count all) 64)
|
||||
`(str ~@(sequence xf pall))
|
||||
`(c/str ~@(sequence xf pall))))
|
||||
`(c/str ~@all)))))
|
||||
[& params]
|
||||
`(str/concat ~@params))
|
||||
|
||||
(defmacro export
|
||||
"A helper macro that allows reexport a var in a current namespace."
|
||||
|
@ -129,36 +78,6 @@
|
|||
;; (.setMacro (var ~n)))
|
||||
~vr))))
|
||||
|
||||
(defn- interpolate
|
||||
[s params]
|
||||
(loop [items (->> (re-seq #"([^\%]+)*(\%(\d+)?)?" s)
|
||||
(remove (fn [[full seg]] (and (nil? seg) (not full)))))
|
||||
result []
|
||||
index 0]
|
||||
(if-let [[_ segment var? sidx] (first items)]
|
||||
(cond
|
||||
(and var? sidx)
|
||||
(let [cidx (dec (d/read-string sidx))]
|
||||
(recur (rest items)
|
||||
(-> result
|
||||
(conj segment)
|
||||
(conj (nth params cidx)))
|
||||
(inc index)))
|
||||
|
||||
var?
|
||||
(recur (rest items)
|
||||
(-> result
|
||||
(conj segment)
|
||||
(conj (nth params index)))
|
||||
(inc index))
|
||||
|
||||
:else
|
||||
(recur (rest items)
|
||||
(conj result segment)
|
||||
(inc index)))
|
||||
|
||||
(remove nil? result))))
|
||||
|
||||
(defmacro fmt
|
||||
"String interpolation helper. Can only be used with strings known at
|
||||
compile time. Can be used with indexed params access or sequential.
|
||||
|
@ -169,7 +88,7 @@
|
|||
(dm/fmt \"url(%1)\" my-url) ; indexed
|
||||
"
|
||||
[s & params]
|
||||
(cons 'app.common.data.macros/str (interpolate s (vec params))))
|
||||
`(str/ffmt ~s ~@params))
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -57,3 +57,31 @@
|
|||
(defn exception?
|
||||
[v]
|
||||
(instance? #?(:clj java.lang.Throwable :cljs js/Error) v))
|
||||
|
||||
|
||||
#?(:cljs
|
||||
(deftype WrappedException [cause meta]
|
||||
cljs.core/IMeta
|
||||
(-meta [_] meta)
|
||||
|
||||
cljs.core/IDeref
|
||||
(-deref [_] cause))
|
||||
:clj
|
||||
(deftype WrappedException [cause meta]
|
||||
clojure.lang.IMeta
|
||||
(meta [_] meta)
|
||||
|
||||
clojure.lang.IDeref
|
||||
(deref [_] cause)))
|
||||
|
||||
|
||||
(ns-unmap 'app.common.exceptions '->WrappedException)
|
||||
(ns-unmap 'app.common.exceptions 'map->WrappedException)
|
||||
|
||||
(defn wrapped?
|
||||
[o]
|
||||
(instance? WrappedException o))
|
||||
|
||||
(defn wrap-with-context
|
||||
[cause context]
|
||||
(WrappedException. cause context))
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.transforms :as gtr]
|
||||
[app.common.geom.shapes.rect :as gre]
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
|
@ -77,18 +77,16 @@
|
|||
(defmethod constraint-modifier :fixed
|
||||
[_ axis parent child _ transformed-parent-rect]
|
||||
(let [parent-rect (:selrect parent)
|
||||
child-rect (:selrect child)
|
||||
child-rect (gre/points->rect (:points child))
|
||||
|
||||
delta-start (get-delta-start axis parent-rect transformed-parent-rect)
|
||||
delta-size (get-delta-size axis parent-rect transformed-parent-rect)
|
||||
child-size (get-size axis child-rect)
|
||||
child-center (gco/center-rect child-rect)]
|
||||
child-size (get-size axis child-rect)]
|
||||
(if (or (not (mth/almost-zero? delta-start))
|
||||
(not (mth/almost-zero? delta-size)))
|
||||
|
||||
{:displacement (get-displacement axis delta-start)
|
||||
:resize-origin (-> (get-displacement axis delta-start (:x1 child-rect) (:y1 child-rect))
|
||||
(gtr/transform-point-center child-center (:transform child (gmt/matrix))))
|
||||
:resize-origin (get-displacement axis delta-start (:x child-rect) (:y child-rect))
|
||||
:resize-vector (get-scale axis (/ (+ child-size delta-size) child-size))}
|
||||
{})))
|
||||
|
||||
|
|
|
@ -6,14 +6,13 @@
|
|||
|
||||
(ns app.common.logging
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.spec :as us]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[cuerdas.core :as str]
|
||||
[clojure.spec.alpha :as s]
|
||||
[fipp.edn :as fpp]
|
||||
#?(:clj [io.aviso.exception :as ie])
|
||||
#?(:cljs [goog.log :as glog]))
|
||||
#?(:cljs (:require-macros [app.common.logging])
|
||||
:clj (:import
|
||||
|
@ -22,7 +21,6 @@
|
|||
org.apache.logging.log4j.Logger
|
||||
org.apache.logging.log4j.ThreadContext
|
||||
org.apache.logging.log4j.CloseableThreadContext
|
||||
org.apache.logging.log4j.message.MapMessage
|
||||
org.apache.logging.log4j.spi.LoggerContext)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -31,11 +29,22 @@
|
|||
|
||||
#?(:clj (set! *warn-on-reflection* true))
|
||||
|
||||
#?(:clj
|
||||
(defn build-map-message
|
||||
[m]
|
||||
(let [message (MapMessage. (count m))]
|
||||
(reduce-kv #(.with ^MapMessage %1 (name %2) %3) message m))))
|
||||
(def ^:private reserved-props
|
||||
#{:level :cause ::logger ::async ::raw ::context})
|
||||
|
||||
(def ^:private props-xform
|
||||
(comp (partition-all 2)
|
||||
(remove (fn [[k]] (contains? reserved-props k)))
|
||||
(map vec)))
|
||||
|
||||
(defn build-message
|
||||
[props]
|
||||
(loop [pairs (sequence props-xform props)
|
||||
result []]
|
||||
(if-let [[k v] (first pairs)]
|
||||
(recur (rest pairs)
|
||||
(conj result (str/concat (d/name k) "=" (pr-str v))))
|
||||
result)))
|
||||
|
||||
#?(:clj
|
||||
(def logger-context
|
||||
|
@ -45,13 +54,6 @@
|
|||
(def logging-agent
|
||||
(agent nil :error-mode :continue)))
|
||||
|
||||
(defn- simple-prune
|
||||
([s] (simple-prune s (* 1024 1024)))
|
||||
([s max-length]
|
||||
(if (> (count s) max-length)
|
||||
(str (subs s 0 max-length) " [...]")
|
||||
s)))
|
||||
|
||||
#?(:clj
|
||||
(defn stringify-data
|
||||
[val]
|
||||
|
@ -132,22 +134,25 @@
|
|||
|
||||
(defn write-log!
|
||||
[logger level exception message]
|
||||
#?(:clj
|
||||
(if exception
|
||||
(.log ^Logger logger
|
||||
^Level level
|
||||
^Object message
|
||||
^Throwable exception)
|
||||
(.log ^Logger logger
|
||||
^Level level
|
||||
^Object message))
|
||||
:cljs
|
||||
(when glog/ENABLED
|
||||
(when-let [l (get-logger logger)]
|
||||
(let [level (get-level level)
|
||||
record (glog/LogRecord. level message (.getName ^js l))]
|
||||
(when exception (.setException record exception))
|
||||
(glog/publishLogRecord l record))))))
|
||||
(let [message (if (string? message)
|
||||
message
|
||||
(str/join ", " message))]
|
||||
#?(:clj
|
||||
(if exception
|
||||
(.log ^Logger logger
|
||||
^Level level
|
||||
^Object message
|
||||
^Throwable exception)
|
||||
(.log ^Logger logger
|
||||
^Level level
|
||||
^Object message))
|
||||
:cljs
|
||||
(when glog/ENABLED
|
||||
(when-let [l (get-logger logger)]
|
||||
(let [level (get-level level)
|
||||
record (glog/LogRecord. level message (.getName ^js l))]
|
||||
(when exception (.setException record exception))
|
||||
(glog/publishLogRecord l record)))))))
|
||||
|
||||
#?(:clj
|
||||
(defn enabled?
|
||||
|
@ -167,13 +172,13 @@
|
|||
{:spec-explain (us/pretty-explain data)})))))
|
||||
|
||||
(defmacro log
|
||||
[& {:keys [level cause ::logger ::async ::raw ::context] :or {async true} :as props}]
|
||||
[& props]
|
||||
(if (:ns &env) ; CLJS
|
||||
`(write-log! ~(or logger (str *ns*))
|
||||
~level
|
||||
~cause
|
||||
(or ~raw ~(dissoc props :level :cause ::logger ::raw ::context)))
|
||||
(let [props (dissoc props :level :cause ::logger ::async ::raw ::context)
|
||||
(let [{:keys [level cause ::logger ::raw]} props
|
||||
message (or raw (build-message props))]
|
||||
`(write-log! ~(or logger (str *ns*)) ~level ~cause (or ~raw (build-message ~(vec props)))))
|
||||
|
||||
(let [{:keys [level cause ::logger ::async ::raw ::context] :or {async true}} props
|
||||
logger (or logger (str *ns*))
|
||||
logger-sym (gensym "log")
|
||||
level-sym (gensym "log")]
|
||||
|
@ -184,17 +189,17 @@
|
|||
`(do
|
||||
(send-off logging-agent
|
||||
(fn [_#]
|
||||
(try
|
||||
(let [message# (or ~raw (build-message ~(vec props)))]
|
||||
(with-context (-> {:id (uuid/next)}
|
||||
(into ~context)
|
||||
(into (get-error-context ~cause)))
|
||||
(->> (or ~raw (build-map-message ~props))
|
||||
(write-log! ~logger-sym ~level-sym ~cause)))
|
||||
(catch Throwable cause#
|
||||
(write-log! ~logger-sym (get-level :error) cause#
|
||||
"unexpected error on writting log")))))
|
||||
(try
|
||||
(write-log! ~logger-sym ~level-sym ~cause message#)
|
||||
(catch Throwable cause#
|
||||
(write-log! ~logger-sym (get-level :error) cause#
|
||||
"unexpected error on writting log")))))))
|
||||
nil)
|
||||
`(let [message# (or ~raw (build-map-message ~props))]
|
||||
`(let [message# (or ~raw (build-message ~(vec props)))]
|
||||
(write-log! ~logger-sym ~level-sym ~cause message#)
|
||||
nil)))))))
|
||||
|
||||
|
@ -284,8 +289,8 @@
|
|||
#?(:cljs
|
||||
(defn- prepare-message
|
||||
[message]
|
||||
(loop [kvpairs (seq message)
|
||||
message (array-map)
|
||||
(loop [kvpairs (seq message)
|
||||
message []
|
||||
specials []]
|
||||
(if (nil? kvpairs)
|
||||
[message specials]
|
||||
|
@ -304,7 +309,7 @@
|
|||
|
||||
:else
|
||||
(recur (next kvpairs)
|
||||
(assoc message k v)
|
||||
(conj message (str/concat (d/name k) "=" (pr-str v)))
|
||||
specials)))))))
|
||||
|
||||
#?(:cljs
|
||||
|
@ -320,7 +325,7 @@
|
|||
(js/console.log message header-styles normal-styles))
|
||||
(let [[message specials] (prepare-message message)]
|
||||
(if (seq specials)
|
||||
(let [message (str header "%c" (pr-str message))]
|
||||
(let [message (str header "%c" message)]
|
||||
(js/console.group message header-styles normal-styles)
|
||||
(doseq [[type n v] specials]
|
||||
(case type
|
||||
|
@ -329,7 +334,7 @@
|
|||
(js/console.error (pr-str v))
|
||||
(js/console.error v))))
|
||||
(js/console.groupEnd message))
|
||||
(let [message (str header "%c" (pr-str message))]
|
||||
(let [message (str header "%c" message)]
|
||||
(js/console.log message header-styles normal-styles)))))
|
||||
|
||||
(when exception
|
||||
|
|
|
@ -96,6 +96,7 @@
|
|||
(-> (update :touched cph/set-touched-group :shapes-group)
|
||||
(dissoc :remote-synced?)))))
|
||||
|
||||
;; TODO: this looks wrong, why we allow nil values?
|
||||
(update-objects [objects parent-id]
|
||||
(if (and (or (nil? parent-id) (contains? objects parent-id))
|
||||
(or (nil? frame-id) (contains? objects frame-id)))
|
||||
|
|
27
common/src/app/common/pprint.cljc
Normal file
27
common/src/app/common/pprint.cljc
Normal file
|
@ -0,0 +1,27 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.common.pprint
|
||||
(:refer-clojure :exclude [prn])
|
||||
(:require
|
||||
[cuerdas.core :as str]
|
||||
[fipp.edn :as fpp]))
|
||||
|
||||
(defn pprint-str
|
||||
[expr]
|
||||
(binding [*print-level* 8
|
||||
*print-length* 25]
|
||||
(with-out-str
|
||||
(fpp/pprint expr {:width 110}))))
|
||||
|
||||
(defn pprint
|
||||
([expr]
|
||||
(println (pprint-str expr)))
|
||||
([label expr]
|
||||
(println (str/concat "============ " label "============"))
|
||||
(pprint expr)))
|
||||
|
||||
|
|
@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
|
|||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV NODE_VERSION=v16.14.0 \
|
||||
CLOJURE_VERSION=1.10.3.1075 \
|
||||
CLJKONDO_VERSION=2022.02.09 \
|
||||
BABASHKA_VERSION=0.7.6 \
|
||||
ENV NODE_VERSION=v16.14.2 \
|
||||
CLOJURE_VERSION=1.11.0.1100 \
|
||||
CLJKONDO_VERSION=2022.03.09 \
|
||||
BABASHKA_VERSION=0.8.0 \
|
||||
LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8
|
||||
|
||||
|
@ -30,6 +30,7 @@ RUN set -ex; \
|
|||
rsync \
|
||||
fakeroot \
|
||||
netcat \
|
||||
file \
|
||||
; \
|
||||
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
|
||||
locale-gen; \
|
||||
|
@ -102,22 +103,14 @@ 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/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.2_8.tar.gz; \
|
||||
mkdir -p /usr/lib/jvm/openjdk17; \
|
||||
cd /usr/lib/jvm/openjdk17; \
|
||||
curl -LfsSo /tmp/openjdk.tar.gz https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18%2B36/OpenJDK18U-jdk_x64_linux_hotspot_18_36.tar.gz; \
|
||||
mkdir -p /usr/lib/jvm/openjdk; \
|
||||
cd /usr/lib/jvm/openjdk; \
|
||||
tar -xf /tmp/openjdk.tar.gz --strip-components=1; \
|
||||
rm -rf /tmp/openjdk.tar.gz;
|
||||
|
||||
ENV PATH="/usr/lib/jvm/openjdk17/bin:/usr/local/nodejs/bin:$PATH" JAVA_HOME=/usr/lib/jvm/openjdk17
|
||||
ENV PATH="/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:$PATH" JAVA_HOME=/usr/lib/jvm/openjdk
|
||||
|
||||
RUN set -ex; \
|
||||
curl -LfsSo /tmp/clojure.sh https://download.clojure.org/install/linux-install-$CLOJURE_VERSION.sh; \
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
export PATH=/usr/lib/jvm/openjdk17/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
||||
export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
||||
|
||||
alias l='ls --color -GFlh'
|
||||
alias rm='rm -r'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
export PATH=/usr/lib/jvm/openjdk17/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
||||
export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
||||
|
||||
set -e
|
||||
usermod -u ${EXTERNAL_UID:-1000} penpot
|
||||
|
|
|
@ -46,7 +46,7 @@ http {
|
|||
listen 3449 default_server;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 50M;
|
||||
client_max_body_size 30M;
|
||||
charset utf-8;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
|
|
|
@ -14,6 +14,7 @@ yarn install
|
|||
popd
|
||||
pushd ~/penpot/exporter/
|
||||
yarn install
|
||||
npx playwright install chromium
|
||||
popd
|
||||
|
||||
tmux -2 new-session -d -s penpot
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Should be set to the public domain where penpot is going to be served.
|
||||
PENPOT_PUBLIC_URI=http://localhost:9001
|
||||
PENPOT_TENANT=pro
|
||||
|
||||
# Temporal workaround because of bad builtin default
|
||||
PENPOT_HTTP_SERVER_HOST=0.0.0.0
|
||||
|
@ -16,7 +17,7 @@ PENPOT_REDIS_URI=redis://penpot-redis/0
|
|||
# can be configured to store in AWS S3 or completely in de the database.
|
||||
# Storing in the database makes the backups more easy but will make access to
|
||||
# media less performant.
|
||||
ASSETS_STORAGE_BACKEND=assets-fs
|
||||
PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
|
||||
PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets
|
||||
|
||||
# Telemetry. When enabled, a periodical process will send anonymous data about
|
||||
|
|
|
@ -56,11 +56,12 @@
|
|||
|
||||
(defn screenshot
|
||||
([frame] (screenshot frame {}))
|
||||
([frame {:keys [full-page? omit-background? type quality]
|
||||
([frame {:keys [full-page? omit-background? type quality path]
|
||||
:or {type "png" full-page? false omit-background? false quality 95}}]
|
||||
(let [options (-> (obj/new)
|
||||
(obj/set! "type" (name type))
|
||||
(obj/set! "omitBackground" omit-background?)
|
||||
(cond-> path (obj/set! "path" path))
|
||||
(cond-> (= "jpeg" type) (obj/set! "quality" quality))
|
||||
(cond-> full-page? (-> (obj/set! "fullPage" true)
|
||||
(obj/set! "clip" nil))))]
|
||||
|
@ -73,10 +74,10 @@
|
|||
|
||||
(defn pdf
|
||||
([page] (pdf page {}))
|
||||
([page {:keys [scale save-path page-ranges]
|
||||
([page {:keys [scale path page-ranges]
|
||||
:or {page-ranges "1"
|
||||
scale 1}}]
|
||||
(.pdf ^js page #js {:path save-path
|
||||
(.pdf ^js page #js {:path path
|
||||
:scale scale
|
||||
:pageRanges page-ranges
|
||||
:printBackground true
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
(ns app.handlers
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
|
@ -32,6 +32,7 @@
|
|||
(let [explain (us/pretty-explain data)
|
||||
data (-> data
|
||||
(assoc :explain explain)
|
||||
(assoc :type :validation)
|
||||
(dissoc ::s/problems ::s/value ::s/spec))]
|
||||
(-> exchange
|
||||
(assoc :response/status 400)
|
||||
|
@ -46,19 +47,24 @@
|
|||
|
||||
(and (= :internal type)
|
||||
(= :browser-not-ready code))
|
||||
(-> exchange
|
||||
(assoc :response/status 503)
|
||||
(assoc :response/body (t/encode data))
|
||||
(assoc :response/headers {"content-type" "application/transit+json"}))
|
||||
(let [data {:type :server-error
|
||||
:code :internal
|
||||
:hint (ex-message error)
|
||||
:data data}]
|
||||
(-> exchange
|
||||
(assoc :response/status 503)
|
||||
(assoc :response/body (t/encode data))
|
||||
(assoc :response/headers {"content-type" "application/transit+json"})))
|
||||
|
||||
:else
|
||||
(let [data {:type :server-error
|
||||
:code type
|
||||
:hint (ex-message error)
|
||||
:data data}]
|
||||
(l/error :hint "unexpected internal error" :cause error)
|
||||
(-> exchange
|
||||
(assoc :response/status 500)
|
||||
(assoc :response/body (t/encode data))
|
||||
(assoc :response/body (t/encode (d/without-nils data)))
|
||||
(assoc :response/headers {"content-type" "application/transit+json"}))))))
|
||||
|
||||
(defmulti command-spec :cmd)
|
||||
|
@ -98,4 +104,4 @@
|
|||
:export-frames (export-frames/handler exchange params)
|
||||
(ex/raise :type :internal
|
||||
:code :method-not-implemented
|
||||
:hint (dm/fmt "method % not implemented" cmd)))))
|
||||
:hint (str/istr "method ~{cmd} not implemented")))))
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
(ns app.handlers.export-frames
|
||||
(:require
|
||||
["path" :as path]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as exc :include-macros true]
|
||||
[app.common.logging :as l]
|
||||
[app.common.exceptions :as exc]
|
||||
[app.common.spec :as us]
|
||||
[app.common.pprint :as pp]
|
||||
[app.handlers.resources :as rsc]
|
||||
[app.handlers.export-shapes :refer [prepare-exports]]
|
||||
[app.redis :as redis]
|
||||
[app.renderer.pdf :as rp]
|
||||
[app.renderer :as rd]
|
||||
[app.util.shell :as sh]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -20,19 +22,17 @@
|
|||
|
||||
(declare ^:private handle-export)
|
||||
(declare ^:private create-pdf)
|
||||
(declare ^:private export-frame)
|
||||
(declare ^:private join-pdf)
|
||||
(declare ^:private move-file)
|
||||
(declare ^:private clean-tmp)
|
||||
|
||||
(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 ::object-id ::us/uuid)
|
||||
(s/def ::uri ::us/uri)
|
||||
|
||||
(s/def ::export
|
||||
(s/keys :req-un [::file-id ::page-id ::frame-id ::name]))
|
||||
(s/keys :req-un [::file-id ::page-id ::object-id ::name]))
|
||||
|
||||
(s/def ::exports
|
||||
(s/every ::export :kind vector? :min-count 1))
|
||||
|
@ -42,42 +42,53 @@
|
|||
:opt-un [::uri ::name]))
|
||||
|
||||
(defn handler
|
||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports uri] :as params}]
|
||||
(let [xform (map #(assoc % :token auth-token :uri uri))
|
||||
exports (sequence xform exports)]
|
||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports uri profile-id] :as params}]
|
||||
;; NOTE: we need to have the `:type` prop because the exports
|
||||
;; datastructure preparation uses it for creating the groups.
|
||||
(let [exports (-> (map #(assoc % :type :pdf :scale 1 :suffix "") exports)
|
||||
(prepare-exports auth-token uri))]
|
||||
(handle-export exchange (assoc params :exports exports))))
|
||||
|
||||
(defn handle-export
|
||||
[exchange {:keys [exports wait uri name] :as params}]
|
||||
(let [topic (-> exports first :file-id str)
|
||||
[exchange {:keys [exports wait uri name profile-id] :as params}]
|
||||
(let [total (count exports)
|
||||
topic (str profile-id)
|
||||
resource (rsc/create :pdf (or name (-> exports first :name)))
|
||||
|
||||
on-progress (fn [progress]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:status "running"
|
||||
:progress progress}]
|
||||
(redis/pub! topic data)))
|
||||
on-progress (fn [{:keys [done]}]
|
||||
(when-not wait
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:filename (:filename resource)
|
||||
:status "running"
|
||||
:total total
|
||||
:done done}]
|
||||
(redis/pub! topic data))))
|
||||
|
||||
on-complete (fn [resource]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:size (:size resource)
|
||||
:status "ended"}]
|
||||
(redis/pub! topic data)))
|
||||
on-complete (fn []
|
||||
(when-not wait
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:filename (:filename resource)
|
||||
:status "ended"}]
|
||||
(redis/pub! topic data))))
|
||||
|
||||
on-error (fn [cause]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:status "error"
|
||||
:cause (ex-message cause)}]
|
||||
(redis/pub! topic data)))
|
||||
(l/error :hint "unexpected error on frames exportation" :cause cause)
|
||||
(if wait
|
||||
(p/rejected cause)
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:filename (:filename resource)
|
||||
:status "error"
|
||||
:cause (ex-message cause)}]
|
||||
(redis/pub! topic data))))
|
||||
|
||||
proc (create-pdf :resource resource
|
||||
:items exports
|
||||
:exports exports
|
||||
:on-progress on-progress
|
||||
:on-complete on-complete
|
||||
:on-error on-error)]
|
||||
|
@ -86,70 +97,46 @@
|
|||
(assoc exchange :response/body (dissoc resource :path)))))
|
||||
|
||||
(defn create-pdf
|
||||
[& {:keys [resource items on-progress on-complete on-error]
|
||||
:or {on-progress identity
|
||||
on-complete identity
|
||||
on-error identity}}]
|
||||
(p/let [progress (atom 0)
|
||||
tmpdir (sh/create-tmpdir! "pdfexport")
|
||||
file-id (-> items first :file-id)
|
||||
items (into [] (map #(partial export-frame tmpdir %)) items)
|
||||
xform (map (fn [export-fn]
|
||||
#(p/finally
|
||||
(export-fn)
|
||||
(fn [result _]
|
||||
(on-progress {:total (count items)
|
||||
:done (swap! progress inc)
|
||||
:name (:name result)})))))]
|
||||
(-> (reduce (fn [res export-fn]
|
||||
(p/let [res res
|
||||
out (export-fn)]
|
||||
(cons (:path out) res)))
|
||||
(p/resolved nil)
|
||||
(into '() xform items))
|
||||
(p/then (partial join-pdf tmpdir file-id))
|
||||
[& {:keys [resource exports on-progress on-complete on-error]
|
||||
:or {on-progress (constantly nil)
|
||||
on-complete (constantly nil)
|
||||
on-error p/rejected}}]
|
||||
|
||||
(let [file-id (-> exports first :file-id)
|
||||
result (atom [])
|
||||
|
||||
on-object
|
||||
(fn [{:keys [path] :as object}]
|
||||
(let [res (swap! result conj path)]
|
||||
(on-progress {:done (count res)})))]
|
||||
|
||||
(-> (p/loop [exports (seq exports)]
|
||||
(when-let [export (first exports)]
|
||||
(p/let [proc (rd/render export on-object)]
|
||||
(p/recur (rest exports)))))
|
||||
|
||||
(p/then (fn [_] (deref result)))
|
||||
(p/then (partial join-pdf file-id))
|
||||
(p/then (partial move-file resource))
|
||||
(p/then (partial clean-tmp tmpdir))
|
||||
(p/then (constantly resource))
|
||||
(p/then (fn [resource]
|
||||
(-> (sh/stat (:path resource))
|
||||
(p/then #(merge resource %)))))
|
||||
(p/catch on-error)
|
||||
(p/finally (fn [result cause]
|
||||
(if cause
|
||||
(on-error cause)
|
||||
(on-complete result)))))))
|
||||
|
||||
(defn- export-frame
|
||||
[tmpdir {:keys [file-id page-id frame-id token uri] :as params}]
|
||||
(let [file-name (dm/fmt "%.pdf" frame-id)
|
||||
save-path (path/join tmpdir file-name)]
|
||||
(-> (rp/render {:name (dm/str frame-id)
|
||||
:uri uri
|
||||
:suffix ""
|
||||
:token token
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id frame-id
|
||||
:scale 1
|
||||
:save-path save-path})
|
||||
(p/then (fn [_]
|
||||
{:name file-name
|
||||
:path save-path})))))
|
||||
(when-not cause
|
||||
(on-complete)))))))
|
||||
|
||||
(defn- join-pdf
|
||||
[tmpdir file-id paths]
|
||||
(let [output-path (path/join tmpdir (str file-id ".pdf"))
|
||||
paths-str (str/join " " paths)]
|
||||
(-> (sh/run-cmd! (str "pdfunite " paths-str " " output-path))
|
||||
(p/then (constantly output-path)))))
|
||||
[file-id paths]
|
||||
(p/let [tmpdir (sh/mktmpdir! "join-pdf")
|
||||
path (path/join tmpdir (str/concat file-id ".pdf"))]
|
||||
(sh/run-cmd! (str "pdfunite " (str/join " " paths) " " path))
|
||||
path))
|
||||
|
||||
(defn- move-file
|
||||
[{:keys [path] :as resource} output-path]
|
||||
(p/do
|
||||
(sh/move! output-path path)
|
||||
(sh/rmdir! (path/dirname output-path))
|
||||
resource))
|
||||
|
||||
(defn- clean-tmp
|
||||
[tdpath data]
|
||||
(p/do!
|
||||
(sh/rmdir! tdpath)
|
||||
data))
|
||||
|
|
|
@ -6,34 +6,35 @@
|
|||
|
||||
(ns app.handlers.export-shapes
|
||||
(:require
|
||||
[app.common.exceptions :as exc :include-macros true]
|
||||
["path" :as path]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as exc]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.redis :as redis]
|
||||
[app.handlers.resources :as rsc]
|
||||
[app.renderer.bitmap :as rb]
|
||||
[app.renderer.pdf :as rp]
|
||||
[app.renderer.svg :as rs]
|
||||
[app.redis :as redis]
|
||||
[app.renderer :as rd]
|
||||
[app.util.mime :as mime]
|
||||
[app.util.shell :as sh]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(declare ^:private handle-exports)
|
||||
(declare ^:private handle-single-export)
|
||||
(declare ^:private handle-multiple-export)
|
||||
(declare ^:private run-export)
|
||||
(declare ^:private assign-file-name)
|
||||
(declare ^:private assoc-file-name)
|
||||
(declare prepare-exports)
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::filename ::us/string)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::object-id ::us/uuid)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::scale ::us/number)
|
||||
(s/def ::suffix ::us/string)
|
||||
(s/def ::type ::us/keyword)
|
||||
(s/def ::suffix string?)
|
||||
(s/def ::scale number?)
|
||||
(s/def ::uri ::us/uri)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::wait ::us/boolean)
|
||||
|
||||
(s/def ::export
|
||||
|
@ -47,13 +48,13 @@
|
|||
:opt-un [::uri ::wait ::name]))
|
||||
|
||||
(defn handler
|
||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}]
|
||||
(let [xform (comp
|
||||
(map #(assoc % :token auth-token))
|
||||
(assign-file-name))
|
||||
exports (into [] xform exports)]
|
||||
(if (= 1 (count exports))
|
||||
(handle-single-export exchange (assoc params :export (first exports)))
|
||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports uri] :as params}]
|
||||
(let [exports (prepare-exports exports auth-token uri)]
|
||||
(if (and (= 1 (count exports))
|
||||
(= 1 (count (-> exports first :objects))))
|
||||
(handle-single-export exchange (-> params
|
||||
(assoc :export (first exports))
|
||||
(dissoc :exports)))
|
||||
(handle-multiple-export exchange (assoc params :exports exports)))))
|
||||
|
||||
(defn- handle-single-export
|
||||
|
@ -61,87 +62,102 @@
|
|||
(let [topic (str profile-id)
|
||||
resource (rsc/create (:type export) (or name (:name export)))
|
||||
|
||||
on-progress (fn [progress]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:status "running"
|
||||
:progress progress}]
|
||||
(redis/pub! topic data)))
|
||||
|
||||
on-complete (fn [resource]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:size (:size resource)
|
||||
:name (:name resource)
|
||||
:status "ended"}]
|
||||
(redis/pub! topic data)))
|
||||
on-progress (fn [{:keys [path] :as object}]
|
||||
(p/do
|
||||
;; Move the generated path to the resource
|
||||
;; path destination.
|
||||
(sh/move! path (:path resource))
|
||||
|
||||
(when-not wait
|
||||
(redis/pub! topic {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:status "running"
|
||||
:total 1
|
||||
:done 1})
|
||||
(redis/pub! topic {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:filename (:filename resource)
|
||||
:name (:name resource)
|
||||
:status "ended"}))))
|
||||
on-error (fn [cause]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:status "error"
|
||||
:cause (ex-message cause)}]
|
||||
(redis/pub! topic data)))
|
||||
(l/error :hint "unexpected error happened on export multiple process"
|
||||
:cause cause)
|
||||
(if wait
|
||||
(p/rejected cause)
|
||||
(redis/pub! topic {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:status "error"
|
||||
:cause (ex-message cause)})))
|
||||
|
||||
proc (-> (rd/render export on-progress)
|
||||
(p/then (constantly resource))
|
||||
(p/catch on-error))]
|
||||
|
||||
proc (rsc/create-simple :task #(run-export export)
|
||||
:resource resource
|
||||
:on-progress on-progress
|
||||
:on-error on-error
|
||||
:on-complete on-complete)]
|
||||
(if wait
|
||||
(p/then proc #(assoc exchange :response/body (dissoc % :path)))
|
||||
(assoc exchange :response/body (dissoc resource :path)))))
|
||||
|
||||
(defn- handle-multiple-export
|
||||
[exchange {:keys [exports wait uri profile-id name] :as params}]
|
||||
(let [tasks (map #(fn [] (run-export %)) exports)
|
||||
(let [resource (rsc/create :zip (or name (-> exports first :name)))
|
||||
total (count exports)
|
||||
topic (str profile-id)
|
||||
resource (rsc/create :zip (or name (-> exports first :name)))
|
||||
|
||||
on-progress (fn [progress]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:status "running"
|
||||
:progress progress}]
|
||||
(redis/pub! topic data)))
|
||||
to-delete (atom #{})
|
||||
|
||||
on-complete (fn [resource]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:size (:size resource)
|
||||
:status "ended"}]
|
||||
(redis/pub! topic data)))
|
||||
on-progress (fn [{:keys [done]}]
|
||||
(when-not wait
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:status "running"
|
||||
:total total
|
||||
:done done}]
|
||||
(redis/pub! topic data))))
|
||||
|
||||
on-complete (fn []
|
||||
(when-not wait
|
||||
(let [data {:type :export-update
|
||||
:name (:name resource)
|
||||
:filename (:filename resource)
|
||||
:resource-id (:id resource)
|
||||
:status "ended"}]
|
||||
(redis/pub! topic data))))
|
||||
|
||||
on-error (fn [cause]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:status "error"
|
||||
:cause (ex-message cause)}]
|
||||
(redis/pub! topic data)))
|
||||
(l/error :hint "unexpected error on multiple exportation" :cause cause)
|
||||
(if wait
|
||||
(p/rejected cause)
|
||||
(redis/pub! topic {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:status "error"
|
||||
:cause (ex-message cause)})))
|
||||
|
||||
proc (rsc/create-zip :resource resource
|
||||
:tasks tasks
|
||||
:on-progress on-progress
|
||||
zip (rsc/create-zip :resource resource
|
||||
:on-complete on-complete
|
||||
:on-error on-error)]
|
||||
:on-error on-error
|
||||
:on-progress on-progress)
|
||||
|
||||
append (fn [{:keys [filename path] :as object}]
|
||||
(swap! to-delete conj path)
|
||||
(rsc/add-to-zip! zip path filename))
|
||||
|
||||
proc (-> (p/do
|
||||
(p/loop [exports (seq exports)]
|
||||
(when-let [export (first exports)]
|
||||
(p/let [proc (rd/render export append)]
|
||||
(p/recur (rest exports)))))
|
||||
(.finalize zip))
|
||||
(p/then (fn [_] (p/run! #(sh/rmdir! (path/dirname %)) @to-delete)))
|
||||
(p/then (constantly resource))
|
||||
(p/catch on-error))
|
||||
]
|
||||
|
||||
(if wait
|
||||
(p/then proc #(assoc exchange :response/body (dissoc % :path)))
|
||||
(assoc exchange :response/body (dissoc resource :path)))))
|
||||
|
||||
(defn- run-export
|
||||
[{:keys [type] :as params}]
|
||||
(p/let [res (case type
|
||||
:png (rb/render params)
|
||||
:jpeg (rb/render params)
|
||||
:svg (rs/render params)
|
||||
:pdf (rp/render params))]
|
||||
(assoc res :type type)))
|
||||
|
||||
(defn- assign-file-name
|
||||
(defn- assoc-file-name
|
||||
"A transducer that assocs a candidate filename and avoid duplicates."
|
||||
[]
|
||||
(letfn [(find-candidate [params used]
|
||||
|
@ -149,12 +165,8 @@
|
|||
(let [candidate (str (:name params)
|
||||
(:suffix params "")
|
||||
(when (pos? index)
|
||||
(str "-" (inc index)))
|
||||
(case (:type params)
|
||||
:png ".png"
|
||||
:jpeg ".jpg"
|
||||
:svg ".svg"
|
||||
:pdf ".pdf"))]
|
||||
(str/concat "-" (inc index)))
|
||||
(mime/get-extension (:type params)))]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc index))
|
||||
candidate))))]
|
||||
|
@ -168,3 +180,37 @@
|
|||
params (assoc params :filename candidate)]
|
||||
(vswap! used conj candidate)
|
||||
(rf result params))))))))
|
||||
|
||||
(def ^:const ^:private
|
||||
default-partition-size 50)
|
||||
|
||||
(defn prepare-exports
|
||||
[exports token uri]
|
||||
(letfn [(process-group [group]
|
||||
(sequence (comp (partition-all default-partition-size)
|
||||
(map process-partition))
|
||||
group))
|
||||
|
||||
(process-partition [[part1 :as part]]
|
||||
{:file-id (:file-id part1)
|
||||
:page-id (:page-id part1)
|
||||
:name (:name part1)
|
||||
:token token
|
||||
:uri uri
|
||||
:type (:type part1)
|
||||
:scale (:scale part1)
|
||||
:objects (mapv part-entry->object part)})
|
||||
|
||||
(part-entry->object [entry]
|
||||
{:id (:object-id entry)
|
||||
:filename (:filename entry)
|
||||
:name (:name entry)
|
||||
:suffix (:suffix entry)})]
|
||||
|
||||
(let [xform (comp
|
||||
(map #(assoc % :token token))
|
||||
(assoc-file-name))]
|
||||
(->> (sequence xform exports)
|
||||
(d/group-by (juxt :scale :type))
|
||||
(map second)
|
||||
(into [] (mapcat process-group))))))
|
||||
|
|
|
@ -12,104 +12,33 @@
|
|||
["os" :as os]
|
||||
["path" :as path]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.shell :as sh]
|
||||
[app.util.mime :as mime]
|
||||
[cljs.core :as c]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn- get-path
|
||||
[type id]
|
||||
(path/join (os/tmpdir) (dm/str "exporter." (d/name type) "." id)))
|
||||
|
||||
(defn- get-mtype
|
||||
[type]
|
||||
|
||||
(case (d/name type)
|
||||
"zip" "application/zip"
|
||||
"pdf" "application/pdf"
|
||||
"svg" "image/svg+xml"
|
||||
"jpeg" "image/jpeg"
|
||||
"png" "image/png"))
|
||||
(path/join (os/tmpdir) (str/concat "exporter-resource." (c/name type) "." id)))
|
||||
|
||||
(defn create
|
||||
"Generates ephimeral resource object."
|
||||
[type name]
|
||||
(let [task-id (uuid/next)]
|
||||
{:path (get-path type task-id)
|
||||
:mtype (get-mtype type)
|
||||
:name name
|
||||
:id (dm/str (c/name type) "." task-id)}))
|
||||
|
||||
(defn- write-as-zip!
|
||||
[{:keys [id path]} items on-progress]
|
||||
(let [^js zip (arc/create "zip")
|
||||
^js out (fs/createWriteStream path)
|
||||
append! (fn [{:keys [data name] :as result}]
|
||||
(.append zip data #js {:name name}))
|
||||
progress (atom 0)]
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(.on zip "error" #(reject %))
|
||||
(.on zip "end" resolve)
|
||||
(.on zip "entry" (fn [data]
|
||||
(let [name (unchecked-get data "name")
|
||||
num (swap! progress inc)]
|
||||
;; Sample code used for testing failing exports
|
||||
#_(when (= 2 num)
|
||||
(.abort ^js zip)
|
||||
(reject (js/Error. "unable to create zip file")))
|
||||
(on-progress
|
||||
{:total (count items)
|
||||
:done num}))))
|
||||
(.pipe zip out)
|
||||
(-> (reduce (fn [res export-fn]
|
||||
(p/then res (fn [_] (-> (export-fn) (p/then append!)))))
|
||||
(p/resolved 1)
|
||||
items)
|
||||
(p/then #(.finalize zip))
|
||||
(p/catch reject))))))
|
||||
|
||||
(defn create-simple
|
||||
[& {:keys [task resource on-progress on-complete on-error]
|
||||
:or {on-progress identity
|
||||
on-complete identity
|
||||
on-error identity}
|
||||
:as params}]
|
||||
(let [path (:path resource)]
|
||||
(-> (task)
|
||||
(p/then (fn [{:keys [data name]}]
|
||||
(on-progress {:total 1 :done 1 :name name})
|
||||
(.writeFile fs/promises path data)))
|
||||
(p/then #(sh/stat path))
|
||||
(p/then #(merge resource %))
|
||||
(p/finally (fn [result cause]
|
||||
(if cause
|
||||
(on-error cause)
|
||||
(on-complete result)))))))
|
||||
|
||||
(defn create-zip
|
||||
"Creates a resource with multiple files merget into a single zip file."
|
||||
[& {:keys [resource tasks on-error on-progress on-complete]
|
||||
:or {on-error identity
|
||||
on-progress identity
|
||||
on-complete identity}}]
|
||||
(let [{:keys [path id] :as resource} resource]
|
||||
(-> (write-as-zip! resource tasks on-progress)
|
||||
(p/then #(sh/stat path))
|
||||
(p/then #(merge resource %))
|
||||
(p/finally (fn [result cause]
|
||||
(if cause
|
||||
(on-error cause)
|
||||
(on-complete result)))))))
|
||||
{:path (get-path type task-id)
|
||||
:mtype (mime/get type)
|
||||
:name name
|
||||
:filename (str/concat name (mime/get-extension type))
|
||||
:id (str/concat (c/name type) "." task-id)}))
|
||||
|
||||
(defn- lookup
|
||||
[id]
|
||||
(p/let [[type task-id] (str/split id "." 2)
|
||||
path (get-path type task-id)
|
||||
mtype (get-mtype type)
|
||||
mtype (mime/get (keyword type))
|
||||
stat (sh/stat path)]
|
||||
|
||||
(when-not stat
|
||||
|
@ -131,3 +60,25 @@
|
|||
(assoc :response/status 200)
|
||||
(assoc :response/body stream)
|
||||
(assoc :response/headers headers))))))
|
||||
|
||||
(defn create-zip
|
||||
[& {:keys [resource on-complete on-progress on-error]}]
|
||||
(let [^js zip (arc/create "zip")
|
||||
^js out (fs/createWriteStream (:path resource))
|
||||
progress (atom 0)]
|
||||
(.on zip "error" on-error)
|
||||
(.on zip "end" on-complete)
|
||||
(.on zip "entry" (fn [data]
|
||||
(let [name (unchecked-get data "name")
|
||||
num (swap! progress inc)]
|
||||
(on-progress {:done num :filename name}))))
|
||||
(.pipe zip out)
|
||||
zip))
|
||||
|
||||
(defn add-to-zip!
|
||||
[zip path name]
|
||||
(.file ^js zip path #js {:name name}))
|
||||
|
||||
(defn close-zip!
|
||||
[zip]
|
||||
(.finalize ^js zip))
|
||||
|
|
45
exporter/src/app/renderer.cljs
Normal file
45
exporter/src/app/renderer.cljs
Normal file
|
@ -0,0 +1,45 @@
|
|||
;; 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.renderer
|
||||
"Common renderer interface."
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.renderer.bitmap :as rb]
|
||||
[app.renderer.pdf :as rp]
|
||||
[app.renderer.svg :as rs]
|
||||
[cljs.spec.alpha :as s]))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::suffix ::us/string)
|
||||
(s/def ::type #{:jpeg :png :pdf :svg})
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::scale ::us/number)
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::uri ::us/uri)
|
||||
(s/def ::filename ::us/string)
|
||||
|
||||
(s/def ::object
|
||||
(s/keys :req-un [::id ::name ::suffix ::filename]))
|
||||
|
||||
(s/def ::objects
|
||||
(s/coll-of ::object :min-count 1))
|
||||
|
||||
(s/def ::render-params
|
||||
(s/keys :req-un [::file-id ::page-id ::scale ::token ::type ::objects]
|
||||
:opt-un [::uri]))
|
||||
|
||||
(defn- render
|
||||
[{:keys [type] :as params} on-object]
|
||||
(us/verify ::render-params params)
|
||||
(us/verify fn? on-object)
|
||||
(case type
|
||||
:png (rb/render params on-object)
|
||||
:jpeg (rb/render params on-object)
|
||||
:pdf (rp/render params on-object)
|
||||
:svg (rs/render params on-object)))
|
||||
|
|
@ -7,75 +7,61 @@
|
|||
(ns app.renderer.bitmap
|
||||
"A bitmap renderer."
|
||||
(:require
|
||||
["path" :as path]
|
||||
[app.browser :as bw]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex :include-macros true]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.util.mime :as mime]
|
||||
[app.util.shell :as sh]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn screenshot-object
|
||||
[{:keys [file-id page-id object-id token scale type uri]}]
|
||||
(p/let [params {:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id object-id
|
||||
:route "render-object"}
|
||||
|
||||
uri (-> (or uri (cf/get :public-uri))
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(bw/exec!
|
||||
#js {:screen #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:viewport #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:locale "en-US"
|
||||
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
||||
:deviceScaleFactor scale
|
||||
:userAgent bw/default-user-agent}
|
||||
(fn [page]
|
||||
(l/info :uri uri)
|
||||
(p/do!
|
||||
(bw/nav! page (str uri))
|
||||
(p/let [node (bw/select page "#screenshot")]
|
||||
(bw/wait-for node)
|
||||
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
||||
(bw/sleep page 2000) ; the good old fix with sleep
|
||||
(case type
|
||||
:png (bw/screenshot node {:omit-background? true :type type})
|
||||
:jpeg (bw/screenshot node {:omit-background? false :type type}))))))))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::suffix ::us/string)
|
||||
(s/def ::type #{:jpeg :png})
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::object-id ::us/uuid)
|
||||
(s/def ::scale ::us/number)
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::uri ::us/uri)
|
||||
|
||||
(s/def ::params
|
||||
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id]
|
||||
:opt-un [::uri]))
|
||||
|
||||
(defn render
|
||||
[params]
|
||||
(us/verify ::params params)
|
||||
(p/let [content (screenshot-object params)]
|
||||
{:data content
|
||||
:name (str (:name params)
|
||||
(:suffix params "")
|
||||
(case (:type params)
|
||||
:png ".png"
|
||||
:jpeg ".jpg"))
|
||||
:size (alength content)
|
||||
:mtype (case (:type params)
|
||||
:png "image/png"
|
||||
:jpeg "image/jpeg")}))
|
||||
[{:keys [file-id page-id token scale type uri objects] :as params} on-object]
|
||||
(letfn [(prepare-options [uri]
|
||||
#js {:screen #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:viewport #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:locale "en-US"
|
||||
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
||||
:deviceScaleFactor scale
|
||||
:userAgent bw/default-user-agent})
|
||||
|
||||
(render-object [page {:keys [id] :as object}]
|
||||
(p/let [tmpdir (sh/mktmpdir! "bitmap-render")
|
||||
path (path/join tmpdir (str/concat id (mime/get-extension type)))
|
||||
node (bw/select page (str/concat "#screenshot-" id))]
|
||||
(bw/wait-for node)
|
||||
(case type
|
||||
:png (bw/screenshot node {:omit-background? true :type type :path path})
|
||||
:jpeg (bw/screenshot node {:omit-background? false :type type :path path}))
|
||||
(on-object (assoc object :path path))))
|
||||
|
||||
(render [uri page]
|
||||
(l/info :uri uri)
|
||||
(p/do
|
||||
;; navigate to the page and perform basic setup
|
||||
(bw/nav! page (str uri))
|
||||
(bw/sleep page 1000) ; the good old fix with sleep
|
||||
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
||||
|
||||
;; take the screnshot of requested objects, one by one
|
||||
(p/run! (partial render-object page) objects)
|
||||
nil))]
|
||||
|
||||
(p/let [params {:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id (mapv :id objects)
|
||||
:route "objects"}
|
||||
uri (-> (or uri (cf/get :public-uri))
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(bw/exec! (prepare-options uri) (partial render uri)))))
|
||||
|
|
|
@ -7,68 +7,62 @@
|
|||
(ns app.renderer.pdf
|
||||
"A pdf renderer."
|
||||
(:require
|
||||
["path" :as path]
|
||||
[app.browser :as bw]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex :include-macros true]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.util.mime :as mime]
|
||||
[app.util.shell :as sh]
|
||||
[cuerdas.core :as str]
|
||||
[cljs.spec.alpha :as s]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn pdf-from-object
|
||||
[{:keys [file-id page-id object-id token scale type save-path uri] :as params}]
|
||||
(p/let [params {:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id object-id
|
||||
:route "render-object"}
|
||||
uri (-> (or uri (cf/get :public-uri))
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
(bw/exec!
|
||||
#js {:screen #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:viewport #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:locale "en-US"
|
||||
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
||||
:deviceScaleFactor scale
|
||||
:userAgent bw/default-user-agent}
|
||||
(fn [page]
|
||||
(l/info :uri uri)
|
||||
(p/do!
|
||||
(bw/nav! page uri)
|
||||
(p/let [dom (bw/select page "#screenshot")]
|
||||
(bw/wait-for dom)
|
||||
(bw/screenshot dom {:full-page? true})
|
||||
(bw/sleep page 2000) ; the good old fix with sleep
|
||||
(if save-path
|
||||
(bw/pdf page {:save-path save-path})
|
||||
(bw/pdf page))))))))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::suffix ::us/string)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::object-id ::us/uuid)
|
||||
(s/def ::scale ::us/number)
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::save-path ::us/string)
|
||||
(s/def ::uri ::us/uri)
|
||||
|
||||
(s/def ::render-params
|
||||
(s/keys :req-un [::name ::suffix ::object-id ::page-id ::scale ::token ::file-id]
|
||||
:opt-un [::save-path ::uri]))
|
||||
|
||||
(defn render
|
||||
[params]
|
||||
(us/assert ::render-params params)
|
||||
(p/let [content (pdf-from-object params)]
|
||||
{:data content
|
||||
:name (str (:name params)
|
||||
(:suffix params "")
|
||||
".pdf")
|
||||
:size (alength content)
|
||||
:mtype "application/pdf"}))
|
||||
[{:keys [file-id page-id token scale type uri objects] :as params} on-object]
|
||||
(letfn [(prepare-options [uri]
|
||||
#js {:screen #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:viewport #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:locale "en-US"
|
||||
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
||||
:deviceScaleFactor scale
|
||||
:userAgent bw/default-user-agent})
|
||||
|
||||
(prepare-uri [base-uri object-id]
|
||||
(let [params {:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id object-id
|
||||
:route "objects"}]
|
||||
(-> base-uri
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))))
|
||||
|
||||
(render-object [page base-uri {:keys [id] :as object}]
|
||||
(p/let [uri (prepare-uri base-uri id)
|
||||
tmp (sh/mktmpdir! "pdf-render")
|
||||
path (path/join tmp (str/concat id (mime/get-extension type)))]
|
||||
(l/info :uri uri)
|
||||
(bw/nav! page uri)
|
||||
(p/let [dom (bw/select page (dm/str "#screenshot-" id))]
|
||||
(bw/wait-for dom)
|
||||
(bw/screenshot dom {:full-page? true})
|
||||
(bw/sleep page 2000) ; the good old fix with sleep
|
||||
(bw/pdf page {:path path})
|
||||
path)))
|
||||
|
||||
(render [base-uri page]
|
||||
(p/loop [objects (seq objects)]
|
||||
(when-let [object (first objects)]
|
||||
(p/let [uri (prepare-uri base-uri (:id object))
|
||||
path (render-object page base-uri object)]
|
||||
(on-object (assoc object :path path))
|
||||
(p/recur (rest objects))))))]
|
||||
|
||||
(let [base-uri (or uri (cf/get :public-uri))]
|
||||
(bw/exec! (prepare-options base-uri)
|
||||
(partial render base-uri)))))
|
||||
|
|
|
@ -10,12 +10,14 @@
|
|||
["xml-js" :as xml]
|
||||
[app.browser :as bw]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex :include-macros true]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.util.mime :as mime]
|
||||
[app.util.shell :as sh]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.walk :as walk]
|
||||
|
@ -111,9 +113,8 @@
|
|||
{:width width
|
||||
:height height}))
|
||||
|
||||
|
||||
(defn- render-object
|
||||
[{:keys [page-id file-id object-id token scale suffix type uri]}]
|
||||
(defn render
|
||||
[{:keys [page-id file-id objects token scale suffix type uri]} on-object]
|
||||
(letfn [(convert-to-ppm [pngpath]
|
||||
(l/trace :fn :convert-to-ppm)
|
||||
(let [basepath (path/dirname pngpath)
|
||||
|
@ -246,7 +247,7 @@
|
|||
|
||||
(trace-node [{:keys [data] :as node}]
|
||||
(l/trace :fn :trace-node)
|
||||
(p/let [tdpath (sh/create-tmpdir! "svgexport-")
|
||||
(p/let [tdpath (sh/mktmpdir! "svgexport")
|
||||
pngpath (path/join tdpath "origin.png")
|
||||
_ (sh/write-file! pngpath data)
|
||||
ppmpath (convert-to-ppm pngpath)
|
||||
|
@ -293,88 +294,78 @@
|
|||
(sh/rmdir! tempdir)
|
||||
(dissoc node :tempdir)))
|
||||
|
||||
(process-text-node [page item]
|
||||
(extract-txt-node [page item]
|
||||
(-> (p/resolved item)
|
||||
(p/then (partial resolve-text-node page))
|
||||
(p/then extract-single-node)
|
||||
(p/then trace-node)
|
||||
(p/then clean-temp-data)))
|
||||
|
||||
(process-text-nodes [page]
|
||||
(extract-txt-nodes [page {:keys [id] :as objects}]
|
||||
(l/trace :fn :process-text-nodes)
|
||||
(-> (bw/select-all page "#screenshot foreignObject")
|
||||
(p/then (fn [nodes] (p/all (map (partial process-text-node page) nodes))))))
|
||||
(-> (bw/select-all page (str/concat "#screenshot-" id " foreignObject"))
|
||||
(p/then (fn [nodes] (p/all (map (partial extract-txt-node page) nodes))))
|
||||
(p/then (fn [nodes] (d/index-by :id nodes)))))
|
||||
|
||||
(extract [page]
|
||||
(p/let [dom (bw/select page "#screenshot")
|
||||
xmldata (bw/eval! dom (fn [elem] (.-outerHTML ^js elem)))
|
||||
nodes (process-text-nodes page)
|
||||
nodes (d/index-by :id nodes)
|
||||
result (replace-text-nodes xmldata nodes)
|
||||
(extract-svg [page {:keys [id] :as object}]
|
||||
(let [node (bw/select page (str/concat "#screenshot-" id))]
|
||||
(bw/wait-for node)
|
||||
(bw/eval! node (fn [elem] (.-outerHTML ^js elem)))))
|
||||
|
||||
;; SVG standard don't allow the entity nbsp.   is equivalent but
|
||||
;; compatible with SVG
|
||||
result (str/replace result " " " ")]
|
||||
;; (println "------- ORIGIN:")
|
||||
;; (cljs.pprint/pprint (xml->clj xmldata))
|
||||
;; (println "------- RESULT:")
|
||||
;; (cljs.pprint/pprint (xml->clj result))
|
||||
;; (println "-------")
|
||||
result))
|
||||
]
|
||||
(prepare-options [uri]
|
||||
#js {:screen #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:viewport #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:locale "en-US"
|
||||
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
||||
:deviceScaleFactor scale
|
||||
:userAgent bw/default-user-agent})
|
||||
|
||||
(p/let [params {:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id object-id
|
||||
:render-texts true
|
||||
:embed true
|
||||
:route "render-object"}
|
||||
(render-object [page {:keys [id] :as object}]
|
||||
(p/let [tmpdir (sh/mktmpdir! "svg-render")
|
||||
path (path/join tmpdir (str/concat id (mime/get-extension type)))
|
||||
node (bw/select page (str/concat "#screenshot-" id))]
|
||||
(bw/wait-for node)
|
||||
(p/let [xmldata (extract-svg page object)
|
||||
txtdata (extract-txt-nodes page object)
|
||||
result (replace-text-nodes xmldata txtdata)
|
||||
|
||||
uri (-> (or uri (cf/get :public-uri))
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
;; SVG standard don't allow the entity
|
||||
;; nbsp.   is equivalent but compatible
|
||||
;; with SVG.
|
||||
result (str/replace result " " " ")]
|
||||
|
||||
(bw/exec!
|
||||
#js {:screen #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:viewport #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:locale "en-US"
|
||||
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
||||
:deviceScaleFactor scale
|
||||
:userAgent bw/default-user-agent}
|
||||
(fn [page]
|
||||
(l/info :uri uri)
|
||||
(p/do!
|
||||
(bw/nav! page uri)
|
||||
(p/let [dom (bw/select page "#screenshot")]
|
||||
(bw/wait-for dom)
|
||||
(bw/sleep page 2000))
|
||||
;; (println "------- ORIGIN:")
|
||||
;; (cljs.pprint/pprint (xml->clj xmldata))
|
||||
;; (println "------- RESULT:")
|
||||
;; (cljs.pprint/pprint (xml->clj result))
|
||||
;; (println "-------")
|
||||
|
||||
(extract page)))))))
|
||||
(sh/write-file! path result)
|
||||
(on-object (assoc object :path path))
|
||||
path)))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::suffix ::us/string)
|
||||
(s/def ::type #{:svg})
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::object-id ::us/uuid)
|
||||
(s/def ::scale ::us/number)
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::uri ::us/uri)
|
||||
(render [uri page]
|
||||
(l/info :uri uri)
|
||||
(p/do
|
||||
;; navigate to the page and perform basic setup
|
||||
(bw/nav! page (str uri))
|
||||
(bw/sleep page 1000) ; the good old fix with sleep
|
||||
|
||||
(s/def ::params
|
||||
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::file-id ::scale ::token]
|
||||
:opt-un [::uri]))
|
||||
;; take the screnshot of requested objects, one by one
|
||||
(p/run! (partial render-object page) objects)
|
||||
nil))]
|
||||
|
||||
(defn render
|
||||
[params]
|
||||
(us/assert ::params params)
|
||||
(p/let [content (render-object params)]
|
||||
{:data content
|
||||
:name (str (:name params)
|
||||
(:suffix params "")
|
||||
".svg")
|
||||
:size (alength content)
|
||||
:mtype "image/svg+xml"}))
|
||||
(p/let [params {:file-id file-id
|
||||
:page-id page-id
|
||||
:render-texts true
|
||||
:render-embed true
|
||||
:object-id (mapv :id objects)
|
||||
:route "objects"}
|
||||
uri (-> (or uri (cf/get :public-uri))
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(bw/exec! (prepare-options uri)
|
||||
(partial render uri)))))
|
||||
|
||||
|
|
32
exporter/src/app/util/mime.cljs
Normal file
32
exporter/src/app/util/mime.cljs
Normal file
|
@ -0,0 +1,32 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.util.mime
|
||||
"Mimetype and file extension helpers."
|
||||
(:refer-clojure :exclude [get])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[cljs.core :as c]))
|
||||
|
||||
(defn get-extension
|
||||
[type]
|
||||
(case type
|
||||
:png ".png"
|
||||
:jpeg ".jpg"
|
||||
:svg ".svg"
|
||||
:pdf ".pdf"
|
||||
:zip ".zip"))
|
||||
|
||||
(defn- get
|
||||
[type]
|
||||
(case type
|
||||
:zip "application/zip"
|
||||
:pdf "application/pdf"
|
||||
:svg "image/svg+xml"
|
||||
:jpeg "image/jpeg"
|
||||
:png "image/png"))
|
||||
|
||||
|
|
@ -16,12 +16,9 @@
|
|||
|
||||
(l/set-level! :trace)
|
||||
|
||||
(defn create-tmpdir!
|
||||
(defn mktmpdir!
|
||||
[prefix]
|
||||
(-> (.mkdtemp fs/promises prefix)
|
||||
(p/then (fn [result]
|
||||
(path/join (os/tmpdir) result)))))
|
||||
|
||||
(.mkdtemp fs/promises (path/join (os/tmpdir) prefix)))
|
||||
|
||||
(defn move!
|
||||
[origin-path dest-path]
|
||||
|
|
|
@ -5,15 +5,17 @@
|
|||
|
||||
org.clojure/clojure {:mvn/version "1.10.3"}
|
||||
binaryage/devtools {:mvn/version "RELEASE"}
|
||||
metosin/reitit-core {:mvn/version "0.5.15"}
|
||||
metosin/reitit-core {:mvn/version "0.5.17"}
|
||||
|
||||
funcool/beicon {:mvn/version "2021.07.05-1"}
|
||||
funcool/okulary {:mvn/version "2020.04.14-0"}
|
||||
funcool/okulary {:mvn/version "2022.04.01-10"}
|
||||
funcool/potok {:mvn/version "2021.09.20-0"}
|
||||
funcool/rumext {:mvn/version "2022.01.20.128"}
|
||||
funcool/rumext {:mvn/version "2022.03.31-133"}
|
||||
funcool/tubax {:mvn/version "2021.05.20-0"}
|
||||
|
||||
instaparse/instaparse {:mvn/version "1.4.10"}
|
||||
garden/garden {:mvn/version "1.3.10"}
|
||||
|
||||
}
|
||||
|
||||
:aliases
|
||||
|
@ -30,9 +32,9 @@
|
|||
:dev
|
||||
{:extra-paths ["dev"]
|
||||
:extra-deps
|
||||
{thheller/shadow-cljs {:mvn/version "2.17.5"}
|
||||
{thheller/shadow-cljs {:mvn/version "2.17.8"}
|
||||
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||
cider/cider-nrepl {:mvn/version "0.28.2"}}}
|
||||
cider/cider-nrepl {:mvn/version "0.28.3"}}}
|
||||
|
||||
:shadow-cljs
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
"test-e2e-gui": "cypress open"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.2",
|
||||
"cypress": "^9.5.0",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"cypress": "^9.5.3",
|
||||
"cypress-file-upload": "^5.0.8",
|
||||
"gettext-parser": "^4.2.0",
|
||||
"gulp": "4.0.2",
|
||||
|
@ -43,12 +43,12 @@
|
|||
"mkdirp": "^1.0.4",
|
||||
"nodemon": "^2.0.15",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.6",
|
||||
"postcss": "^8.4.12",
|
||||
"postcss-clean": "^1.2.2",
|
||||
"prettier": "^2.5.1",
|
||||
"prettier": "^2.6.1",
|
||||
"rimraf": "^3.0.0",
|
||||
"sass": "^1.49.7",
|
||||
"shadow-cljs": "2.17.5"
|
||||
"sass": "^1.49.9",
|
||||
"shadow-cljs": "2.17.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/browser": "^6.17.4",
|
||||
|
@ -56,16 +56,16 @@
|
|||
"date-fns": "^2.28.0",
|
||||
"draft-js": "^0.11.7",
|
||||
"highlight.js": "^11.4.0",
|
||||
"js-beautify": "^1.14.0",
|
||||
"js-beautify": "^1.14.2",
|
||||
"jszip": "^3.6.0",
|
||||
"luxon": "^2.3.0",
|
||||
"luxon": "^2.3.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"opentype.js": "^1.3.4",
|
||||
"randomcolor": "^0.6.2",
|
||||
"react": "~17.0.2",
|
||||
"react-dom": "~17.0.2",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"rxjs": "~7.5.2",
|
||||
"rxjs": "~7.5.5",
|
||||
"sax": "^1.2.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tdigest": "^0.1.1",
|
||||
|
|
|
@ -82,7 +82,10 @@
|
|||
}
|
||||
|
||||
.tool-window-bar-icon {
|
||||
height: 15px;
|
||||
height: 21px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 15px;
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
window.penpotTranslations = JSON.parse({{& translations}});
|
||||
window.penpotThemes = {{& themes}};
|
||||
window.penpotVersion = "%version%";
|
||||
window.penpotBuildDate = "%buildDate%";
|
||||
</script>
|
||||
|
||||
{{# manifest}}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
set -ex
|
||||
|
||||
CURRENT_VERSION=$1;
|
||||
BUILD_DATE=$(date -R);
|
||||
CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
|
||||
EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS;
|
||||
|
||||
|
@ -14,4 +15,4 @@ npx gulp dist:clean || exit 1;
|
|||
npx gulp dist:copy || exit 1;
|
||||
|
||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/index.html;
|
||||
|
||||
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html;
|
||||
|
|
|
@ -68,6 +68,13 @@
|
|||
(-> (obj/get global "penpotVersion")
|
||||
(v/parse)))
|
||||
|
||||
(defn parse-build-date
|
||||
[global]
|
||||
(let [date (obj/get global "penpotBuildDate")]
|
||||
(if (= date "%buildDate%")
|
||||
"unknown"
|
||||
date)))
|
||||
|
||||
;; --- Globar Config Vars
|
||||
|
||||
(def default-theme "default")
|
||||
|
@ -83,6 +90,7 @@
|
|||
(def sentry-dsn (obj/get global "penpotSentryDsn"))
|
||||
(def onboarding-form-id (obj/get global "penpotOnboardingQuestionsFormId"))
|
||||
|
||||
(def build-date (parse-build-date global))
|
||||
(def flags (atom (parse-flags global)))
|
||||
(def version (atom (parse-version global)))
|
||||
(def target (atom (parse-target global)))
|
||||
|
|
|
@ -33,7 +33,10 @@
|
|||
(log/set-level! :app :info)
|
||||
|
||||
(when (= :browser @cf/target)
|
||||
(log/info :message "Welcome to penpot" :version (:full @cf/version) :public-uri (str cf/public-uri)))
|
||||
(log/info :message "Welcome to penpot"
|
||||
:version (:full @cf/version)
|
||||
:build-date cf/build-date
|
||||
:public-uri (str cf/public-uri)))
|
||||
|
||||
(declare reinit)
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
(ns app.main.data.exports
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.persistence :as dwp]
|
||||
|
@ -47,6 +46,7 @@
|
|||
state
|
||||
(dissoc state :export))))))
|
||||
|
||||
|
||||
(defn show-workspace-export-dialog
|
||||
([] (show-workspace-export-dialog nil))
|
||||
([{:keys [selected]}]
|
||||
|
@ -55,8 +55,6 @@
|
|||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)
|
||||
|
||||
filename (-> (wsh/lookup-page state page-id) :name)
|
||||
selected (or selected (wsh/lookup-selected state page-id {}))
|
||||
|
||||
shapes (if (seq selected)
|
||||
|
@ -74,11 +72,10 @@
|
|||
(assoc :name (:name shape))))]
|
||||
|
||||
(rx/of (modal/show :export-shapes
|
||||
{:exports (vec exports)
|
||||
:filename filename})))))))
|
||||
{:exports (vec exports)})))))))
|
||||
|
||||
(defn show-viewer-export-dialog
|
||||
[{:keys [shapes filename page-id file-id exports]}]
|
||||
[{:keys [shapes page-id file-id exports]}]
|
||||
(ptk/reify ::show-viewer-export-dialog
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
|
@ -91,51 +88,44 @@
|
|||
(assoc :object-id (:id shape))
|
||||
(assoc :shape (dissoc shape :exports))
|
||||
(assoc :name (:name shape))))]
|
||||
(rx/of (modal/show :export-shapes {:exports (vec exports)
|
||||
:filename filename}))))))
|
||||
(rx/of (modal/show :export-shapes {:exports (vec exports)}))))))
|
||||
|
||||
(defn show-workspace-export-frames-dialog
|
||||
([frames]
|
||||
(ptk/reify ::show-workspace-export-frames-dialog
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)
|
||||
filename (-> (wsh/lookup-page state page-id)
|
||||
:name
|
||||
(dm/str ".pdf"))
|
||||
[frames]
|
||||
(ptk/reify ::show-workspace-export-frames-dialog
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)
|
||||
exports (for [frame frames]
|
||||
{:enabled true
|
||||
:page-id page-id
|
||||
:file-id file-id
|
||||
:object-id (:id frame)
|
||||
:shape frame
|
||||
:name (:name frame)})]
|
||||
|
||||
exports (for [frame frames]
|
||||
{:enabled true
|
||||
:page-id page-id
|
||||
:file-id file-id
|
||||
:frame-id (:id frame)
|
||||
:shape frame
|
||||
:name (:name frame)})]
|
||||
|
||||
(rx/of (modal/show :export-frames
|
||||
{:exports (vec exports)
|
||||
:filename filename})))))))
|
||||
(rx/of (modal/show :export-frames
|
||||
{:exports (vec exports)}))))))
|
||||
|
||||
(defn- initialize-export-status
|
||||
[exports filename resource-id query-name]
|
||||
[exports cmd resource]
|
||||
(ptk/reify ::initialize-export-status
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :export {:in-progress true
|
||||
:resource-id resource-id
|
||||
:resource-id (:id resource)
|
||||
:healthy? true
|
||||
:error false
|
||||
:progress 0
|
||||
:widget-visible true
|
||||
:detail-visible true
|
||||
:exports exports
|
||||
:filename filename
|
||||
:last-update (dt/now)
|
||||
:query-name query-name}))))
|
||||
:cmd cmd}))))
|
||||
|
||||
(defn- update-export-status
|
||||
[{:keys [progress status resource-id name] :as data}]
|
||||
[{:keys [done status resource-id filename] :as data}]
|
||||
(ptk/reify ::update-export-status
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
@ -144,10 +134,10 @@
|
|||
healthy? (< time-diff (dt/duration {:seconds 6}))]
|
||||
(cond-> state
|
||||
(= status "running")
|
||||
(update :export assoc :progress (:done progress) :last-update (dt/now) :healthy? healthy?)
|
||||
(update :export assoc :progress done :last-update (dt/now) :healthy? healthy?)
|
||||
|
||||
(= status "error")
|
||||
(update :export assoc :error (:cause data) :last-update (dt/now) :healthy? healthy?)
|
||||
(update :export assoc :in-progress false :error (:cause data) :last-update (dt/now) :healthy? healthy?)
|
||||
|
||||
(= status "ended")
|
||||
(update :export assoc :in-progress false :last-update (dt/now) :healthy? healthy?))))
|
||||
|
@ -155,12 +145,12 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(when (= status "ended")
|
||||
(->> (rp/query! :download-export-resource resource-id)
|
||||
(->> (rp/query! :exporter {:cmd :get-resource :blob? true :id resource-id})
|
||||
(rx/delay 500)
|
||||
(rx/map #(dom/trigger-download name %)))))))
|
||||
(rx/map #(dom/trigger-download filename %)))))))
|
||||
|
||||
(defn request-simple-export
|
||||
[{:keys [export filename]}]
|
||||
[{:keys [export]}]
|
||||
(ptk/reify ::request-simple-export
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
@ -170,22 +160,26 @@
|
|||
(watch [_ state _]
|
||||
(let [profile-id (:profile-id state)
|
||||
params {:exports [export]
|
||||
:profile-id profile-id}]
|
||||
:profile-id profile-id
|
||||
:cmd :export-shapes
|
||||
:wait true}]
|
||||
(rx/concat
|
||||
(rx/of ::dwp/force-persist)
|
||||
(->> (rp/query! :export-shapes-simple params)
|
||||
(rx/map (fn [data]
|
||||
(dom/trigger-download filename data)
|
||||
(clear-export-state uuid/zero)))
|
||||
(->> (rp/query! :exporter params)
|
||||
(rx/mapcat (fn [{:keys [id filename]}]
|
||||
(->> (rp/query! :exporter {:cmd :get-resource :blob? true :id id})
|
||||
(rx/map (fn [data]
|
||||
(dom/trigger-download filename data)
|
||||
(clear-export-state uuid/zero))))))
|
||||
(rx/catch (fn [cause]
|
||||
(prn "KKKK" cause)
|
||||
(rx/concat
|
||||
(rx/of (clear-export-state uuid/zero))
|
||||
(rx/throw cause))))))))))
|
||||
|
||||
|
||||
(defn request-multiple-export
|
||||
[{:keys [filename exports query-name]
|
||||
:or {query-name :export-shapes-multiple}
|
||||
[{:keys [exports cmd]
|
||||
:or {cmd :export-shapes}
|
||||
:as params}]
|
||||
(ptk/reify ::request-multiple-export
|
||||
ptk/WatchEvent
|
||||
|
@ -194,7 +188,7 @@
|
|||
profile-id (:profile-id state)
|
||||
ws-conn (:ws-conn state)
|
||||
params {:exports exports
|
||||
:name filename
|
||||
:cmd cmd
|
||||
:profile-id profile-id
|
||||
:wait false}
|
||||
|
||||
|
@ -219,11 +213,10 @@
|
|||
|
||||
;; Launch the exportation process and stores the resource id
|
||||
;; locally.
|
||||
(->> (rp/query! query-name params)
|
||||
(rx/tap (fn [{:keys [id]}]
|
||||
(vreset! resource-id id)))
|
||||
(rx/map (fn [{:keys [id]}]
|
||||
(initialize-export-status exports filename id query-name))))
|
||||
(->> (rp/query! :exporter params)
|
||||
(rx/map (fn [{:keys [id] :as resource}]
|
||||
(vreset! resource-id id)
|
||||
(initialize-export-status exports cmd resource))))
|
||||
|
||||
;; We proceed to update the export state with incoming
|
||||
;; progress updates. We delay the stoper for give some time
|
||||
|
@ -246,13 +239,12 @@
|
|||
(rx/map #(clear-export-state @resource-id))
|
||||
(rx/take-until (rx/delay 6000 stoper))))))))
|
||||
|
||||
|
||||
(defn retry-last-export
|
||||
[]
|
||||
(ptk/reify ::retry-last-export
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [params (select-keys (:export state) [:filename :exports :query-name])]
|
||||
(let [params (select-keys (:export state) [:exports :cmd])]
|
||||
(when (seq params)
|
||||
(rx/of (request-multiple-export params)))))))
|
||||
|
||||
|
|
|
@ -964,18 +964,23 @@
|
|||
(ptk/reify ::toggle-file-thumbnail-selected
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [selected (wsh/lookup-selected state)
|
||||
pages (-> state :workspace-data :pages-index vals)
|
||||
extract (fn [{:keys [objects id] :as page}]
|
||||
(->> (cph/get-frames objects)
|
||||
(filter :file-thumbnail)
|
||||
(map :id)
|
||||
(remove selected)
|
||||
(map (fn [frame-id] [id frame-id]))))]
|
||||
(let [selected (wsh/lookup-selected state)
|
||||
pages (-> state :workspace-data :pages-index vals)
|
||||
get-frames (fn [{:keys [objects id] :as page}]
|
||||
(->> (cph/get-frames objects)
|
||||
(sequence
|
||||
(comp (filter :use-for-thumbnail?)
|
||||
(map :id)
|
||||
(remove selected)
|
||||
(map (partial vector id))))))]
|
||||
|
||||
(rx/concat
|
||||
(rx/from (for [[page-id frame-id] (mapcat extract pages)]
|
||||
(dch/update-shapes [frame-id] #(dissoc % :file-thumbnail) page-id nil)))
|
||||
(rx/of (dch/update-shapes selected #(assoc % :file-thumbnail true))))))))
|
||||
(rx/from
|
||||
(->> (mapcat get-frames pages)
|
||||
(d/group-by first second)
|
||||
(map (fn [[page-id frame-ids]]
|
||||
(dch/update-shapes frame-ids #(dissoc % :use-for-thumbnail?) {:page-id page-id})))))
|
||||
(rx/of (dch/update-shapes selected #(update % :use-for-thumbnail? not))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Navigation
|
||||
|
|
|
@ -32,10 +32,9 @@
|
|||
(def commit-changes? (ptk/type? ::commit-changes))
|
||||
|
||||
(defn update-shapes
|
||||
([ids update-fn] (update-shapes ids update-fn nil nil))
|
||||
([ids update-fn keys] (update-shapes ids update-fn nil keys))
|
||||
([ids update-fn page-id {:keys [reg-objects? save-undo? attrs ignore-tree]
|
||||
:or {reg-objects? false save-undo? true attrs nil}}]
|
||||
([ids update-fn] (update-shapes ids update-fn nil))
|
||||
([ids update-fn {:keys [reg-objects? save-undo? attrs ignore-tree page-id]
|
||||
:or {reg-objects? false save-undo? true}}]
|
||||
|
||||
(us/assert ::coll-of-uuid ids)
|
||||
(us/assert fn? update-fn)
|
||||
|
@ -49,20 +48,15 @@
|
|||
|
||||
changes (reduce
|
||||
(fn [changes id]
|
||||
(pcb/update-shapes changes
|
||||
[id]
|
||||
update-fn
|
||||
{:attrs attrs
|
||||
:ignore-geometry? (get ignore-tree id)}))
|
||||
(let [opts {:attrs attrs :ignore-geometry? (get ignore-tree id)}]
|
||||
(pcb/update-shapes changes [id] update-fn opts)))
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/set-save-undo? save-undo?)
|
||||
(pcb/with-objects objects))
|
||||
ids)]
|
||||
|
||||
(when (seq (:redo-changes changes))
|
||||
(let [changes (cond-> changes
|
||||
reg-objects?
|
||||
(pcb/resize-parents ids))]
|
||||
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))]
|
||||
(rx/of (commit-changes changes)))))))))
|
||||
|
||||
(defn update-indices
|
||||
|
|
|
@ -55,8 +55,8 @@
|
|||
(->> stream
|
||||
(rx/filter (ptk/type? ::dws/message))
|
||||
(rx/map deref)
|
||||
(rx/map process-message)
|
||||
(rx/filter #(= subs-id (:subs-id %))))
|
||||
(rx/filter #(= subs-id (:subs-id %)))
|
||||
(rx/map process-message))
|
||||
|
||||
;; On reconnect, send again the subscription messages
|
||||
(->> stream
|
||||
|
@ -158,9 +158,7 @@
|
|||
(update-presence [presence]
|
||||
(-> presence
|
||||
(update session-id update-session presence)
|
||||
(d/without-nils)))
|
||||
|
||||
]
|
||||
(d/without-nils)))]
|
||||
|
||||
(ptk/reify ::handle-presence
|
||||
ptk/UpdateEvent
|
||||
|
|
|
@ -20,39 +20,45 @@
|
|||
([state]
|
||||
(lookup-page-objects state (:current-page-id state)))
|
||||
([state page-id]
|
||||
(get-in state [:workspace-data :pages-index page-id :objects])))
|
||||
(dm/get-in state [:workspace-data :pages-index page-id :objects])))
|
||||
|
||||
(defn lookup-page-options
|
||||
([state]
|
||||
(lookup-page-options state (:current-page-id state)))
|
||||
([state page-id]
|
||||
(get-in state [:workspace-data :pages-index page-id :options])))
|
||||
(dm/get-in state [:workspace-data :pages-index page-id :options])))
|
||||
|
||||
(defn lookup-component-objects
|
||||
([state component-id]
|
||||
(get-in state [:workspace-data :components component-id :objects])))
|
||||
(dm/get-in state [:workspace-data :components component-id :objects])))
|
||||
|
||||
(defn lookup-local-components
|
||||
([state]
|
||||
(get-in state [:workspace-data :components])))
|
||||
(dm/get-in state [:workspace-data :components])))
|
||||
|
||||
(defn process-selected-shapes
|
||||
([objects selected]
|
||||
(process-selected-shapes objects selected nil))
|
||||
|
||||
([objects selected {:keys [omit-blocked?] :or {omit-blocked? false}}]
|
||||
(letfn [(selectable? [id]
|
||||
(and (contains? objects id)
|
||||
(or (not omit-blocked?)
|
||||
(not (get-in objects [id :blocked] false)))))]
|
||||
(let [selected (->> selected (cph/clean-loops objects))]
|
||||
(into (d/ordered-set)
|
||||
(filter selectable?)
|
||||
selected)))))
|
||||
|
||||
;; TODO: improve performance of this
|
||||
(defn lookup-selected
|
||||
([state]
|
||||
(lookup-selected state nil))
|
||||
([state options]
|
||||
(lookup-selected state (:current-page-id state) options))
|
||||
([state page-id {:keys [omit-blocked?] :or {omit-blocked? false}}]
|
||||
([state page-id options]
|
||||
(let [objects (lookup-page-objects state page-id)
|
||||
selected (->> (dm/get-in state [:workspace-local :selected])
|
||||
(cph/clean-loops objects))
|
||||
selectable? (fn [id]
|
||||
(and (contains? objects id)
|
||||
(or (not omit-blocked?)
|
||||
(not (get-in objects [id :blocked] false)))))]
|
||||
(into (d/ordered-set)
|
||||
(filter selectable?)
|
||||
selected))))
|
||||
selected (dm/get-in state [:workspace-local :selected])]
|
||||
(process-selected-shapes objects selected options))))
|
||||
|
||||
(defn lookup-shapes
|
||||
([state ids]
|
||||
|
@ -79,7 +85,7 @@
|
|||
[state file-id]
|
||||
(if (= file-id (:current-file-id state))
|
||||
(get state :workspace-data)
|
||||
(get-in state [:workspace-libraries file-id :data])))
|
||||
(dm/get-in state [:workspace-libraries file-id :data])))
|
||||
|
||||
(defn get-libraries
|
||||
"Retrieve all libraries, including the local file."
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
(ns app.main.errors
|
||||
"Generic error handling"
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pprint :as pp]
|
||||
[app.config :as cf]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.users :as du]
|
||||
|
@ -17,8 +19,6 @@
|
|||
[app.util.i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.timers :as ts]
|
||||
[expound.alpha :as expound]
|
||||
[fipp.edn :as fpp]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(defn on-error
|
||||
|
@ -60,16 +60,15 @@
|
|||
(defmethod ptk/handle-error :validation
|
||||
[error]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(msg/show {:content "Unexpected validation error."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
#(st/emit! (msg/show {:content "Validation error"
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
|
||||
;; Print to the console some debug info.
|
||||
(js/console.group "Validation Error:")
|
||||
(ex/ignoring
|
||||
(js/console.info
|
||||
(with-out-str (fpp/pprint (dissoc error :explain)))))
|
||||
(pp/pprint-str (dissoc error :explain))))
|
||||
|
||||
(when-let [explain (:explain error)]
|
||||
(js/console.group "Spec explain:")
|
||||
|
@ -79,24 +78,46 @@
|
|||
(js/console.groupEnd "Validation Error:"))
|
||||
|
||||
|
||||
;; All the errors that happens on worker are handled here.
|
||||
(defmethod ptk/handle-error :worker-error
|
||||
[{:keys [code data hint] :as error}]
|
||||
(let [hint (or hint (:hint data) (:message data) (d/name code))
|
||||
info (pp/pprint-str (dissoc data :explain))
|
||||
msg (dm/str "Internal Worker Error: " hint)]
|
||||
|
||||
(ts/schedule
|
||||
#(st/emit!
|
||||
(msg/show {:content "Something wrong has happened (on worker)."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
|
||||
(js/console.group msg)
|
||||
(js/console.info info)
|
||||
|
||||
(when-let [explain (:explain data)]
|
||||
(js/console.group "Spec explain:")
|
||||
(js/console.log explain)
|
||||
(js/console.groupEnd "Spec explain:"))
|
||||
|
||||
(js/console.groupEnd msg)))
|
||||
|
||||
|
||||
;; Error on parsing an SVG
|
||||
;; TODO: looks unused and deprecated
|
||||
(defmethod ptk/handle-error :svg-parser
|
||||
[_]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(msg/show {:content "SVG is invalid or malformed"
|
||||
:type :error
|
||||
:timeout 3000}))))
|
||||
#(st/emit! (msg/show {:content "SVG is invalid or malformed"
|
||||
:type :error
|
||||
:timeout 3000}))))
|
||||
|
||||
;; TODO: should be handled in the event and not as general error handler
|
||||
(defmethod ptk/handle-error :comment-error
|
||||
[_]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(msg/show {:content "There was an error with the comment"
|
||||
:type :error
|
||||
:timeout 3000}))))
|
||||
#(st/emit! (msg/show {:content "There was an error with the comment"
|
||||
:type :error
|
||||
:timeout 3000}))))
|
||||
|
||||
;; This is a pure frontend error that can be caused by an active
|
||||
;; assertion (assertion that is preserved on production builds). From
|
||||
|
@ -111,15 +132,13 @@
|
|||
(dm/str cf/public-uri "js/cljs-runtime/" (:file error))
|
||||
(:line error))]
|
||||
(ts/schedule
|
||||
(st/emitf
|
||||
(msg/show {:content "Internal error: assertion."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
#(st/emit! (msg/show {:content "Internal error: assertion."
|
||||
:type :error
|
||||
:timeout 3000})))
|
||||
|
||||
;; Print to the console some debugging info
|
||||
(js/console.group message)
|
||||
(js/console.info context)
|
||||
(js/console.error (with-out-str (expound/printer error)))
|
||||
(js/console.groupEnd message)))
|
||||
|
||||
;; That are special case server-errors that should be treated
|
||||
|
@ -141,7 +160,7 @@
|
|||
(defmethod ptk/handle-error :server-error
|
||||
[{:keys [data hint] :as error}]
|
||||
(let [hint (or hint (:hint data) (:message data))
|
||||
info (with-out-str (fpp/pprint (dissoc data :explain)))
|
||||
info (pp/pprint-str (dissoc data :explain))
|
||||
msg (dm/str "Internal Server Error: " hint)]
|
||||
|
||||
(ts/schedule
|
||||
|
|
|
@ -102,8 +102,22 @@
|
|||
(l/derived :workspace-drawing st/state))
|
||||
|
||||
;; TODO: rename to workspace-selected (?)
|
||||
;; Don't use directly from components, this is a proxy to improve performance of selected-shapes
|
||||
(def ^:private selected-shapes-data
|
||||
(l/derived
|
||||
(fn [state]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
selected (dm/get-in state [:workspace-local :selected])]
|
||||
{:objects objects :selected selected}))
|
||||
st/state (fn [v1 v2]
|
||||
(and (identical? (:objects v1) (:objects v2))
|
||||
(= (:selected v1) (:selected v2))))))
|
||||
|
||||
(def selected-shapes
|
||||
(l/derived wsh/lookup-selected st/state =))
|
||||
(l/derived
|
||||
(fn [{:keys [objects selected]}]
|
||||
(wsh/process-selected-shapes objects selected))
|
||||
selected-shapes-data))
|
||||
|
||||
(defn make-selected-ref
|
||||
[id]
|
||||
|
@ -258,7 +272,7 @@
|
|||
|
||||
(defn objects-by-id
|
||||
[ids]
|
||||
(l/derived #(wsh/lookup-shapes % ids) st/state =))
|
||||
(l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects))
|
||||
|
||||
(defn- set-content-modifiers [state]
|
||||
(fn [id shape]
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
(:require
|
||||
["react-dom/server" :as rds]
|
||||
[app.common.colors :as clr]
|
||||
[app.common.geom.align :as gal]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
|
@ -22,10 +23,12 @@
|
|||
[app.common.pages.helpers :as cph]
|
||||
[app.config :as cfg]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.bool :as bool]
|
||||
[app.main.ui.shapes.circle :as circle]
|
||||
[app.main.ui.shapes.embed :as embed]
|
||||
[app.main.ui.shapes.export :as export]
|
||||
[app.main.ui.shapes.filters :as filters]
|
||||
[app.main.ui.shapes.frame :as frame]
|
||||
[app.main.ui.shapes.group :as group]
|
||||
[app.main.ui.shapes.image :as image]
|
||||
|
@ -57,11 +60,9 @@
|
|||
:fill color}])
|
||||
|
||||
(defn- calculate-dimensions
|
||||
[{:keys [objects] :as data} vport]
|
||||
(let [shapes (cph/get-immediate-children objects)
|
||||
rect (cond->> (gsh/selection-rect shapes)
|
||||
(some? vport)
|
||||
(gal/adjust-to-viewport vport))]
|
||||
[objects]
|
||||
(let [shapes (cph/get-immediate-children objects)
|
||||
rect (gsh/selection-rect shapes)]
|
||||
(-> rect
|
||||
(update :x mth/finite 0)
|
||||
(update :y mth/finite 0)
|
||||
|
@ -156,24 +157,63 @@
|
|||
(->> [x y width height]
|
||||
(map #(ust/format-precision % viewbox-decimal-precision)))))
|
||||
|
||||
(defn adapt-root-frame
|
||||
[objects object]
|
||||
(let [shapes (cph/get-immediate-children objects)
|
||||
srect (gsh/selection-rect shapes)
|
||||
object (merge object (select-keys srect [:x :y :width :height]))
|
||||
object (gsh/transform-shape object)]
|
||||
(assoc object :fill-color "#f0f0f0")))
|
||||
|
||||
(defn adapt-objects-for-shape
|
||||
[objects object-id]
|
||||
(let [object (get objects object-id)
|
||||
object (cond->> object
|
||||
(cph/root-frame? object)
|
||||
(adapt-root-frame objects))
|
||||
|
||||
;; Replace the previous object with the new one
|
||||
objects (assoc objects object-id object)
|
||||
|
||||
modifier (-> (gpt/point (:x object) (:y object))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
mod-ids (cons object-id (cph/get-children-ids objects object-id))
|
||||
updt-fn #(-> %1
|
||||
(assoc-in [%2 :modifiers :displacement] modifier)
|
||||
(update %2 gsh/transform-shape))]
|
||||
|
||||
(reduce updt-fn objects mod-ids)))
|
||||
|
||||
(defn get-object-bounds
|
||||
[objects object-id]
|
||||
(let [object (get objects object-id)
|
||||
padding (filters/calculate-padding object)
|
||||
bounds (-> (filters/get-filters-bounds object)
|
||||
(update :x - (:horizontal padding))
|
||||
(update :y - (:vertical padding))
|
||||
(update :width + (* 2 (:horizontal padding)))
|
||||
(update :height + (* 2 (:vertical padding))))]
|
||||
|
||||
(if (cph/group-shape? object)
|
||||
(if (:masked-group? object)
|
||||
(get-object-bounds objects (-> object :shapes first))
|
||||
(->> (:shapes object)
|
||||
(into [bounds] (map (partial get-object-bounds objects)))
|
||||
(gsh/join-rects)))
|
||||
bounds)))
|
||||
|
||||
(mf/defc page-svg
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [data width height thumbnails? embed? include-metadata?] :as props
|
||||
:or {embed? false include-metadata? false}}]
|
||||
[{:keys [data thumbnails? render-embed? include-metadata?] :as props
|
||||
:or {render-embed? false include-metadata? false}}]
|
||||
(let [objects (:objects data)
|
||||
shapes (cph/get-immediate-children objects)
|
||||
|
||||
root-children
|
||||
(->> shapes
|
||||
(remove cph/frame-shape?)
|
||||
(mapcat #(cph/get-children-with-self objects (:id %))))
|
||||
|
||||
vport (when (and (some? width) (some? height))
|
||||
{:width width :height height})
|
||||
|
||||
dim (calculate-dimensions data vport)
|
||||
dim (calculate-dimensions objects)
|
||||
vbox (format-viewbox dim)
|
||||
background-color (get-in data [:options :background] default-color)
|
||||
bgcolor (dm/get-in data [:options :background] default-color)
|
||||
|
||||
frame-wrapper
|
||||
(mf/use-memo
|
||||
|
@ -185,7 +225,7 @@
|
|||
(mf/deps objects)
|
||||
#(shape-wrapper-factory objects))]
|
||||
|
||||
[:& (mf/provider embed/context) {:value embed?}
|
||||
[:& (mf/provider embed/context) {:value render-embed?}
|
||||
[:& (mf/provider export/include-metadata-ctx) {:value include-metadata?}
|
||||
[:svg {:view-box vbox
|
||||
:version "1.1"
|
||||
|
@ -194,12 +234,17 @@
|
|||
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
|
||||
:style {:width "100%"
|
||||
:height "100%"
|
||||
:background background-color}}
|
||||
:background bgcolor}}
|
||||
|
||||
(when include-metadata?
|
||||
[:& export/export-page {:options (:options data)}])
|
||||
|
||||
[:& ff/fontfaces-style {:shapes root-children}]
|
||||
|
||||
(let [shapes (->> shapes
|
||||
(remove cph/frame-shape?)
|
||||
(mapcat #(cph/get-children-with-self objects (:id %))))]
|
||||
[:& ff/fontfaces-style {:shapes shapes}])
|
||||
|
||||
(for [item shapes]
|
||||
(let [frame? (= (:type item) :frame)]
|
||||
(cond
|
||||
|
@ -214,6 +259,10 @@
|
|||
[:& shape-wrapper {:shape item
|
||||
:key (:id item)}])))]]]))
|
||||
|
||||
|
||||
;; Component that serves for render frame thumbnails, mainly used in
|
||||
;; the viewer and handoff
|
||||
|
||||
(mf/defc frame-svg
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects frame zoom show-thumbnails?] :or {zoom 1} :as props}]
|
||||
|
@ -260,6 +309,10 @@
|
|||
[:> shape-container {:shape frame}
|
||||
[:& frame/frame-thumbnail {:shape frame}]]))]))
|
||||
|
||||
|
||||
;; Component for rendering a thumbnail of a single componenent. Mainly
|
||||
;; used to render thumbnails on assets panel.
|
||||
|
||||
(mf/defc component-svg
|
||||
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
|
||||
[{:keys [objects group zoom] :or {zoom 1} :as props}]
|
||||
|
@ -304,81 +357,122 @@
|
|||
[:> shape-container {:shape group}
|
||||
[:& group-wrapper {:shape group :view-box vbox}]]]))
|
||||
|
||||
(mf/defc object-svg
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects object zoom render-texts? render-embed?]
|
||||
:or {zoom 1 render-embed? false}
|
||||
:as props}]
|
||||
(let [object (cond-> object
|
||||
(:hide-fill-on-export object)
|
||||
(assoc :fills []))
|
||||
|
||||
obj-id (:id object)
|
||||
x (* (:x object) zoom)
|
||||
y (* (:y object) zoom)
|
||||
width (* (:width object) zoom)
|
||||
height (* (:height object) zoom)
|
||||
|
||||
vbox (dm/str x " " y " " width " " height)
|
||||
|
||||
frame-wrapper
|
||||
(mf/with-memo [objects]
|
||||
(frame-wrapper-factory objects))
|
||||
|
||||
group-wrapper
|
||||
(mf/with-memo [objects]
|
||||
(group-wrapper-factory objects))
|
||||
|
||||
shape-wrapper
|
||||
(mf/with-memo [objects]
|
||||
(shape-wrapper-factory objects))
|
||||
|
||||
text-shapes (sequence (filter cph/text-shape?) (vals objects))
|
||||
render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))]
|
||||
|
||||
[:& (mf/provider embed/context) {:value render-embed?}
|
||||
[:svg {:id (dm/str "screenshot-" obj-id)
|
||||
:view-box vbox
|
||||
:width width
|
||||
:height height
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
;; Fix Chromium bug about color of html texts
|
||||
;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5
|
||||
:style {:-webkit-print-color-adjust :exact}}
|
||||
|
||||
(let [shapes (cph/get-children objects obj-id)]
|
||||
[:& ff/fontfaces-style {:shapes shapes}])
|
||||
|
||||
(case (:type object)
|
||||
:frame [:& frame-wrapper {:shape object :view-box vbox}]
|
||||
:group [:> shape-container {:shape object}
|
||||
[:& group-wrapper {:shape object}]]
|
||||
[:& shape-wrapper {:shape object}])]
|
||||
|
||||
;; Auxiliary SVG for rendering text-shapes
|
||||
(when render-texts?
|
||||
(for [object text-shapes]
|
||||
[:& (mf/provider muc/text-plain-colors-ctx) {:value true}
|
||||
[:svg
|
||||
{:id (dm/str "screenshot-text-" (:id object))
|
||||
:view-box (dm/str "0 0 " (:width object) " " (:height object))
|
||||
:width (:width object)
|
||||
:height (:height object)
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"}
|
||||
[:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SPRITES (DEBUG)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(mf/defc component-symbol
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [id (obj/get props "id")
|
||||
data (obj/get props "data")
|
||||
name (:name data)
|
||||
path (:path data)
|
||||
objects (:objects data)
|
||||
root (get objects id)
|
||||
selrect (:selrect root)
|
||||
[{:keys [id data] :as props}]
|
||||
(let [name (:name data)
|
||||
objects (-> (:objects data)
|
||||
(adapt-objects-for-shape id))
|
||||
object (get objects id)
|
||||
selrect (:selrect object)
|
||||
|
||||
vbox
|
||||
(format-viewbox
|
||||
{:width (:width selrect)
|
||||
:height (:height selrect)})
|
||||
|
||||
modifier
|
||||
(mf/use-memo
|
||||
(mf/deps (:x root) (:y root))
|
||||
(fn []
|
||||
(-> (gpt/point (:x root) (:y root))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))))
|
||||
|
||||
objects
|
||||
(mf/use-memo
|
||||
(mf/deps modifier id objects)
|
||||
(fn []
|
||||
(let [modifier-ids (cons id (cph/get-children-ids objects id))
|
||||
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)]
|
||||
(reduce update-fn objects modifier-ids))))
|
||||
|
||||
root
|
||||
(mf/use-memo
|
||||
(mf/deps modifier root)
|
||||
(fn [] (assoc-in root [:modifiers :displacement] modifier)))
|
||||
|
||||
group-wrapper
|
||||
(mf/use-memo
|
||||
(mf/deps objects)
|
||||
(fn [] (group-wrapper-factory objects)))]
|
||||
|
||||
[:> "symbol" #js {:id (str id)
|
||||
:viewBox vbox
|
||||
"penpot:path" path}
|
||||
[:> "symbol" #js {:id (str id) :viewBox vbox}
|
||||
[:title name]
|
||||
[:> shape-container {:shape root}
|
||||
[:& group-wrapper {:shape root :view-box vbox}]]]))
|
||||
[:> shape-container {:shape object}
|
||||
[:& group-wrapper {:shape object :view-box vbox}]]]))
|
||||
|
||||
(mf/defc components-sprite-svg
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [data (obj/get props "data")
|
||||
children (obj/get props "children")
|
||||
embed? (obj/get props "embed?")
|
||||
render-embed? (obj/get props "render-embed?")
|
||||
include-metadata? (obj/get props "include-metadata?")]
|
||||
[:& (mf/provider embed/context) {:value embed?}
|
||||
[:& (mf/provider embed/context) {:value render-embed?}
|
||||
[:& (mf/provider export/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 (when include-metadata? "https://penpot.app/xmlns")
|
||||
:style {:width "100vw"
|
||||
:height "100vh"
|
||||
:display (when-not (some? children) "none")}}
|
||||
:style {: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}])]
|
||||
(for [[id data] (:components data)]
|
||||
[:& component-symbol {:id id :key (dm/str id) :data data}])]
|
||||
|
||||
children]]]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; RENDERING
|
||||
;; RENDER FOR DOWNLOAD (wrongly called exportation)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- get-image-data [shape]
|
||||
|
@ -426,7 +520,7 @@
|
|||
(->> (rx/of data)
|
||||
(rx/map
|
||||
(fn [data]
|
||||
(let [elem (mf/element page-svg #js {:data data :embed? true :include-metadata? true})]
|
||||
(let [elem (mf/element page-svg #js {:data data :render-embed? true :include-metadata? true})]
|
||||
(rds/renderToStaticMarkup elem)))))))
|
||||
|
||||
(defn render-components
|
||||
|
@ -445,5 +539,6 @@
|
|||
(->> (rx/of data)
|
||||
(rx/map
|
||||
(fn [data]
|
||||
(let [elem (mf/element components-sprite-svg #js {:data data :embed? true :include-metadata? true})]
|
||||
(let [elem (mf/element components-sprite-svg
|
||||
#js {:data data :render-embed? true :include-metadata? true})]
|
||||
(rds/renderToStaticMarkup elem))))))))
|
||||
|
|
|
@ -31,6 +31,10 @@
|
|||
(= 200 status)
|
||||
(rx/of body)
|
||||
|
||||
(= 413 status)
|
||||
(rx/throw {:type :validation
|
||||
:code :request-body-too-large})
|
||||
|
||||
(and (>= status 400)
|
||||
(map? body))
|
||||
(rx/throw body)
|
||||
|
@ -105,34 +109,22 @@
|
|||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)))
|
||||
|
||||
(defn- send-export-command
|
||||
[& {:keys [cmd params blob?]}]
|
||||
(defn- send-export
|
||||
[{:keys [blob?] :as params}]
|
||||
(->> (http/send! {:method :post
|
||||
:uri (u/join base-uri "api/export")
|
||||
:body (http/transit-data (assoc params :cmd cmd))
|
||||
:body (http/transit-data (dissoc params :blob?))
|
||||
:credentials "include"
|
||||
:response-type (if blob? :blob :text)})
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)))
|
||||
|
||||
(defmethod query :export-shapes-simple
|
||||
(defmethod query :exporter
|
||||
[_ params]
|
||||
(let [params (merge {:wait true} params)]
|
||||
(->> (rx/of params)
|
||||
(rx/mapcat #(send-export-command :cmd :export-shapes :params % :blob? false))
|
||||
(rx/mapcat #(send-export-command :cmd :get-resource :params % :blob? true)))))
|
||||
|
||||
(defmethod query :export-shapes-multiple
|
||||
[_ params]
|
||||
(send-export-command :cmd :export-shapes :params params :blob? false))
|
||||
|
||||
(defmethod query :export-frames-multiple
|
||||
[_ params]
|
||||
(send-export-command :cmd :export-frames :params (assoc params :uri (str base-uri)) :blob? false))
|
||||
|
||||
(defmethod query :download-export-resource
|
||||
[_ id]
|
||||
(send-export-command :cmd :get-resource :params {:id id} :blob? true))
|
||||
(let [default {:wait false
|
||||
:blob? false
|
||||
:uri (str base-uri)}]
|
||||
(send-export (merge default params))))
|
||||
|
||||
(derive :upload-file-media-object ::multipart-upload)
|
||||
(derive :update-profile-photo ::multipart-upload)
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
[app.main.ui.onboarding]
|
||||
[app.main.ui.onboarding.questions]
|
||||
[app.main.ui.releases]
|
||||
[app.main.ui.render :as render]
|
||||
[app.main.ui.settings :as settings]
|
||||
[app.main.ui.static :as static]
|
||||
[app.main.ui.viewer :as viewer]
|
||||
|
@ -110,15 +109,6 @@
|
|||
:index index
|
||||
:share-id share-id}]))
|
||||
|
||||
;; TODO: maybe move to `app.render` entrypoint (handled by render.html)
|
||||
:render-sprite
|
||||
(do
|
||||
(let [file-id (uuid (get-in route [:path-params :file-id]))
|
||||
component-id (get-in route [:query-params :component-id])
|
||||
component-id (when (some? component-id) (uuid component-id))]
|
||||
[:& render/render-sprite {:file-id file-id
|
||||
:component-id component-id}]))
|
||||
|
||||
:workspace
|
||||
(let [project-id (some-> params :path :project-id uuid)
|
||||
file-id (some-> params :path :file-id uuid)
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
on-keyup
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(cond
|
||||
(kbd/esc? event)
|
||||
(on-cancel)
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc export-multiple-dialog
|
||||
[{:keys [exports filename title query-name no-selection]}]
|
||||
[{:keys [exports title cmd no-selection]}]
|
||||
(let [lstate (mf/deref refs/export)
|
||||
in-progress? (:in-progress lstate)
|
||||
|
||||
|
@ -33,7 +33,10 @@
|
|||
all-checked? (every? :enabled all-exports)
|
||||
all-unchecked? (every? (complement :enabled) all-exports)
|
||||
|
||||
enabled-exports (into [] (filter :enabled) all-exports)
|
||||
enabled-exports (into []
|
||||
(comp (filter :enabled)
|
||||
(map #(dissoc % :shape :enabled)))
|
||||
all-exports)
|
||||
|
||||
cancel-fn
|
||||
(fn [event]
|
||||
|
@ -45,9 +48,8 @@
|
|||
(dom/prevent-default event)
|
||||
(st/emit! (modal/hide)
|
||||
(de/request-multiple-export
|
||||
{:filename filename
|
||||
:exports enabled-exports
|
||||
:query-name query-name})))
|
||||
{:exports enabled-exports
|
||||
:cmd cmd})))
|
||||
|
||||
on-toggle-enabled
|
||||
(fn [index]
|
||||
|
@ -145,25 +147,23 @@
|
|||
(mf/defc export-shapes-dialog
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :export-shapes}
|
||||
[{:keys [exports filename]}]
|
||||
[{:keys [exports]}]
|
||||
(let [title (tr "dashboard.export-shapes.title")]
|
||||
[:& export-multiple-dialog
|
||||
{:exports exports
|
||||
:filename filename
|
||||
:title title
|
||||
:query-name :export-shapes-multiple
|
||||
:cmd :export-shapes
|
||||
:no-selection shapes-no-selection}]))
|
||||
|
||||
(mf/defc export-frames
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :export-frames}
|
||||
[{:keys [exports filename]}]
|
||||
[{:keys [exports]}]
|
||||
(let [title (tr "dashboard.export-frames.title")]
|
||||
[:& export-multiple-dialog
|
||||
{:exports exports
|
||||
:filename filename
|
||||
:title title
|
||||
:query-name :export-frames-multiple}]))
|
||||
:cmd :export-frames}]))
|
||||
|
||||
(mf/defc export-progress-widget
|
||||
{::mf/wrap [mf/memo]}
|
||||
|
|
|
@ -1,203 +0,0 @@
|
|||
;; 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.ui.render
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[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.helpers :as cph]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.render :as render]
|
||||
[app.main.repo :as repo]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.embed :as embed]
|
||||
[app.main.ui.shapes.filters :as filters]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||
[app.util.dom :as dom]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn calc-bounds
|
||||
[object objects]
|
||||
(let [xf-get-bounds (comp (map #(get objects %)) (map #(calc-bounds % objects)))
|
||||
padding (filters/calculate-padding object)
|
||||
obj-bounds (-> (filters/get-filters-bounds object)
|
||||
(update :x - (:horizontal padding))
|
||||
(update :y - (:vertical padding))
|
||||
(update :width + (* 2 (:horizontal padding)))
|
||||
(update :height + (* 2 (:vertical padding))))]
|
||||
|
||||
(cond
|
||||
(and (= :group (:type object))
|
||||
(:masked-group? object))
|
||||
(calc-bounds (get objects (first (:shapes object))) objects)
|
||||
|
||||
(= :group (:type object))
|
||||
(->> (:shapes object)
|
||||
(into [obj-bounds] xf-get-bounds)
|
||||
(gsh/join-rects))
|
||||
|
||||
:else
|
||||
obj-bounds)))
|
||||
|
||||
(mf/defc object-svg
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects object-id zoom render-texts? embed?]
|
||||
:or {zoom 1 embed? false}
|
||||
:as props}]
|
||||
(let [object (get objects object-id)
|
||||
frame-id (if (= :frame (:type object))
|
||||
(:id object)
|
||||
(:frame-id object))
|
||||
|
||||
modifier (-> (gpt/point (:x object) (:y object))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
mod-ids (cons frame-id (cph/get-children-ids objects frame-id))
|
||||
updt-fn #(-> %1
|
||||
(assoc-in [%2 :modifiers :displacement] modifier)
|
||||
(update %2 gsh/transform-shape))
|
||||
|
||||
objects (reduce updt-fn objects mod-ids)
|
||||
object (get objects object-id)
|
||||
|
||||
object (cond-> object
|
||||
(:hide-fill-on-export object)
|
||||
(assoc :fills []))
|
||||
|
||||
all-children (cph/get-children objects object-id)
|
||||
|
||||
{:keys [x y width height] :as bs} (calc-bounds object objects)
|
||||
[_ _ width height :as coords] (->> [x y width height] (map #(* % zoom)))
|
||||
|
||||
vbox (str/join " " coords)
|
||||
|
||||
frame-wrapper
|
||||
(mf/with-memo [objects]
|
||||
(render/frame-wrapper-factory objects))
|
||||
|
||||
group-wrapper
|
||||
(mf/with-memo [objects]
|
||||
(render/group-wrapper-factory objects))
|
||||
|
||||
shape-wrapper
|
||||
(mf/with-memo [objects]
|
||||
(render/shape-wrapper-factory objects))
|
||||
|
||||
is-text? (fn [shape] (= :text (:type shape)))
|
||||
|
||||
text-shapes (sequence (comp (map second) (filter is-text?)) objects)
|
||||
|
||||
render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))]
|
||||
|
||||
(mf/with-effect [width height]
|
||||
(dom/set-page-style!
|
||||
{:size (dm/str (mth/ceil width) "px "
|
||||
(mth/ceil height) "px")}))
|
||||
|
||||
[:& (mf/provider embed/context) {:value embed?}
|
||||
[:svg {:id "screenshot"
|
||||
:view-box vbox
|
||||
:width width
|
||||
:height height
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
;; Fix Chromium bug about color of html texts
|
||||
;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5
|
||||
:style {:-webkit-print-color-adjust :exact}}
|
||||
|
||||
[:& ff/fontfaces-style {:shapes all-children}]
|
||||
|
||||
(case (:type object)
|
||||
:frame [:& frame-wrapper {:shape object :view-box vbox}]
|
||||
:group [:> shape-container {:shape object}
|
||||
[:& group-wrapper {:shape object}]]
|
||||
[:& shape-wrapper {:shape object}])]
|
||||
|
||||
;; Auxiliary SVG for rendering text-shapes
|
||||
(when render-texts?
|
||||
(for [object text-shapes]
|
||||
[:& (mf/provider muc/text-plain-colors-ctx) {:value true}
|
||||
[:svg {:id (str "screenshot-text-" (:id object))
|
||||
:view-box (str "0 0 " (:width object) " " (:height object))
|
||||
:width (:width object)
|
||||
:height (:height object)
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"}
|
||||
[:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))]))
|
||||
|
||||
(defn- adapt-root-frame
|
||||
[objects object-id]
|
||||
(if (uuid/zero? object-id)
|
||||
(let [object (get objects object-id)
|
||||
shapes (cph/get-immediate-children objects)
|
||||
srect (gsh/selection-rect shapes)
|
||||
object (merge object (select-keys srect [:x :y :width :height]))
|
||||
object (gsh/transform-shape object)
|
||||
object (assoc object :fill-color "#f0f0f0")]
|
||||
(assoc objects (:id object) object))
|
||||
objects))
|
||||
|
||||
(mf/defc render-object
|
||||
[{:keys [file-id page-id object-id render-texts? embed?] :as props}]
|
||||
(let [objects (mf/use-state nil)]
|
||||
|
||||
(mf/with-effect [file-id page-id object-id]
|
||||
(->> (rx/zip
|
||||
(repo/query! :font-variants {:file-id file-id})
|
||||
(repo/query! :trimmed-file {:id file-id :page-id page-id :object-id object-id}))
|
||||
(rx/subs
|
||||
(fn [[fonts {:keys [data]}]]
|
||||
(when (seq fonts)
|
||||
(st/emit! (df/fonts-fetched fonts)))
|
||||
(let [objs (get-in data [:pages-index page-id :objects])
|
||||
objs (adapt-root-frame objs object-id)]
|
||||
(reset! objects objs)))))
|
||||
(constantly nil))
|
||||
|
||||
(when @objects
|
||||
[:& object-svg {:objects @objects
|
||||
:object-id object-id
|
||||
:embed? embed?
|
||||
:render-texts? render-texts?
|
||||
:zoom 1}])))
|
||||
|
||||
(mf/defc render-sprite
|
||||
[{:keys [file-id component-id] :as props}]
|
||||
(let [file (mf/use-state nil)]
|
||||
|
||||
(mf/with-effect [file-id]
|
||||
(->> (repo/query! :file {:id file-id})
|
||||
(rx/subs
|
||||
(fn [result]
|
||||
(reset! file result))))
|
||||
(constantly nil))
|
||||
|
||||
(when @file
|
||||
[:*
|
||||
[:& render/components-sprite-svg {:data (:data @file) :embed true}
|
||||
|
||||
(when (some? component-id)
|
||||
[:use {:x 0 :y 0
|
||||
:xlinkHref (str "#" component-id)}])]
|
||||
|
||||
(when-not (some? component-id)
|
||||
[:ul
|
||||
(for [[id data] (get-in @file [:data :components])]
|
||||
(let [url (str "#/render-sprite/" (:id @file) "?component-id=" id)]
|
||||
[:li [:a {:href url} (:name data)]]))])])))
|
||||
|
|
@ -61,7 +61,6 @@
|
|||
["/debug/icons-preview" :debug-icons-preview])
|
||||
|
||||
;; Used for export
|
||||
["/render-object/:file-id/:page-id/:object-id" :render-object]
|
||||
["/render-sprite/:file-id" :render-sprite]
|
||||
|
||||
["/dashboard/team/:team-id"
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.config :as cfg]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.attrs :as attrs]
|
||||
[app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]]
|
||||
|
@ -50,9 +51,16 @@
|
|||
|
||||
[:> :g group-props
|
||||
(for [[index data] (d/enumerate position-data)]
|
||||
(let [props (-> #js {:x (mth/round (:x data))
|
||||
:y (mth/round (- (:y data) (:height data)))
|
||||
:alignmentBaseline "text-before-edge"
|
||||
(let [y (if (cfg/check-browser? :safari)
|
||||
(mth/round (- (:y data) (:height data)))
|
||||
(mth/round (:y data)))
|
||||
|
||||
alignment-bl (when (cfg/check-browser? :safari) "text-before-edge")
|
||||
dominant-bl (when-not (cfg/check-browser? :safari) "ideographic")
|
||||
props (-> #js {:x (mth/round (:x data))
|
||||
:y y
|
||||
:alignmentBaseline alignment-bl
|
||||
:dominantBaseline dominant-bl
|
||||
:style (-> #js {:fontFamily (:font-family data)
|
||||
:fontSize (:font-size data)
|
||||
:fontWeight (:font-weight data)
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
[app.util.code-gen :as cg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn has-image? [shape]
|
||||
|
@ -34,12 +35,10 @@
|
|||
[:div.attributes-value (-> shape :metadata :height) "px"]
|
||||
[:& copy-button {:data (cg/generate-css-props shape :height)}]]
|
||||
|
||||
(let [mtype (-> shape :metadata :mtype)
|
||||
name (:name shape)
|
||||
(let [mtype (-> shape :metadata :mtype)
|
||||
name (:name shape)
|
||||
extension (dom/mtype->extension mtype)]
|
||||
[:a.download-button {:target "_blank"
|
||||
:download (if extension
|
||||
(str name "." extension)
|
||||
name)
|
||||
:download (cond-> name extension (str/concat extension))
|
||||
:href (cfg/resolve-file-media (-> shape :metadata))}
|
||||
(tr "handoff.attributes.image.download")])])))
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
(ns app.main.ui.viewer.shapes
|
||||
"The main container for a frame in viewer mode"
|
||||
(:require
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.spec.interactions :as cti]
|
||||
|
@ -390,34 +388,3 @@
|
|||
:bool [:> bool-container {:shape shape :frame frame :objects objects}]
|
||||
:svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}])))))))
|
||||
|
||||
(mf/defc frame-svg
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects frame zoom] :or {zoom 1} :as props}]
|
||||
(let [modifier (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
|
||||
|
||||
frame-id (:id frame)
|
||||
modifier-ids (into [frame-id] (cph/get-children-ids objects frame-id))
|
||||
objects (reduce update-fn objects modifier-ids)
|
||||
frame (assoc-in frame [:modifiers :displacement] modifier)
|
||||
width (* (:width frame) zoom)
|
||||
height (* (:height frame) zoom)
|
||||
|
||||
vbox (str "0 0 " (:width frame 0)
|
||||
" " (:height frame 0))
|
||||
wrapper (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(frame-container-factory objects))]
|
||||
|
||||
[:svg {:view-box vbox
|
||||
:width width
|
||||
:height height
|
||||
:version "1.1"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns "http://www.w3.org/2000/svg"}
|
||||
[:& wrapper {:shape frame
|
||||
:view-box vbox}]]))
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"A workspace specific context menu (mouse right click)."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.spec.page :as csp]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
|
@ -167,13 +168,12 @@
|
|||
|
||||
(mf/defc context-menu-thumbnail
|
||||
[{:keys [shapes]}]
|
||||
(let [single? (= (count shapes) 1)
|
||||
has-frame? (->> shapes (d/seek #(= :frame (:type %))))
|
||||
is-frame? (and single? has-frame?)
|
||||
(let [single? (= (count shapes) 1)
|
||||
has-frame? (some cph/frame-shape? shapes)
|
||||
do-toggle-thumbnail (st/emitf (dw/toggle-file-thumbnail-selected))]
|
||||
(when is-frame?
|
||||
(when (and single? has-frame?)
|
||||
[:*
|
||||
(if (every? :file-thumbnail shapes)
|
||||
(if (every? :use-for-thumbnail? shapes)
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.thumbnail-remove")
|
||||
:on-click do-toggle-thumbnail}]
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.thumbnail-set")
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.data :refer [matches-search]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.strings :refer [matches-search]]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
[app.main.ui.shapes.rect :as rect]
|
||||
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||
[app.main.ui.workspace.shapes.bool :as bool]
|
||||
[app.main.ui.workspace.shapes.bounding-box :refer [bounding-box]]
|
||||
[app.main.ui.workspace.shapes.common :as common]
|
||||
[app.main.ui.workspace.shapes.frame :as frame]
|
||||
[app.main.ui.workspace.shapes.group :as group]
|
||||
|
@ -26,7 +25,6 @@
|
|||
[app.main.ui.workspace.shapes.svg-raw :as svg-raw]
|
||||
[app.main.ui.workspace.shapes.text :as text]
|
||||
[app.util.object :as obj]
|
||||
[debug :refer [debug?]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(declare shape-wrapper)
|
||||
|
@ -87,10 +85,7 @@
|
|||
;; Only used when drawing a new frame.
|
||||
:frame [:> frame-wrapper opts]
|
||||
|
||||
nil)
|
||||
|
||||
(when (debug? :bounding-boxes)
|
||||
[:> bounding-box opts])])))
|
||||
nil)])))
|
||||
|
||||
(def group-wrapper (group/group-wrapper-factory shape-wrapper))
|
||||
(def svg-raw-wrapper (svg-raw/svg-raw-wrapper-factory shape-wrapper))
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
;; 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.ui.workspace.shapes.bounding-box
|
||||
(:require
|
||||
["randomcolor" :as rdcolor]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.refs :as refs]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn fixed
|
||||
[num]
|
||||
(when num (.toFixed num 2)))
|
||||
|
||||
(mf/defc cross-point [{:keys [point zoom color]}]
|
||||
(let [width (/ 5 zoom)]
|
||||
[:g.point
|
||||
[:line {:x1 (- (:x point) width) :y1 (- (:y point) width)
|
||||
:x2 (+ (:x point) width) :y2 (+ (:y point) width)
|
||||
:stroke color
|
||||
:stroke-width "1px"
|
||||
:stroke-opacity 0.5}]
|
||||
|
||||
[:line {:x1 (+ (:x point) width) :y1 (- (:y point) width)
|
||||
:x2 (- (:x point) width) :y2 (+ (:y point) width)
|
||||
:stroke color
|
||||
:stroke-width "1px"
|
||||
:stroke-opacity 0.5}]]))
|
||||
|
||||
(mf/defc render-rect [{{:keys [x y width height]} :rect :keys [color transform]}]
|
||||
[:rect {:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height
|
||||
:transform (or transform "none")
|
||||
:style {:stroke color
|
||||
:fill "none"
|
||||
:stroke-width "1px"
|
||||
:pointer-events "none"}}])
|
||||
|
||||
(mf/defc render-rect-points [{:keys [points color]}]
|
||||
(for [[p1 p2] (map vector points (concat (rest points) [(first points)]))]
|
||||
[:line {:x1 (:x p1)
|
||||
:y1 (:y p1)
|
||||
:x2 (:x p2)
|
||||
:y2 (:y p2)
|
||||
:style {:stroke color
|
||||
:stroke-width "1px"}}]))
|
||||
|
||||
(mf/defc bounding-box
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
bounding-box (gsh/points->selrect (-> shape :points))
|
||||
shape-center (gsh/center-shape shape)
|
||||
line-color (rdcolor #js {:seed (str (:id shape))})
|
||||
zoom (mf/deref refs/selected-zoom)]
|
||||
|
||||
[:g.bounding-box
|
||||
[:text {:x (:x bounding-box)
|
||||
:y (- (:y bounding-box) 5)
|
||||
:font-size 10
|
||||
:fill line-color
|
||||
:stroke "var(--color-white)"
|
||||
:stroke-width 0.1}
|
||||
(str/format "%s - (%s, %s)" (str/slice (str (:id shape)) 0 8) (fixed (:x bounding-box)) (fixed (:y bounding-box)))]
|
||||
|
||||
[:g.center
|
||||
[:& cross-point {:point shape-center
|
||||
:zoom zoom
|
||||
:color line-color}]]
|
||||
|
||||
[:g.points
|
||||
(for [point (:points shape)]
|
||||
[:& cross-point {:point point
|
||||
:zoom zoom
|
||||
:color line-color}])
|
||||
#_[:& render-rect-points {:points (:points shape)
|
||||
:color line-color}]]
|
||||
|
||||
[:g.selrect
|
||||
[:& render-rect {:rect (:selrect shape)
|
||||
;; :transform (gsh/transform-matrix shape)
|
||||
:color line-color}]
|
||||
#_[:& render-rect {:rect bounding-box
|
||||
:color line-color}]]]))
|
|
@ -32,12 +32,12 @@
|
|||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]]
|
||||
[app.util.data :refer [matches-search]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.dom.dnd :as dnd]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.router :as rt]
|
||||
[app.util.strings :refer [matches-search]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
|
|
|
@ -189,7 +189,7 @@
|
|||
(when (and single? selected?)
|
||||
(ts/schedule
|
||||
100
|
||||
#(dom/scroll-into-view! node #js {:block "nearest", :behavior "smooth"})))]
|
||||
#(dom/scroll-into-view! node #js {:block "center", :behavior "smooth"})))]
|
||||
|
||||
#(when (some? subid)
|
||||
(rx/dispose! subid)))))
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
state (mf/deref refs/export)
|
||||
in-progress? (:in-progress state)
|
||||
|
||||
filename (when (seqable? exports)
|
||||
sname (when (seqable? exports)
|
||||
(let [shapes (wsh/lookup-shapes @st/state ids)
|
||||
sname (-> shapes first :name)
|
||||
suffix (-> exports first :suffix)]
|
||||
|
@ -56,13 +56,13 @@
|
|||
;; separatelly by the export-modal.
|
||||
(let [defaults {:page-id page-id
|
||||
:file-id file-id
|
||||
:name filename
|
||||
:name sname
|
||||
:object-id (first ids)}
|
||||
exports (mapv #(merge % defaults) exports)]
|
||||
(if (= 1 (count exports))
|
||||
(let [export (first exports)]
|
||||
(st/emit! (de/request-simple-export {:export export :filename (:name export)})))
|
||||
(st/emit! (de/request-multiple-export {:exports exports :filename filename})))))))
|
||||
(st/emit! (de/request-simple-export {:export export})))
|
||||
(st/emit! (de/request-multiple-export {:exports exports})))))))
|
||||
|
||||
;; TODO: maybe move to specific events for avoid to have this logic here?
|
||||
add-export
|
||||
|
|
|
@ -155,7 +155,7 @@
|
|||
:on-blur on-blur}])])
|
||||
|
||||
(when (or (= type :frame)
|
||||
(and (= type :multiple) (some? hide-fill-on-export?)))
|
||||
(and (= type :multiple) (some? (:hide-fill-on-export values))))
|
||||
[:div.input-checkbox
|
||||
[:input {:type "checkbox"
|
||||
:id "show-fill-on-export"
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.ui.workspace.sidebar.options.shapes.svg-raw
|
||||
(:require
|
||||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]]
|
||||
|
@ -15,7 +16,6 @@
|
|||
[app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.svg-attrs :refer [svg-attrs-menu]]
|
||||
[app.util.color :as uc]
|
||||
[app.util.data :as d]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
@ -75,7 +75,7 @@
|
|||
stroke-width (-> (or (get-in shape [:content :attrs :stroke-width])
|
||||
(get-in shape [:content :attrs :style :stroke-width])
|
||||
"1")
|
||||
(d/parse-int))
|
||||
(d/parse-integer))
|
||||
|
||||
stroke-values (if (empty? stroke-values)
|
||||
{:stroke-color stroke-color
|
||||
|
|
|
@ -212,6 +212,7 @@
|
|||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns:penpot "https://penpot.app/xmlns"
|
||||
:preserveAspectRatio "xMidYMid meet"
|
||||
:shape-rendering "crispEdges"
|
||||
:key (str "render" page-id)
|
||||
:width (:width vport 0)
|
||||
:height (:height vport 0)
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
:pattern-units "userSpaceOnUse"}
|
||||
[:path {:d "M 1 0 L 0 0 0 1"
|
||||
:style {:fill "none"
|
||||
:stroke "var(--color-gray-20)"
|
||||
:stroke-opacity "1"
|
||||
:stroke "var(--color-info)"
|
||||
:stroke-opacity "0.2"
|
||||
:stroke-width (str (/ 1 zoom))}}]]]
|
||||
[:rect {:x (:x vbox)
|
||||
:y (:y vbox)
|
||||
|
|
|
@ -7,27 +7,38 @@
|
|||
(ns app.render
|
||||
"The main entry point for UI part needed by the exporter."
|
||||
(:require
|
||||
[app.common.logging :as log]
|
||||
[app.common.logging :as l]
|
||||
[app.common.math :as mth]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.main.ui.render :as render]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.render :as render]
|
||||
[app.main.repo :as repo]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as glob]
|
||||
[beicon.core :as rx]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[garden.core :refer [css]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(log/initialize!)
|
||||
(log/set-level! :root :warn)
|
||||
(log/set-level! :app :info)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SETUP
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare reinit)
|
||||
(l/initialize!)
|
||||
(l/set-level! :root :warn)
|
||||
(l/set-level! :app :info)
|
||||
|
||||
(declare ^:private render-object)
|
||||
(declare ^:private render-single-object)
|
||||
(declare ^:private render-components)
|
||||
(declare ^:private render-objects)
|
||||
|
||||
(log/info :hint "Welcome to penpot (Export)"
|
||||
:version (:full @cf/version)
|
||||
:public-uri (str cf/public-uri))
|
||||
(l/info :hint "Welcome to penpot (Export)"
|
||||
:version (:full @cf/version)
|
||||
:public-uri (str cf/public-uri))
|
||||
|
||||
(defn- parse-params
|
||||
[loc]
|
||||
|
@ -38,7 +49,8 @@
|
|||
[]
|
||||
(when-let [params (parse-params glob/location)]
|
||||
(when-let [component (case (:route params)
|
||||
"render-object" (render-object params)
|
||||
"objects" (render-objects params)
|
||||
"components" (render-components params)
|
||||
nil)]
|
||||
(mf/mount component (dom/get-element "app")))))
|
||||
|
||||
|
@ -55,23 +67,223 @@
|
|||
[]
|
||||
(reinit))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; COMPONENTS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; ---- SINGLE OBJECT
|
||||
|
||||
(defn use-resource
|
||||
"A general purpose hook for retrieve or subscribe to remote changes
|
||||
using the reactive-streams mechanism mechanism.
|
||||
|
||||
It receives a function to execute for retrieve the stream that will
|
||||
be used for creating the subscription. The function should be
|
||||
stable, so is the responsability of the user of this hook to
|
||||
properly memoize it.
|
||||
|
||||
TODO: this should be placed in some generic hooks namespace but his
|
||||
right now is pending of refactor and it will be done later."
|
||||
[f]
|
||||
(let [[state ^js update-state!] (mf/useState {:loaded? false})]
|
||||
(mf/with-effect [f]
|
||||
(update-state! (fn [prev] (assoc prev :refreshing? true)))
|
||||
(let [on-value (fn [data]
|
||||
(update-state! #(-> %
|
||||
(assoc :refreshing? false)
|
||||
(assoc :loaded? true)
|
||||
(merge data))))
|
||||
subs (rx/subscribe (f) on-value)]
|
||||
#(rx/dispose! subs)))
|
||||
state))
|
||||
|
||||
(mf/defc object-svg
|
||||
[{:keys [page-id file-id object-id render-embed? render-texts?]}]
|
||||
(let [fetch-state (mf/use-fn
|
||||
(mf/deps file-id page-id object-id)
|
||||
(fn []
|
||||
(->> (rx/zip
|
||||
(repo/query! :font-variants {:file-id file-id})
|
||||
(repo/query! :page {:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id object-id}))
|
||||
(rx/tap (fn [[fonts]]
|
||||
(when (seq fonts)
|
||||
(st/emit! (df/fonts-fetched fonts)))))
|
||||
(rx/map (comp :objects second))
|
||||
(rx/map (fn [objects]
|
||||
(let [objects (render/adapt-objects-for-shape objects object-id)
|
||||
bounds (render/get-object-bounds objects object-id)
|
||||
object (get objects object-id)]
|
||||
{:objects objects
|
||||
:object (merge object bounds)}))))))
|
||||
|
||||
{:keys [objects object]} (use-resource fetch-state)]
|
||||
|
||||
;; Set the globa CSS to assign the page size, needed for PDF
|
||||
;; exportation process.
|
||||
(mf/with-effect [object]
|
||||
(when object
|
||||
(dom/set-page-style!
|
||||
{:size (str/concat
|
||||
(mth/ceil (:width object)) "px "
|
||||
(mth/ceil (:height object)) "px")})))
|
||||
|
||||
(when objects
|
||||
[:& render/object-svg
|
||||
{:objects objects
|
||||
:object object
|
||||
:render-embed? render-embed?
|
||||
:render-texts? render-texts?
|
||||
:zoom 1}])))
|
||||
|
||||
(mf/defc objects-svg
|
||||
[{:keys [page-id file-id object-ids render-embed? render-texts?]}]
|
||||
(let [fetch-state (mf/use-fn
|
||||
(mf/deps file-id page-id)
|
||||
(fn []
|
||||
(->> (rx/zip
|
||||
(repo/query! :font-variants {:file-id file-id})
|
||||
(repo/query! :page {:file-id file-id
|
||||
:page-id page-id}))
|
||||
(rx/tap (fn [[fonts]]
|
||||
(when (seq fonts)
|
||||
(st/emit! (df/fonts-fetched fonts)))))
|
||||
(rx/map (comp :objects second)))))
|
||||
|
||||
objects (use-resource fetch-state)]
|
||||
|
||||
(when objects
|
||||
(for [object-id object-ids]
|
||||
(let [objects (render/adapt-objects-for-shape objects object-id)
|
||||
bounds (render/get-object-bounds objects object-id)
|
||||
object (merge (get objects object-id) bounds)]
|
||||
[:& render/object-svg
|
||||
{:objects objects
|
||||
:key (str object-id)
|
||||
:object object
|
||||
:render-embed? render-embed?
|
||||
:render-texts? render-texts?
|
||||
:zoom 1}])))))
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::object-id ::us/uuid)
|
||||
(s/def ::object-id
|
||||
(s/or :single ::us/uuid
|
||||
:multiple (s/coll-of ::us/uuid)))
|
||||
(s/def ::render-text ::us/boolean)
|
||||
(s/def ::embed ::us/boolean)
|
||||
|
||||
(s/def ::render-object-params
|
||||
(s/def ::render-objects
|
||||
(s/keys :req-un [::file-id ::page-id ::object-id]
|
||||
:opt-un [::render-text ::embed]))
|
||||
:opt-un [::render-text ::render-embed]))
|
||||
|
||||
(defn- render-object
|
||||
(defn- render-objects
|
||||
[params]
|
||||
(let [{:keys [page-id file-id object-id render-texts embed]} (us/conform ::render-object-params params)]
|
||||
(let [{:keys [file-id
|
||||
page-id
|
||||
render-embed
|
||||
render-texts]
|
||||
:as params}
|
||||
(us/conform ::render-objects params)
|
||||
|
||||
[type object-id] (:object-id params)]
|
||||
|
||||
(case type
|
||||
:single
|
||||
(mf/html
|
||||
[:& object-svg
|
||||
{:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id object-id
|
||||
:render-embed? render-embed
|
||||
:render-texts? render-texts}])
|
||||
|
||||
:multiple
|
||||
(mf/html
|
||||
[:& objects-svg
|
||||
{:file-id file-id
|
||||
:page-id page-id
|
||||
:object-ids (into #{} object-id)
|
||||
:render-embed? render-embed
|
||||
:render-texts? render-texts}]))))
|
||||
|
||||
;; ---- COMPONENTS SPRITE
|
||||
|
||||
(mf/defc components-sprite-svg
|
||||
[{:keys [file-id embed] :as props}]
|
||||
(let [fetch (mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [] (repo/query! :file {:id file-id})))
|
||||
|
||||
file (use-resource fetch)
|
||||
state (mf/use-state nil)]
|
||||
|
||||
(when file
|
||||
[:*
|
||||
[:style
|
||||
(css [[:body
|
||||
{:margin 0
|
||||
:overflow "hidden"
|
||||
:width "100vw"
|
||||
:height "100vh"}]
|
||||
|
||||
[:main
|
||||
{:overflow "auto"
|
||||
:display "flex"
|
||||
:justify-content "center"
|
||||
:align-items "center"
|
||||
:height "calc(100vh - 200px)"}
|
||||
[:svg {:width "50%"
|
||||
:height "50%"}]]
|
||||
[:.nav
|
||||
{:display "flex"
|
||||
:margin 0
|
||||
:padding "10px"
|
||||
:flex-direction "column"
|
||||
:flex-wrap "wrap"
|
||||
:height "200px"
|
||||
:list-style "none"
|
||||
:overflow-x "scroll"
|
||||
:border-bottom "1px dotted #e6e6e6"}
|
||||
[:a {:cursor :pointer
|
||||
:text-overflow "ellipsis"
|
||||
:white-space "nowrap"
|
||||
:overflow "hidden"
|
||||
:text-decoration "underline"}]
|
||||
[:li {:display "flex"
|
||||
:width "150px"
|
||||
:padding "5px"
|
||||
:border "0px solid black"}]]])]
|
||||
|
||||
[:ul.nav
|
||||
(for [[id data] (get-in file [:data :components])]
|
||||
(let [on-click (fn [event]
|
||||
(dom/prevent-default event)
|
||||
(swap! state assoc :component-id id))]
|
||||
[:li {:key (str id)}
|
||||
[:a {:on-click on-click} (:name data)]]))]
|
||||
|
||||
[:main
|
||||
[:& render/components-sprite-svg
|
||||
{:data (:data file)
|
||||
:embed embed}
|
||||
|
||||
(when-let [component-id (:component-id @state)]
|
||||
[:use {:x 0 :y 0 :xlinkHref (str "#" component-id)}])]]
|
||||
|
||||
])))
|
||||
|
||||
(s/def ::component-id ::us/uuid)
|
||||
(s/def ::render-components
|
||||
(s/keys :req-un [::file-id]
|
||||
:opt-un [::embed ::component-id]))
|
||||
|
||||
(defn render-components
|
||||
[params]
|
||||
(let [{:keys [file-id component-id embed]} (us/conform ::render-components params)]
|
||||
(mf/html
|
||||
[:& render/render-object
|
||||
[:& components-sprite-svg
|
||||
{:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id object-id
|
||||
:embed? embed
|
||||
:render-texts? render-texts}])))
|
||||
:component-id component-id
|
||||
:embed embed}])))
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
;; 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.util.data
|
||||
"A collection of data transformation utils."
|
||||
(:require [cljs.reader :as r]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; TODO: partially move to app.common.data
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data structure manipulation
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn index-by
|
||||
"Return a indexed map of the collection
|
||||
keyed by the result of executing the getter
|
||||
over each element of the collection."
|
||||
[getter coll]
|
||||
(persistent!
|
||||
(reduce #(assoc! %1 (getter %2) %2) (transient {}) coll)))
|
||||
|
||||
(def index-by-id #(index-by :id %))
|
||||
|
||||
(defn without-nils
|
||||
"Given a map, return a map removing key-value
|
||||
pairs when value is `nil`."
|
||||
[data]
|
||||
(into {} (remove (comp nil? second) data)))
|
||||
|
||||
(defn without-keys
|
||||
"Return a map without the keys provided
|
||||
in the `keys` parameter."
|
||||
[data keys]
|
||||
(persistent!
|
||||
(reduce #(dissoc! %1 %2) (transient data) keys)))
|
||||
|
||||
(defn dissoc-in
|
||||
[m [k & ks :as _keys]]
|
||||
(if ks
|
||||
(if-let [nextmap (get m k)]
|
||||
(let [newmap (dissoc-in nextmap ks)]
|
||||
(if (seq newmap)
|
||||
(assoc m k newmap)
|
||||
(dissoc m k)))
|
||||
m)
|
||||
(dissoc m k)))
|
||||
|
||||
(defn index-of
|
||||
"Return the first index when appears the `v` value
|
||||
in the `coll` collection."
|
||||
[coll v]
|
||||
(first (keep-indexed (fn [idx x]
|
||||
(when (= v x) idx))
|
||||
coll)))
|
||||
|
||||
(defn replace-by-id
|
||||
([value]
|
||||
(map (fn [item]
|
||||
(if (= (:id item) (:id value))
|
||||
value
|
||||
item))))
|
||||
([coll value]
|
||||
(sequence (replace-by-id value) coll)))
|
||||
|
||||
(defn deep-merge
|
||||
"Like merge, but merges maps recursively."
|
||||
[& maps]
|
||||
(if (every? map? maps)
|
||||
(apply merge-with deep-merge maps)
|
||||
(last maps)))
|
||||
|
||||
(defn conj-or-disj
|
||||
"Given a set, and an element remove that element from set
|
||||
if it exists or add it if it does not exists."
|
||||
[s v]
|
||||
(if (contains? s v)
|
||||
(disj s v)
|
||||
(conj s v)))
|
||||
|
||||
(defn enumerate
|
||||
([items] (enumerate items 0))
|
||||
([items start]
|
||||
(loop [idx start
|
||||
items items
|
||||
res []]
|
||||
(if (empty? items)
|
||||
res
|
||||
(recur (inc idx)
|
||||
(rest items)
|
||||
(conj res [idx (first items)]))))))
|
||||
|
||||
(defn concatv
|
||||
[& colls]
|
||||
(loop [colls colls
|
||||
result []]
|
||||
(if (seq colls)
|
||||
(recur (rest colls) (reduce conj result (first colls)))
|
||||
result)))
|
||||
|
||||
(defn seek
|
||||
([pred coll]
|
||||
(seek pred coll nil))
|
||||
([pred coll not-found]
|
||||
(reduce (fn [_ x]
|
||||
(if (pred x)
|
||||
(reduced x)
|
||||
not-found))
|
||||
not-found coll)))
|
||||
|
||||
(defn remove-equal-values [m1 m2]
|
||||
(if (and (map? m1) (map? m2) (not (nil? m1)) (not (nil? m2)))
|
||||
(->> m1
|
||||
(remove (fn [[k v]] (= (k m2) v)))
|
||||
(into {}))
|
||||
m1))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Numbers Parsing
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn nan?
|
||||
[v]
|
||||
(js/isNaN v))
|
||||
|
||||
(defn read-string
|
||||
[v]
|
||||
(r/read-string v))
|
||||
|
||||
(defn parse-int
|
||||
([v]
|
||||
(parse-int v nil))
|
||||
([v default]
|
||||
(let [v (js/parseInt v 10)]
|
||||
(if (or (not v) (nan? v))
|
||||
default
|
||||
v))))
|
||||
|
||||
(defn parse-float
|
||||
([v]
|
||||
(parse-float v nil))
|
||||
([v default]
|
||||
(let [v (js/parseFloat v)]
|
||||
(if (or (not v) (nan? v))
|
||||
default
|
||||
v))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Other
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn normalize-props
|
||||
[props]
|
||||
(clj->js props :keyword-fn (fn [key]
|
||||
(if (or (= key :class) (= key :class-name))
|
||||
"className"
|
||||
(str/camel (name key))))))
|
||||
|
||||
(defn matches-search
|
||||
[name search-term]
|
||||
(if (str/empty? search-term)
|
||||
true
|
||||
(let [st (str/trim (str/lower search-term))
|
||||
nm (str/trim (str/lower name))]
|
||||
(str/includes? nm st))))
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.util.dom
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.logging :as log]
|
||||
|
@ -231,20 +232,20 @@
|
|||
(.-innerText el)))
|
||||
|
||||
(defn query
|
||||
([^string query]
|
||||
(query globals/document query))
|
||||
([^string selector]
|
||||
(query globals/document selector))
|
||||
|
||||
([^js el ^string query]
|
||||
([^js el ^string selector]
|
||||
(when (some? el)
|
||||
(.querySelector el query))))
|
||||
(.querySelector el selector))))
|
||||
|
||||
(defn query-all
|
||||
([^string query]
|
||||
(query-all globals/document query))
|
||||
([^string selector]
|
||||
(query-all globals/document selector))
|
||||
|
||||
([^js el ^string query]
|
||||
([^js el ^string selector]
|
||||
(when (some? el)
|
||||
(.querySelectorAll el query))))
|
||||
(.querySelectorAll el selector))))
|
||||
|
||||
(defn get-client-position
|
||||
[^js event]
|
||||
|
@ -403,16 +404,16 @@
|
|||
(defn mtype->extension [mtype]
|
||||
;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
|
||||
(case mtype
|
||||
"image/apng" "apng"
|
||||
"image/avif" "avif"
|
||||
"image/gif" "gif"
|
||||
"image/jpeg" "jpg"
|
||||
"image/png" "png"
|
||||
"image/svg+xml" "svg"
|
||||
"image/webp" "webp"
|
||||
"application/zip" "zip"
|
||||
"application/penpot" "penpot"
|
||||
"application/pdf" "pdf"
|
||||
"image/apng" ".apng"
|
||||
"image/avif" ".avif"
|
||||
"image/gif" ".gif"
|
||||
"image/jpeg" ".jpg"
|
||||
"image/png" ".png"
|
||||
"image/svg+xml" ".svg"
|
||||
"image/webp" ".webp"
|
||||
"application/zip" ".zip"
|
||||
"application/penpot" ".penpot"
|
||||
"application/pdf" ".pdf"
|
||||
nil))
|
||||
|
||||
(defn set-attribute! [^js node ^string attr value]
|
||||
|
@ -464,11 +465,11 @@
|
|||
|
||||
(defn trigger-download-uri
|
||||
[filename mtype uri]
|
||||
(let [link (create-element "a")
|
||||
(let [link (create-element "a")
|
||||
extension (mtype->extension mtype)
|
||||
filename (if extension
|
||||
(str filename "." extension)
|
||||
filename)]
|
||||
filename (if (and extension (not (str/ends-with? filename extension)))
|
||||
(str/concat filename extension)
|
||||
filename)]
|
||||
(obj/set! link "href" uri)
|
||||
(obj/set! link "download" filename)
|
||||
(obj/set! (.-style ^js link) "display" "none")
|
||||
|
@ -535,3 +536,13 @@
|
|||
(and (some? node)
|
||||
(some? candidate)
|
||||
(.contains node candidate)))
|
||||
|
||||
(defn seq-nodes
|
||||
[root-node]
|
||||
(letfn [(branch? [node]
|
||||
(d/not-empty? (get-children node)))
|
||||
|
||||
(get-children [node]
|
||||
(seq (.-children node)))]
|
||||
(->> root-node
|
||||
(tree-seq branch? get-children))))
|
||||
|
|
|
@ -214,11 +214,13 @@
|
|||
|
||||
(= type :frame)
|
||||
(let [;; The nodes with the "frame-background" class can have some anidation depending on the strokes they have
|
||||
g-nodes (find-all-nodes node :g)
|
||||
g-nodes (find-all-nodes node :g)
|
||||
defs-nodes (flatten (map #(find-all-nodes % :defs) g-nodes))
|
||||
gg-nodes (flatten (map #(find-all-nodes % :g) g-nodes))
|
||||
rect-nodes (flatten [[(find-all-nodes node :rect)]
|
||||
(map #(find-all-nodes % #{:rect :path}) defs-nodes)
|
||||
(map #(find-all-nodes % #{:rect :path}) g-nodes)])
|
||||
(map #(find-all-nodes % #{:rect :path}) g-nodes)
|
||||
(map #(find-all-nodes % #{:rect :path}) gg-nodes)])
|
||||
svg-node (d/seek #(= "frame-background" (get-in % [:attrs :class])) rect-nodes)]
|
||||
(merge (add-attrs {} (:attrs svg-node)) node-attrs))
|
||||
|
||||
|
@ -405,17 +407,31 @@
|
|||
[props node svg-data]
|
||||
|
||||
(let [fill (:fill svg-data)
|
||||
hide-fill-on-export (get-meta node :hide-fill-on-export str->bool)
|
||||
fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid)
|
||||
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid)
|
||||
gradient (when (str/starts-with? fill "url")
|
||||
(parse-gradient node fill))]
|
||||
hide-fill-on-export (get-meta node :hide-fill-on-export str->bool)
|
||||
fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid)
|
||||
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid)
|
||||
meta-fill-color (get-meta node :fill-color)
|
||||
meta-fill-opacity (get-meta node :fill-opacity)
|
||||
meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url")
|
||||
(parse-gradient node meta-fill-color)
|
||||
(get-meta node :fill-color-gradient))
|
||||
gradient (when (str/starts-with? fill "url")
|
||||
(parse-gradient node fill))]
|
||||
|
||||
(cond-> props
|
||||
:always
|
||||
(assoc :fill-color nil
|
||||
:fill-opacity nil)
|
||||
|
||||
(some? meta-fill-color)
|
||||
(assoc :fill-color meta-fill-color
|
||||
:fill-opacity (d/parse-double meta-fill-opacity))
|
||||
|
||||
(some? meta-fill-color-gradient)
|
||||
(assoc :fill-color-gradient meta-fill-color-gradient
|
||||
:fill-color nil
|
||||
:fill-opacity nil)
|
||||
|
||||
(some? gradient)
|
||||
(assoc :fill-color-gradient gradient
|
||||
:fill-color nil
|
||||
|
@ -775,6 +791,7 @@
|
|||
(-> node
|
||||
(find-node :defs)
|
||||
(find-node :pattern)
|
||||
(find-node :g)
|
||||
(find-node :image))]
|
||||
(or (= type :image)
|
||||
(some? pattern-image))))
|
||||
|
@ -789,12 +806,51 @@
|
|||
(-> node
|
||||
(find-node :defs)
|
||||
(find-node :pattern)
|
||||
(find-node :g)
|
||||
(find-node :image)
|
||||
:attrs)
|
||||
image-data (get-svg-data :image node)
|
||||
svg-data (or image-data pattern-data)]
|
||||
(:xlink:href svg-data)))
|
||||
|
||||
(defn get-image-fill
|
||||
[node]
|
||||
(let [linear-gradient-node (-> node
|
||||
(find-node :defs)
|
||||
(find-node :linearGradient))
|
||||
radial-gradient-node (-> node
|
||||
(find-node :defs)
|
||||
(find-node :radialGradient))
|
||||
gradient-node (or linear-gradient-node radial-gradient-node)
|
||||
stops (parse-stops gradient-node)
|
||||
gradient (cond-> {:stops stops}
|
||||
(some? linear-gradient-node)
|
||||
(assoc :type :linear
|
||||
:start-x (-> linear-gradient-node :attrs :x1 d/parse-double)
|
||||
:start-y (-> linear-gradient-node :attrs :y1 d/parse-double)
|
||||
:end-x (-> linear-gradient-node :attrs :x2 d/parse-double)
|
||||
:end-y (-> linear-gradient-node :attrs :y2 d/parse-double)
|
||||
:width 1)
|
||||
|
||||
(some? radial-gradient-node)
|
||||
(assoc :type :linear
|
||||
:start-x (get-meta radial-gradient-node :start-x d/parse-double)
|
||||
:start-y (get-meta radial-gradient-node :start-y d/parse-double)
|
||||
:end-x (get-meta radial-gradient-node :end-x d/parse-double)
|
||||
:end-y (get-meta radial-gradient-node :end-y d/parse-double)
|
||||
:width (get-meta radial-gradient-node :width d/parse-double)))]
|
||||
|
||||
(if (some? (or linear-gradient-node radial-gradient-node))
|
||||
{:fill-color-gradient gradient}
|
||||
(-> node
|
||||
(find-node :defs)
|
||||
(find-node :pattern)
|
||||
(find-node :g)
|
||||
(find-node :rect)
|
||||
:attrs
|
||||
:style
|
||||
parse-style))))
|
||||
|
||||
(defn parse-data
|
||||
[type node]
|
||||
|
||||
|
|
|
@ -302,16 +302,17 @@
|
|||
(reduce simplify-command [[start] start-pos start-pos start-pos start-pos])
|
||||
(first))))
|
||||
|
||||
|
||||
(defn parse-path [path-str]
|
||||
(let [clean-path-str
|
||||
(-> path-str
|
||||
(str/trim)
|
||||
;; Change "commas" for spaces
|
||||
(str/replace #"," " ")
|
||||
;; Remove all consecutive spaces
|
||||
(str/replace #"\s+" " "))
|
||||
commands (re-seq commands-regex clean-path-str)]
|
||||
(-> (mapcat parse-command commands)
|
||||
(simplify-commands))))
|
||||
(if (empty? path-str)
|
||||
path-str
|
||||
(let [clean-path-str
|
||||
(-> path-str
|
||||
(str/trim)
|
||||
;; Change "commas" for spaces
|
||||
(str/replace #"," " ")
|
||||
;; Remove all consecutive spaces
|
||||
(str/replace #"\s+" " "))
|
||||
commands (re-seq commands-regex clean-path-str)]
|
||||
(-> (mapcat parse-command commands)
|
||||
(simplify-commands)))))
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
(:require
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
|
||||
(def ^:const trail-zeros-regex-1 #"\.0+$")
|
||||
(def ^:const trail-zeros-regex-2 #"(\.\d*[^0])0+$")
|
||||
|
||||
|
@ -36,3 +35,10 @@
|
|||
(catch :default _
|
||||
(str num))))
|
||||
|
||||
(defn matches-search
|
||||
[name search-term]
|
||||
(if (str/empty? search-term)
|
||||
true
|
||||
(let [st (str/trim (str/lower search-term))
|
||||
nm (str/trim (str/lower name))]
|
||||
(str/includes? nm st))))
|
||||
|
|
|
@ -54,9 +54,11 @@
|
|||
|
||||
(reply-error [cause]
|
||||
(if (map? cause)
|
||||
(post {:error cause})
|
||||
(post {:error {:type :unexpected
|
||||
:code :unhandled-error-on-worker
|
||||
(post {:error {:type :worker-error
|
||||
:code (or (:type cause) :wrapped)
|
||||
:data cause}})
|
||||
(post {:error {:type :worker-error
|
||||
:code :unhandled-error
|
||||
:hint (ex-message cause)
|
||||
:data (ex-data cause)}})))
|
||||
|
||||
|
|
|
@ -135,7 +135,7 @@
|
|||
(rx/map #(assoc % :file-id file-id))
|
||||
(rx/flat-map
|
||||
(fn [media]
|
||||
(let [file-path (str file-id "/media/" (:id media) "." (dom/mtype->extension (:mtype media)))]
|
||||
(let [file-path (str/concat file-id "/media/" (:id media) (dom/mtype->extension (:mtype media)))]
|
||||
(->> (http/send!
|
||||
{:uri (cfg/resolve-file-media media)
|
||||
:response-type :blob
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
;; Upload changes batches size
|
||||
(def ^:const change-batch-size 100)
|
||||
|
||||
(def conjv (fnil conj []))
|
||||
|
||||
(defn get-file
|
||||
"Resolves the file inside the context given its id and the data"
|
||||
([context type]
|
||||
|
@ -48,7 +50,7 @@
|
|||
:typographies (str file-id "/typographies.json")
|
||||
:media-list (str file-id "/media.json")
|
||||
:media (let [ext (dom/mtype->extension (:mtype media))]
|
||||
(str file-id "/media/" id "." ext))
|
||||
(str/concat file-id "/media/" id ext))
|
||||
:components (str file-id "/components.svg"))
|
||||
|
||||
parse-svg? (and (not= type :media) (str/ends-with? path "svg"))
|
||||
|
@ -261,25 +263,29 @@
|
|||
(cond-> (some? old-id)
|
||||
(assoc :id (resolve old-id)))
|
||||
(cond-> (< (:version context 1) 2)
|
||||
(translate-frame type file)))
|
||||
(translate-frame type file)))]
|
||||
(try
|
||||
(let [file (case type
|
||||
:frame (fb/add-artboard file data)
|
||||
:group (fb/add-group file data)
|
||||
:bool (fb/add-bool file data)
|
||||
:rect (fb/create-rect file data)
|
||||
:circle (fb/create-circle file data)
|
||||
:path (fb/create-path file data)
|
||||
:text (fb/create-text file data)
|
||||
:image (fb/create-image file data)
|
||||
:svg-raw (fb/create-svg-raw file data)
|
||||
#_default file)]
|
||||
|
||||
file (case type
|
||||
:frame (fb/add-artboard file data)
|
||||
:group (fb/add-group file data)
|
||||
:bool (fb/add-bool file data)
|
||||
:rect (fb/create-rect file data)
|
||||
:circle (fb/create-circle file data)
|
||||
:path (fb/create-path file data)
|
||||
:text (fb/create-text file data)
|
||||
:image (fb/create-image file data)
|
||||
:svg-raw (fb/create-svg-raw file data)
|
||||
#_default file)]
|
||||
;; We store this data for post-processing after every shape has been
|
||||
;; added
|
||||
(cond-> file
|
||||
(d/not-empty? interactions)
|
||||
(assoc-in [:interactions (:id data)] interactions)))
|
||||
|
||||
;; We store this data for post-processing after every shape has been
|
||||
;; added
|
||||
(cond-> file
|
||||
(d/not-empty? interactions)
|
||||
(assoc-in [:interactions (:id data)] interactions))))))
|
||||
(catch :default err
|
||||
(log/error :hint (ex-message err) :cause err :js/data data)
|
||||
(update file :errors conjv data)))))))
|
||||
|
||||
(defn setup-interactions
|
||||
[file]
|
||||
|
@ -300,8 +306,9 @@
|
|||
(if (and (not (cip/close? node))
|
||||
(cip/has-image? node))
|
||||
(let [name (cip/get-image-name node)
|
||||
data-uri (cip/get-image-data node)]
|
||||
(->> (upload-media-files context file-id name data-uri)
|
||||
image-data (cip/get-image-data node)
|
||||
image-fill (cip/get-image-fill node)]
|
||||
(->> (upload-media-files context file-id name image-data)
|
||||
(rx/catch #(do (.error js/console "Error uploading media: " name)
|
||||
(rx/of node)))
|
||||
(rx/map
|
||||
|
@ -310,7 +317,13 @@
|
|||
(assoc-in [:attrs :penpot:media-id] (:id media))
|
||||
(assoc-in [:attrs :penpot:media-width] (:width media))
|
||||
(assoc-in [:attrs :penpot:media-height] (:height media))
|
||||
(assoc-in [:attrs :penpot:media-mtype] (:mtype media)))))))
|
||||
(assoc-in [:attrs :penpot:media-mtype] (:mtype media))
|
||||
|
||||
(assoc-in [:attrs :penpot:fill-color] (:fill image-fill))
|
||||
(assoc-in [:attrs :penpot:fill-color-ref-file] (:fill-color-ref-file image-fill))
|
||||
(assoc-in [:attrs :penpot:fill-color-ref-id] (:fill-color-ref-id image-fill))
|
||||
(assoc-in [:attrs :penpot:fill-opacity] (:fill-opacity image-fill))
|
||||
(assoc-in [:attrs :penpot:fill-color-gradient] (:fill-color-gradient image-fill)))))))
|
||||
|
||||
;; If the node is not an image just return the node
|
||||
(->> (rx/of node)
|
||||
|
|
|
@ -16,22 +16,35 @@
|
|||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn- not-found?
|
||||
[{:keys [type]}]
|
||||
(= :not-found type))
|
||||
|
||||
(defn- handle-response
|
||||
[response]
|
||||
[{:keys [body status] :as response}]
|
||||
(cond
|
||||
(http/success? response)
|
||||
(rx/of (:body response))
|
||||
|
||||
(http/client-error? response)
|
||||
(rx/throw (:body response))
|
||||
(= status 413)
|
||||
(rx/throw {:type :validation
|
||||
:code :request-body-too-large
|
||||
:hint "request body too large"})
|
||||
|
||||
(and (http/client-error? response)
|
||||
(map? body))
|
||||
(rx/throw body)
|
||||
|
||||
:else
|
||||
(rx/throw {:type :unexpected
|
||||
:code (:error response)})))
|
||||
(rx/throw {:type :unexpected-error
|
||||
:code :unhandled-http-response
|
||||
:http-status status
|
||||
:http-body body})))
|
||||
|
||||
(defn- not-found?
|
||||
[{:keys [type]}]
|
||||
(= :not-found type))
|
||||
|
||||
(defn- body-too-large?
|
||||
[{:keys [type code]}]
|
||||
(and (= :validation type)
|
||||
(= :request-body-too-large code)))
|
||||
|
||||
(defn- request-data-for-thumbnail
|
||||
[file-id revn]
|
||||
|
@ -56,16 +69,19 @@
|
|||
:uri (u/join (cfg/get-public-uri) path)
|
||||
:credentials "include"
|
||||
:query params}]
|
||||
|
||||
(->> (http/send! request)
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response))))
|
||||
|
||||
(defn- render-thumbnail
|
||||
[{:keys [data file-id revn] :as params}]
|
||||
(let [elem (if-let [frame (:thumbnail-frame data)]
|
||||
(mf/element render/frame-svg #js {:objects (:objects data) :frame frame})
|
||||
(mf/element render/page-svg #js {:data data :width "290" :height "150" :thumbnails? true}))]
|
||||
{:data (rds/renderToStaticMarkup elem)
|
||||
[{:keys [page file-id revn] :as params}]
|
||||
(let [objects (:objects page)
|
||||
frame (some->> page :thumbnail-frame-id (get objects))
|
||||
element (if frame
|
||||
(mf/element render/frame-svg #js {:objects objects :frame frame})
|
||||
(mf/element render/page-svg #js {:data page :thumbnails? true}))]
|
||||
{:data (rds/renderToStaticMarkup element)
|
||||
:fonts @fonts/loaded
|
||||
:file-id file-id
|
||||
:revn revn}))
|
||||
|
@ -81,9 +97,11 @@
|
|||
:uri (u/join (cfg/get-public-uri) path)
|
||||
:credentials "include"
|
||||
:body (http/transit-data params)}]
|
||||
|
||||
(->> (http/send! request)
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)
|
||||
(rx/catch body-too-large? (constantly nil))
|
||||
(rx/map (constantly params)))))
|
||||
|
||||
(defmethod impl/handler :thumbnails/generate
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns debug
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
|
@ -17,6 +18,7 @@
|
|||
[app.main.data.workspace.path.shortcuts]
|
||||
[app.main.data.workspace.shortcuts]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as timers]
|
||||
[beicon.core :as rx]
|
||||
|
@ -25,6 +27,12 @@
|
|||
[potok.core :as ptk]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn ^:export set-logging
|
||||
([level]
|
||||
(l/set-level! :app (keyword level)))
|
||||
([ns level]
|
||||
(l/set-level! (keyword ns) (keyword level))))
|
||||
|
||||
(def debug-options
|
||||
#{;; Displays the bounding box for the shapes
|
||||
:bounding-boxes
|
||||
|
@ -333,3 +341,9 @@
|
|||
(.log js/console "%c Viewer" style)
|
||||
(print-shortcuts app.main.data.viewer.shortcuts/shortcuts)))
|
||||
nil)
|
||||
|
||||
(defn ^:export nodeStats
|
||||
[]
|
||||
(let [root-node (dom/query ".viewport .render-shapes")
|
||||
num-nodes (->> (dom/seq-nodes root-node) count)]
|
||||
#js {:number num-nodes}))
|
||||
|
|
|
@ -484,14 +484,14 @@ atob@^2.1.2:
|
|||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
||||
|
||||
autoprefixer@^10.4.2:
|
||||
version "10.4.2"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b"
|
||||
integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ==
|
||||
autoprefixer@^10.4.4:
|
||||
version "10.4.4"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.4.tgz#3e85a245b32da876a893d3ac2ea19f01e7ea5a1e"
|
||||
integrity sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA==
|
||||
dependencies:
|
||||
browserslist "^4.19.1"
|
||||
caniuse-lite "^1.0.30001297"
|
||||
fraction.js "^4.1.2"
|
||||
browserslist "^4.20.2"
|
||||
caniuse-lite "^1.0.30001317"
|
||||
fraction.js "^4.2.0"
|
||||
normalize-range "^0.1.2"
|
||||
picocolors "^1.0.0"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
@ -709,15 +709,15 @@ browserify-zlib@^0.2.0:
|
|||
dependencies:
|
||||
pako "~1.0.5"
|
||||
|
||||
browserslist@^4.19.1:
|
||||
version "4.19.1"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3"
|
||||
integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==
|
||||
browserslist@^4.20.2:
|
||||
version "4.20.2"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88"
|
||||
integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==
|
||||
dependencies:
|
||||
caniuse-lite "^1.0.30001286"
|
||||
electron-to-chromium "^1.4.17"
|
||||
caniuse-lite "^1.0.30001317"
|
||||
electron-to-chromium "^1.4.84"
|
||||
escalade "^3.1.1"
|
||||
node-releases "^2.0.1"
|
||||
node-releases "^2.0.2"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
buffer-crc32@~0.2.3:
|
||||
|
@ -823,10 +823,10 @@ camelcase@^6.2.0:
|
|||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||
|
||||
caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297:
|
||||
version "1.0.30001312"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz#e11eba4b87e24d22697dae05455d5aea28550d5f"
|
||||
integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==
|
||||
caniuse-lite@^1.0.30001317:
|
||||
version "1.0.30001322"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001322.tgz#2e4c09d11e1e8f852767dab287069a8d0c29d623"
|
||||
integrity sha512-neRmrmIrCGuMnxGSoh+x7zYtQFFgnSY2jaomjU56sCkTA6JINqQrxutF459JpWcWRajvoyn95sOXq4Pqrnyjew==
|
||||
|
||||
caseless@~0.12.0:
|
||||
version "0.12.0"
|
||||
|
@ -1387,10 +1387,10 @@ cypress-file-upload@^5.0.8:
|
|||
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
|
||||
integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==
|
||||
|
||||
cypress@^9.5.0:
|
||||
version "9.5.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.0.tgz#704a79f0d3d4e775f433334eb8f5ae065e3bea31"
|
||||
integrity sha512-rC5QPolKsVjJ8QJZ7IeZ6HlKM4gswBGZc0XvoAJNL8urQCSL8zTX0A/ai/h35WfF47NQ0iSZnwIXBlHX3MOUIQ==
|
||||
cypress@^9.5.3:
|
||||
version "9.5.3"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.3.tgz#7c56b50fc1f1aa69ef10b271d895aeb4a1d7999e"
|
||||
integrity sha512-ItelIVmqMTnKYbo1JrErhsGgQGjWOxCpHT1TfMvwnIXKXN/OSlPjEK7rbCLYDZhejQL99PmUqul7XORI24Ik0A==
|
||||
dependencies:
|
||||
"@cypress/request" "^2.88.10"
|
||||
"@cypress/xvfb" "^1.2.4"
|
||||
|
@ -1424,7 +1424,7 @@ cypress@^9.5.0:
|
|||
listr2 "^3.8.3"
|
||||
lodash "^4.17.21"
|
||||
log-symbols "^4.0.0"
|
||||
minimist "^1.2.5"
|
||||
minimist "^1.2.6"
|
||||
ospath "^1.2.2"
|
||||
pretty-bytes "^5.6.0"
|
||||
proxy-from-env "1.0.0"
|
||||
|
@ -1686,10 +1686,10 @@ editorconfig@^0.15.3:
|
|||
semver "^5.6.0"
|
||||
sigmund "^1.0.1"
|
||||
|
||||
electron-to-chromium@^1.4.17:
|
||||
version "1.4.71"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz#17056914465da0890ce00351a3b946fd4cd51ff6"
|
||||
integrity sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==
|
||||
electron-to-chromium@^1.4.84:
|
||||
version "1.4.98"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.98.tgz#1a9a0dec9792e78c5be1df052b6c74078d6b1b16"
|
||||
integrity sha512-1IdsuSAnIGVxoYT1LkcUFb9MfjRxdHhCU9qiaDzhl1XvYgK9c8E2O9aJOPgGMQ68CSI8NxmLwrYhjvGauT8yuw==
|
||||
|
||||
elliptic@^6.5.3:
|
||||
version "6.5.4"
|
||||
|
@ -2164,10 +2164,10 @@ form-data@~2.3.2:
|
|||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fraction.js@^4.1.2:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.3.tgz#be65b0f20762ef27e1e793860bc2dfb716e99e65"
|
||||
integrity sha512-pUHWWt6vHzZZiQJcM6S/0PXfS+g6FM4BF5rj9wZyreivhQPdsh5PpE25VtSNxq80wHS5RfY51Ii+8Z0Zl/pmzg==
|
||||
fraction.js@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
|
||||
integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
|
||||
|
||||
fragment-cache@^0.2.1:
|
||||
version "0.2.1"
|
||||
|
@ -3163,10 +3163,10 @@ isstream@~0.1.2:
|
|||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
|
||||
|
||||
js-beautify@^1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.0.tgz#2ce790c555d53ce1e3d7363227acf5dc69024c2d"
|
||||
integrity sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ==
|
||||
js-beautify@^1.14.2:
|
||||
version "1.14.2"
|
||||
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.2.tgz#8180514fd4c7789c4ac4bcc327b6dda634c55666"
|
||||
integrity sha512-H85kX95a53os+q1OCqtYe8AXAmgy3BvtysA/V83S3fdhznm6WlUpGi14DqSPbKFsL3dXZFXYl7YQwW9U1+76ng==
|
||||
dependencies:
|
||||
config-chain "^1.1.12"
|
||||
editorconfig "^0.15.3"
|
||||
|
@ -3512,10 +3512,10 @@ lru-queue@^0.1.0:
|
|||
dependencies:
|
||||
es5-ext "~0.10.2"
|
||||
|
||||
luxon@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.0.tgz#bf16a7e642513c2a20a6230a6a41b0ab446d0045"
|
||||
integrity sha512-gv6jZCV+gGIrVKhO90yrsn8qXPKD8HYZJtrUDSfEbow8Tkw84T9OnCyJhWvnJIaIF/tBuiAjZuQHUt1LddX2mg==
|
||||
luxon@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.1.tgz#f276b1b53fd9a740a60e666a541a7f6dbed4155a"
|
||||
integrity sha512-I8vnjOmhXsMSlNMZlMkSOvgrxKJl0uOsEzdGgGNZuZPaS9KlefpE9KV95QFftlJSC+1UyCC9/I69R02cz/zcCA==
|
||||
|
||||
make-dir@^3.0.0:
|
||||
version "3.1.0"
|
||||
|
@ -3677,6 +3677,11 @@ minimist@^1.2.0, minimist@^1.2.5:
|
|||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
minimist@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mixin-deep@^1.2.0:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
|
||||
|
@ -3732,10 +3737,10 @@ nan@^2.12.1:
|
|||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
|
||||
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
|
||||
|
||||
nanoid@^3.2.0:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
|
||||
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
|
||||
nanoid@^3.3.1:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.2.tgz#c89622fafb4381cd221421c69ec58547a1eec557"
|
||||
integrity sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
|
@ -3805,7 +3810,7 @@ node-libs-browser@^2.2.1:
|
|||
util "^0.11.0"
|
||||
vm-browserify "^1.0.1"
|
||||
|
||||
node-releases@^2.0.1:
|
||||
node-releases@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01"
|
||||
integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==
|
||||
|
@ -4360,12 +4365,12 @@ postcss@^7.0.16:
|
|||
picocolors "^0.2.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
postcss@^8.4.6:
|
||||
version "8.4.6"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1"
|
||||
integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==
|
||||
postcss@^8.4.12:
|
||||
version "8.4.12"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905"
|
||||
integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==
|
||||
dependencies:
|
||||
nanoid "^3.2.0"
|
||||
nanoid "^3.3.1"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
|
@ -4374,10 +4379,10 @@ prepend-http@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||
|
||||
prettier@^2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
|
||||
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
|
||||
prettier@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.1.tgz#d472797e0d7461605c1609808e27b80c0f9cfe17"
|
||||
integrity sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==
|
||||
|
||||
pretty-bytes@^5.6.0:
|
||||
version "5.6.0"
|
||||
|
@ -4876,13 +4881,20 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
|||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rxjs@^7.5.1, rxjs@~7.5.2:
|
||||
rxjs@^7.5.1:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.4.tgz#3d6bd407e6b7ce9a123e76b1e770dc5761aa368d"
|
||||
integrity sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
rxjs@~7.5.5:
|
||||
version "7.5.5"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f"
|
||||
integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
|
@ -4910,10 +4922,10 @@ safe-stable-stringify@^2.3.1:
|
|||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sass@^1.49.7:
|
||||
version "1.49.7"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.7.tgz#22a86a50552b9b11f71404dfad1b9ff44c6b0c49"
|
||||
integrity sha512-13dml55EMIR2rS4d/RDHHP0sXMY3+30e1TKsyXaSz3iLWVoDWEoboY8WzJd5JMnxrRHffKO3wq2mpJ0jxRJiEQ==
|
||||
sass@^1.49.9:
|
||||
version "1.49.9"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.9.tgz#b15a189ecb0ca9e24634bae5d1ebc191809712f9"
|
||||
integrity sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
immutable "^4.0.0"
|
||||
|
@ -5001,10 +5013,10 @@ 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.17.5:
|
||||
version "2.17.5"
|
||||
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.17.5.tgz#ed2fa8b06ea62cb310f069b70314e555f5bf9d50"
|
||||
integrity sha512-Xvev4OLxGkjxC5mT5jHZDpJuAKzSHn7bGa4RPBm+Jp2gBz4iNkNDNPDvkyqt0r9RD0SWaYJF8zGyxi5c18yJBw==
|
||||
shadow-cljs@2.17.8:
|
||||
version "2.17.8"
|
||||
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.17.8.tgz#7ee27ccf7585991f6c042f66f07f17582c0b70af"
|
||||
integrity sha512-O39cLA7ukEh+OeH1yZlaWjGFinPOsDD87TetAWPe1QBD9TZQ0Ail+2ovaXeAyZpJ+6Z37joFfival+LNuCgsmQ==
|
||||
dependencies:
|
||||
node-libs-browser "^2.2.1"
|
||||
readline-sync "^1.4.7"
|
||||
|
|
Loading…
Add table
Reference in a new issue