From 11c3b6cfe21564ad27a69c3825a4d126a14370f6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Apr 2022 23:46:42 +0200 Subject: [PATCH 01/32] :bug: Fix issue with password persistence --- CHANGES.md | 1 + backend/src/app/rpc/mutations/profile.clj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a169138a31..7d73bf5687 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -88,6 +88,7 @@ - Fix issue with shift+select to deselect shapes [Taiga #3154](https://tree.taiga.io/project/penpot/issue/3154) - Fix issue with drag-select shapes [Taiga #3165](https://tree.taiga.io/project/penpot/issue/3165) +- Fix issue on password persistence after registration process on private instances ## 1.12.2-beta diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 855a6ef324..bd4179edbb 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -130,6 +130,7 @@ :hint "you can't use your email as password")) (let [params {:email (:email params) + :password (:password params) :invitation-token (:invitation-token params) :backend "penpot" :iss :prepared-register @@ -156,7 +157,6 @@ (let [claims (tokens :verify {:token token :iss :prepared-register}) params (merge params claims)] (check-profile-existence! conn params) - (let [is-active (or (:is-active params) (contains? cf/flags :insecure-register)) profile (->> (assoc params :is-active is-active) From f43c6ab3c560264871e408a135566842a8d91dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 4 Apr 2022 11:23:45 +0200 Subject: [PATCH 02/32] :bug: Fix resize for rotated shapes with top&down constraints --- CHANGES.md | 1 + common/src/app/common/geom/shapes/constraints.cljc | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7d73bf5687..f1f2002732 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -78,6 +78,7 @@ - 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) ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/common/src/app/common/geom/shapes/constraints.cljc b/common/src/app/common/geom/shapes/constraints.cljc index 8732be74b6..4c404d8f12 100644 --- a/common/src/app/common/geom/shapes/constraints.cljc +++ b/common/src/app/common/geom/shapes/constraints.cljc @@ -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))} {}))) From fc7707ad3e15740c1ea16bf01eca62d29b36bd3b Mon Sep 17 00:00:00 2001 From: Eva Date: Tue, 5 Apr 2022 10:35:26 +0200 Subject: [PATCH 03/32] :bug: Fix sidebar icon in viewer mode --- CHANGES.md | 1 + frontend/resources/styles/main/partials/sidebar.scss | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 7d73bf5687..3f22377ea1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ ### :bug: Bugs fixed +- 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) diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index 084de50e91..452ee52207 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -82,7 +82,10 @@ } .tool-window-bar-icon { - height: 15px; + height: 21px; + display: flex; + align-items: center; + justify-content: center; svg { width: 15px; From abd7a88ba0ca1a89a9374e97620e2d3ccbb707aa Mon Sep 17 00:00:00 2001 From: Eva Date: Mon, 4 Apr 2022 12:25:27 +0200 Subject: [PATCH 04/32] :bug: Fix scroll into view behing fixed element --- CHANGES.md | 1 + frontend/src/app/main/ui/workspace/sidebar/layers.cljs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6105f4267f..d18eab887f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ ### :bug: Bugs fixed +- 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) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index e9f619f516..3633b16746 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -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))))) From cef1c0d1d1af51e77e4a336607e43a7f3d7bf7b1 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 5 Apr 2022 07:48:10 +0200 Subject: [PATCH 05/32] :bug: Edit file name navigates to the file workspace --- CHANGES.md | 1 + frontend/src/app/main/ui/dashboard/inline_edition.cljs | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index d18eab887f..21acf48903 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ ### :bug: Bugs fixed +- 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) diff --git a/frontend/src/app/main/ui/dashboard/inline_edition.cljs b/frontend/src/app/main/ui/dashboard/inline_edition.cljs index 1840afe7b6..a16950e78b 100644 --- a/frontend/src/app/main/ui/dashboard/inline_edition.cljs +++ b/frontend/src/app/main/ui/dashboard/inline_edition.cljs @@ -44,6 +44,7 @@ on-keyup (mf/use-callback (fn [event] + (dom/stop-propagation event) (cond (kbd/esc? event) (on-cancel) From 911d4edb9f675f9e8d538a41b06fc687c1359918 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 1 Apr 2022 08:58:41 +0200 Subject: [PATCH 06/32] :bug: Import a file with image background won't show the background --- frontend/src/app/util/import/parser.cljs | 64 ++++++++++++++++++++++-- frontend/src/app/worker/import.cljs | 13 +++-- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index 71d3946b77..b00485efab 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -405,17 +405,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 +789,7 @@ (-> node (find-node :defs) (find-node :pattern) + (find-node :g) (find-node :image))] (or (= type :image) (some? pattern-image)))) @@ -789,12 +804,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] diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index ad09e48bb2..feeab2ee03 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -300,8 +300,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 +311,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) From d6abd2202cb862115dba322338b8da6b4b7c116f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 5 Apr 2022 13:04:44 +0200 Subject: [PATCH 07/32] :bug: Revert pixel grid color change --- frontend/src/app/main/ui/workspace/viewport/widgets.cljs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index a4361ad65d..a9661c26f0 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -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) From 9140fc71b98159c6f871490ebc99e29fbd849d94 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 29 Mar 2022 12:34:11 +0200 Subject: [PATCH 08/32] :recycle: Refactor exportation process, make it considerably faster --- backend/src/app/rpc/queries/files.clj | 180 ++++++------ common/deps.edn | 2 +- common/src/app/common/data/macros.cljc | 89 +----- common/src/app/common/logging.cljc | 1 - common/src/app/common/pprint.cljc | 27 ++ exporter/src/app/browser.cljs | 7 +- exporter/src/app/handlers.cljs | 20 +- exporter/src/app/handlers/export_frames.cljs | 161 +++++------ exporter/src/app/handlers/export_shapes.cljs | 218 +++++++++------ exporter/src/app/handlers/resources.cljs | 109 ++------ exporter/src/app/renderer.cljs | 45 +++ exporter/src/app/renderer/bitmap.cljs | 104 +++---- exporter/src/app/renderer/pdf.cljs | 102 ++++--- exporter/src/app/renderer/svg.cljs | 133 ++++----- exporter/src/app/util/mime.cljs | 32 +++ exporter/src/app/util/shell.cljs | 7 +- frontend/deps.edn | 4 +- frontend/src/app/main/data/exports.cljs | 98 +++---- frontend/src/app/main/errors.cljs | 6 +- frontend/src/app/main/render.cljs | 231 +++++++++++----- frontend/src/app/main/repo.cljs | 28 +- frontend/src/app/main/ui.cljs | 10 - frontend/src/app/main/ui/export.cljs | 22 +- frontend/src/app/main/ui/render.cljs | 203 -------------- frontend/src/app/main/ui/routes.cljs | 1 - .../ui/viewer/handoff/attributes/image.cljs | 9 +- frontend/src/app/main/ui/viewer/shapes.cljs | 31 --- .../sidebar/options/menus/exports.cljs | 8 +- frontend/src/app/render.cljs | 256 ++++++++++++++++-- frontend/src/app/util/dom.cljs | 28 +- frontend/src/app/worker/export.cljs | 2 +- frontend/src/app/worker/import.cljs | 2 +- frontend/src/app/worker/thumbnails.cljs | 10 +- 33 files changed, 1096 insertions(+), 1090 deletions(-) create mode 100644 common/src/app/common/pprint.cljc create mode 100644 exporter/src/app/renderer.cljs create mode 100644 exporter/src/app/util/mime.cljs delete mode 100644 frontend/src/app/main/ui/render.cljs diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index db6dfc45eb..9794aa6a99 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -205,107 +205,27 @@ (-> (retrieve-file cfg id) (assoc :permissions perms)))) -(declare trim-file-data) - -(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) +(defn- trim-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)))) -(s/def ::strip-frames-with-thumbnails ::us/boolean) +(defn- prune-thumbnails + "Given the page data, removes the `:thumbnail` prop from all + shapes." + [page] + (update page :objects (fn [objects] + (d/mapm #(dissoc %2 :thumbnail) objects)))) -(s/def ::page - (s/keys :req-un [::profile-id ::file-id] - :opt-un [::strip-frames-with-thumbnails])) - -(sv/defmethod ::page - "Retrieves the first page of the file. Used mainly for render - thumbnails on dashboard. - - DEPRECATED: still here for backward compatibility." - [{: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-first-page-data file props)] - data)) - -(s/def ::file-data-for-thumbnail - (s/keys :req-un [::profile-id ::file-id] - :opt-un [::strip-frames-with-thumbnails])) - -(sv/defmethod ::file-data-for-thumbnail - "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)})) - -(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] +(defn- prune-frames-with-thumbnails + "Remove unnecesary shapes from frames that have thumbnail from page + data." + [page] (let [filter-shape? (fn [objects [id shape]] (let [frame-id (:frame-id shape)] @@ -328,7 +248,71 @@ (filter (partial filter-shape? objects))) objects))] - (update data :objects update-objects))) + (update page :objects update-objects))) + +(defn- get-thumbnail-data + [{:keys [data] :as file}] + (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))] + (-> (assoc page :objects objects) + (assoc :thumbnail-frame frame))) + + (let [page-id (-> data :pages first)] + (-> (get-in data [:pages-index page-id]) + (prune-frames-with-thumbnails))))) + +(s/def ::page-id ::us/uuid) +(s/def ::object-id ::us/uuid) +(s/def ::prune-frames-with-thumbnails ::us/boolean) +(s/def ::prune-thumbnails ::us/boolean) + +(s/def ::page + (s/keys :req-un [::profile-id ::file-id] + :opt-un [::page-id + ::object-id + ::prune-frames-with-thumbnails + ::prune-thumbnails])) + +(sv/defmethod ::page + "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. + + 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) + page-id (or page-id (-> file :data :pages first)) + page (get-in file [:data :pages-index page-id])] + + (cond-> page + (:prune-frames-with-thumbnails props) + (prune-frames-with-thumbnails) + + (:prune-thumbnails props) + (prune-thumbnails) + + (uuid? object-id) + (trim-objects object-id)))) + +(s/def ::file-data-for-thumbnail + (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. Returns the page data." + [{: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)] + {:page (get-thumbnail-data file) + :file-id file-id + :revn (:revn file)})) ;; --- Query: Shared Library Files diff --git a/common/deps.edn b/common/deps.edn index ebc7812dc9..22f7ebbc07 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -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]} diff --git a/common/src/app/common/data/macros.cljc b/common/src/app/common/data/macros.cljc index 857892f1d1..af628cb0bc 100644 --- a/common/src/app/common/data/macros.cljc +++ b/common/src/app/common/data/macros.cljc @@ -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)) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index d0ce43f5cf..3a5fc364fd 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -9,7 +9,6 @@ [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] diff --git a/common/src/app/common/pprint.cljc b/common/src/app/common/pprint.cljc new file mode 100644 index 0000000000..e95ad84f6f --- /dev/null +++ b/common/src/app/common/pprint.cljc @@ -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))) + + diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 0f8f898ac3..588b127498 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -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 diff --git a/exporter/src/app/handlers.cljs b/exporter/src/app/handlers.cljs index ce4870eccb..0d0cab3a31 100644 --- a/exporter/src/app/handlers.cljs +++ b/exporter/src/app/handlers.cljs @@ -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"))))) diff --git a/exporter/src/app/handlers/export_frames.cljs b/exporter/src/app/handlers/export_frames.cljs index 9b1b6d0b24..74ac5ab832 100644 --- a/exporter/src/app/handlers/export_frames.cljs +++ b/exporter/src/app/handlers/export_frames.cljs @@ -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)) diff --git a/exporter/src/app/handlers/export_shapes.cljs b/exporter/src/app/handlers/export_shapes.cljs index d35a460a7f..b70e63cf21 100644 --- a/exporter/src/app/handlers/export_shapes.cljs +++ b/exporter/src/app/handlers/export_shapes.cljs @@ -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)))))) diff --git a/exporter/src/app/handlers/resources.cljs b/exporter/src/app/handlers/resources.cljs index 6fa3ddb1a9..e027056101 100644 --- a/exporter/src/app/handlers/resources.cljs +++ b/exporter/src/app/handlers/resources.cljs @@ -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)) diff --git a/exporter/src/app/renderer.cljs b/exporter/src/app/renderer.cljs new file mode 100644 index 0000000000..90e03ec68d --- /dev/null +++ b/exporter/src/app/renderer.cljs @@ -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))) + diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index 67f4b965fd..96dff8bea7 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -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))))) diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index 3131ce22ea..be57229f91 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -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))))) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 4a965ad4da..9a0b681b02 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -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,74 @@ (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) + result (str/replace result " " " ")] - uri (-> (or uri (cf/get :public-uri)) - (assoc :path "/render.html") - (assoc :query (u/map->query-string params)))] + ;; (println "------- ORIGIN:") + ;; (cljs.pprint/pprint (xml->clj xmldata)) + ;; (println "------- RESULT:") + ;; (cljs.pprint/pprint (xml->clj result)) + ;; (println "-------") - (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)) + (sh/write-file! path result) + (on-object (assoc object :path path)) + path))) - (extract page))))))) + (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 ::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) + ;; take the screnshot of requested objects, one by one + (p/run! (partial render-object page) objects) + nil))] -(s/def ::params - (s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::file-id ::scale ::token] - :opt-un [::uri])) - -(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))))) diff --git a/exporter/src/app/util/mime.cljs b/exporter/src/app/util/mime.cljs new file mode 100644 index 0000000000..ed5a19a4c7 --- /dev/null +++ b/exporter/src/app/util/mime.cljs @@ -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")) + + diff --git a/exporter/src/app/util/shell.cljs b/exporter/src/app/util/shell.cljs index ee9f5d1c55..93b5333ed9 100644 --- a/exporter/src/app/util/shell.cljs +++ b/exporter/src/app/util/shell.cljs @@ -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] diff --git a/frontend/deps.edn b/frontend/deps.edn index e3e6000c88..b3196cfa49 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -10,10 +10,12 @@ funcool/beicon {:mvn/version "2021.07.05-1"} funcool/okulary {:mvn/version "2020.04.14-0"} funcool/potok {:mvn/version "2021.09.20-0"} - funcool/rumext {:mvn/version "2022.01.20.128"} + funcool/rumext {:mvn/version "2022.03.28-131"} funcool/tubax {:mvn/version "2021.05.20-0"} instaparse/instaparse {:mvn/version "1.4.10"} + garden/garden {:mvn/version "1.3.10"} + } :aliases diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs index 44946090f2..46dd9ca1f5 100644 --- a/frontend/src/app/main/data/exports.cljs +++ b/frontend/src/app/main/data/exports.cljs @@ -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,7 +134,7 @@ 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?) @@ -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! :export-shapes 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))))))) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index d94e206b7d..549813a408 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -17,7 +17,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])) @@ -113,13 +112,12 @@ (ts/schedule (st/emitf (msg/show {:content "Internal error: assertion." - :type :error - :timeout 3000}))) + :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 diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 34eebbabc7..1136002d63 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -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)))))))) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 2e3a1a9701..6ed957f117 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -105,34 +105,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) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 151e14b211..fd65d7569f 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -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) diff --git a/frontend/src/app/main/ui/export.cljs b/frontend/src/app/main/ui/export.cljs index 3b273c3995..da0e639d07 100644 --- a/frontend/src/app/main/ui/export.cljs +++ b/frontend/src/app/main/ui/export.cljs @@ -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]} diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs deleted file mode 100644 index e731d45559..0000000000 --- a/frontend/src/app/main/ui/render.cljs +++ /dev/null @@ -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)]]))])]))) - diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index efc66c02cc..a456f7bd46 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -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" diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs index 00691e236c..0ae24a525e 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs @@ -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")])]))) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index c7390b6a79..89f4c2c051 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -390,34 +390,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}]])) - diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs index 27bb3fd6a1..e0d58295a5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs @@ -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 diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index b40802fb93..38b61f5446 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -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,225 @@ [] (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 + :prune-thumbnails true})) + (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 + :prune-thumbnails true})) + (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}]))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 64ba990dc3..695cd2ed15 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -403,16 +403,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 +464,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") diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index 1718cbbdbe..1e47d68bcb 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -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 diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index feeab2ee03..cd45b67819 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -48,7 +48,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")) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index ddd657643f..30455109cd 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -56,15 +56,16 @@ :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}))] + [{:keys [page file-id revn] :as params}] + (let [elem (if-let [frame (:thumbnail-frame page)] + (mf/element render/frame-svg #js {:objects (:objects page) :frame frame}) + (mf/element render/page-svg #js {:data page :thumbnails? true}))] {:data (rds/renderToStaticMarkup elem) :fonts @fonts/loaded :file-id file-id @@ -81,6 +82,7 @@ :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) From 147f56749e5240c50a4541011dc2815e3c04bbef Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 29 Mar 2022 14:06:03 +0200 Subject: [PATCH 09/32] :arrow_up: Update some dependencies --- common/deps.edn | 2 +- common/package.json | 2 +- docker/devenv/Dockerfile | 23 ++--- docker/devenv/files/bashrc | 2 +- docker/devenv/files/entrypoint.sh | 2 +- frontend/deps.edn | 6 +- frontend/package.json | 18 ++-- frontend/yarn.lock | 136 ++++++++++++++++-------------- 8 files changed, 98 insertions(+), 93 deletions(-) diff --git a/common/deps.edn b/common/deps.edn index 22f7ebbc07..019017255f 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -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"}} diff --git a/common/package.json b/common/package.json index a4143c62ec..990c172add 100644 --- a/common/package.json +++ b/common/package.json @@ -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" } diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 5bd80543fb..2a25753555 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -4,9 +4,9 @@ LABEL maintainer="Andrey Antukh " 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 \ + CLOJURE_VERSION=1.11.0.1100 \ + CLJKONDO_VERSION=2022.03.09 \ + BABASHKA_VERSION=0.7.8 \ 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; \ diff --git a/docker/devenv/files/bashrc b/docker/devenv/files/bashrc index 3ae8341c91..420ebe2132 100644 --- a/docker/devenv/files/bashrc +++ b/docker/devenv/files/bashrc @@ -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' diff --git a/docker/devenv/files/entrypoint.sh b/docker/devenv/files/entrypoint.sh index d05b118c63..69d372b357 100755 --- a/docker/devenv/files/entrypoint.sh +++ b/docker/devenv/files/entrypoint.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 set -e usermod -u ${EXTERNAL_UID:-1000} penpot diff --git a/frontend/deps.edn b/frontend/deps.edn index b3196cfa49..bffc02ba1e 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -5,7 +5,7 @@ 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"} @@ -32,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"]} diff --git a/frontend/package.json b/frontend/package.json index 5edc48cfdf..104be46654 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8b86ab0ca6..fee26840af 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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" From 20d3251a9371ca6c33713c28fa48cec1fae84499 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 30 Mar 2022 00:11:43 +0200 Subject: [PATCH 10/32] :tada: Add generic file object thumbnail abstraction As replacement to the file frame thumbnail mechanism --- backend/src/app/migrations.clj | 6 + .../sql/0070-del-frame-thumbnail-table.sql | 1 + .../0071-add-file-object-thumbnail-table.sql | 11 + backend/src/app/rpc/mutations/files.clj | 27 +- backend/src/app/rpc/queries/files.clj | 203 +++++++------- backend/src/app/tasks/file_gc.clj | 41 +-- backend/test/app/services_files_test.clj | 254 ++++++++++++++---- backend/test/app/test_helpers.clj | 3 +- common/src/app/common/data.cljc | 3 +- frontend/src/app/main/data/workspace.cljs | 27 +- .../src/app/main/data/workspace/changes.cljs | 7 +- frontend/src/app/main/ui/viewer/shapes.cljs | 2 - .../app/main/ui/workspace/context_menu.cljs | 10 +- frontend/src/app/render.cljs | 6 +- frontend/src/app/worker/thumbnails.cljs | 10 +- 15 files changed, 399 insertions(+), 212 deletions(-) create mode 100644 backend/src/app/migrations/sql/0070-del-frame-thumbnail-table.sql create mode 100644 backend/src/app/migrations/sql/0071-add-file-object-thumbnail-table.sql diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 7f552a5325..d81001a1b4 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -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")} ]) diff --git a/backend/src/app/migrations/sql/0070-del-frame-thumbnail-table.sql b/backend/src/app/migrations/sql/0070-del-frame-thumbnail-table.sql new file mode 100644 index 0000000000..2c4d482c55 --- /dev/null +++ b/backend/src/app/migrations/sql/0070-del-frame-thumbnail-table.sql @@ -0,0 +1 @@ +DROP TABLE file_frame_thumbnail; diff --git a/backend/src/app/migrations/sql/0071-add-file-object-thumbnail-table.sql b/backend/src/app/migrations/sql/0071-add-file-object-thumbnail-table.sql new file mode 100644 index 0000000000..aeb129af91 --- /dev/null +++ b/backend/src/app/migrations/sql/0071-add-file-object-thumbnail-table.sql @@ -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; diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index c3014bfc80..a7bc612381 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -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) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 9794aa6a99..3ce4846bec 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -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,12 +221,16 @@ [{: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))))) -;; --- FILE THUMBNAIL -(defn- trim-objects +;; --- 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." @@ -219,64 +242,19 @@ "Given the page data, removes the `:thumbnail` prop from all shapes." [page] - (update page :objects (fn [objects] - (d/mapm #(dissoc %2 :thumbnail) objects)))) - -(defn- prune-frames-with-thumbnails - "Remove unnecesary shapes from frames that have thumbnail from page - data." - [page] - (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 page :objects update-objects))) - -(defn- get-thumbnail-data - [{:keys [data] :as file}] - (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))] - (-> (assoc page :objects objects) - (assoc :thumbnail-frame frame))) - - (let [page-id (-> data :pages first)] - (-> (get-in data [:pages-index page-id]) - (prune-frames-with-thumbnails))))) + (update page :objects d/update-vals #(dissoc % :thumbnail))) (s/def ::page-id ::us/uuid) (s/def ::object-id ::us/uuid) -(s/def ::prune-frames-with-thumbnails ::us/boolean) -(s/def ::prune-thumbnails ::us/boolean) (s/def ::page - (s/keys :req-un [::profile-id ::file-id] - :opt-un [::page-id - ::object-id - ::prune-frames-with-thumbnails - ::prune-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 page data from file and returns it. If no page-id is @@ -284,6 +262,9 @@ specified, only that object and its children will be returned in the page objects data structure. + 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) @@ -291,28 +272,84 @@ page-id (or page-id (-> file :data :pages first)) page (get-in file [:data :pages-index page-id])] - (cond-> page - (:prune-frames-with-thumbnails props) - (prune-frames-with-thumbnails) - - (:prune-thumbnails props) - (prune-thumbnails) - + (cond-> (prune-thumbnails page) (uuid? object-id) - (trim-objects 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])) (sv/defmethod ::file-data-for-thumbnail - "Retrieves the data for generate the thumbnail of the file. Used mainly for render - thumbnails on dashboard. Returns the page data." + "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)] - {:page (get-thumbnail-data file) - :file-id file-id - :revn (:revn file)})) + {:file-id file-id + :revn (:revn file) + :page (get-file-thumbnail-data cfg file)})) + ;; --- Query: Shared Library Files @@ -412,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) diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj index b8669d96b2..4591e56e8c 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -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] diff --git a/backend/test/app/services_files_test.clj b/backend/test/app/services_files_test.clj index 3977a1317f..b30e2a53bb 100644 --- a/backend/test/app/services_files_test.clj +++ b/backend/test/app/services_files_test.clj @@ -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 diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj index 5699424a60..f94e60701e 100644 --- a/backend/test/app/test_helpers.clj +++ b/backend/test/app/test_helpers.clj @@ -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? diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 872539945b..a87466d819 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -101,7 +101,6 @@ (defn preconj [coll elem] - (assert (or (vector? coll) (nil? coll))) (into [elem] coll)) (defn enumerate @@ -176,7 +175,7 @@ [data keys] (when (map? data) (persistent! - (reduce #(dissoc! %1 %2) (transient data) keys)))) + (reduce dissoc! (transient data) keys)))) (defn remove-at-index "Takes a vector and returns a vector with an element in the diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 4f0e74ecb4..bbed408dd7 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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 diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index 0336f2bab2..1b298f2852 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -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) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 89f4c2c051..c216224097 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -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] diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 28895ba49a..697a6b369e 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -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") diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index 38b61f5446..bd89007bff 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -106,8 +106,7 @@ (repo/query! :font-variants {:file-id file-id}) (repo/query! :page {:file-id file-id :page-id page-id - :object-id object-id - :prune-thumbnails true})) + :object-id object-id})) (rx/tap (fn [[fonts]] (when (seq fonts) (st/emit! (df/fonts-fetched fonts))))) @@ -146,8 +145,7 @@ (->> (rx/zip (repo/query! :font-variants {:file-id file-id}) (repo/query! :page {:file-id file-id - :page-id page-id - :prune-thumbnails true})) + :page-id page-id})) (rx/tap (fn [[fonts]] (when (seq fonts) (st/emit! (df/fonts-fetched fonts))))) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index 30455109cd..e5e9d77de9 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -63,10 +63,12 @@ (defn- render-thumbnail [{:keys [page file-id revn] :as params}] - (let [elem (if-let [frame (:thumbnail-frame page)] - (mf/element render/frame-svg #js {:objects (:objects page) :frame frame}) - (mf/element render/page-svg #js {:data page :thumbnails? true}))] - {:data (rds/renderToStaticMarkup elem) + (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})) From f9e83f2cc771daad29649409c08615c377d1f0a2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 31 Mar 2022 00:26:13 +0200 Subject: [PATCH 11/32] :sparkles: Improve implementation of without-keys helper --- common/src/app/common/data.cljc | 36 ++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index a87466d819..a704b95222 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -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] @@ -173,9 +178,12 @@ "Return a map without the keys provided in the `keys` parameter." [data keys] - (when (map? data) - (persistent! - (reduce dissoc! (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 @@ -208,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)) @@ -343,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] @@ -407,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" From 7d32d0315620df01cd5c1752a38e252ee66c9ae5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 31 Mar 2022 00:28:09 +0200 Subject: [PATCH 12/32] :lipstick: Add cosmetic changes on workspace/changes ns --- frontend/src/app/main/data/workspace/changes.cljs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index 1b298f2852..2738d6d4fd 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -48,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 From 8eae8929835314eda7418d851bc18d0ab5d90f85 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 31 Mar 2022 00:28:34 +0200 Subject: [PATCH 13/32] :fire: Remove old and already deprecated utils.data ns --- .../src/app/main/ui/workspace/libraries.cljs | 2 +- .../app/main/ui/workspace/sidebar/assets.cljs | 2 +- .../sidebar/options/shapes/svg_raw.cljs | 4 +- frontend/src/app/util/data.cljs | 169 ------------------ frontend/src/app/util/strings.cljs | 8 +- 5 files changed, 11 insertions(+), 174 deletions(-) delete mode 100644 frontend/src/app/util/data.cljs diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index cee01fae22..c9742069f2 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -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])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 9f8032f392..5d9023544f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -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] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs index fa209083eb..c21726de83 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs @@ -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 diff --git a/frontend/src/app/util/data.cljs b/frontend/src/app/util/data.cljs deleted file mode 100644 index 0c0d500afd..0000000000 --- a/frontend/src/app/util/data.cljs +++ /dev/null @@ -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)))) diff --git a/frontend/src/app/util/strings.cljs b/frontend/src/app/util/strings.cljs index e51bbe86df..c1d6ad2b4c 100644 --- a/frontend/src/app/util/strings.cljs +++ b/frontend/src/app/util/strings.cljs @@ -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)))) From c5b1b67c50d9480852271aa83b00574b4b5402b4 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 31 Mar 2022 15:12:55 +0200 Subject: [PATCH 14/32] :paperclip: Add TODO comment on changes ns --- common/src/app/common/pages/changes.cljc | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 242f5b9e3e..8ca215bde8 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -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))) From 602b7361639c0888afb33580c5c92688b40b5a90 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 1 Apr 2022 13:23:06 +0200 Subject: [PATCH 15/32] :paperclip: Update default scripts --- backend/scripts/repl | 12 ++++++++---- backend/src/app/config.clj | 7 ++++--- backend/src/app/http/middleware.clj | 5 ++++- backend/src/app/http/websocket.clj | 2 -- docker/images/config.env | 1 + 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/backend/scripts/repl b/backend/scripts/repl index 86eaccb801..73f4d97041 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -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 diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 26f65c6ece..6acb96cf49 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -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 diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index 125d75e5e0..bdc997c051 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -99,7 +99,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)))) diff --git a/backend/src/app/http/websocket.clj b/backend/src/app/http/websocket.clj index 4b896e22cb..f39a898911 100644 --- a/backend/src/app/http/websocket.clj +++ b/backend/src/app/http/websocket.clj @@ -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) diff --git a/docker/images/config.env b/docker/images/config.env index 484b900e43..e77b85d686 100644 --- a/docker/images/config.env +++ b/docker/images/config.env @@ -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 From c026d05bc3ebf48adce41f5eaa07e252e6f3ebbe Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 1 Apr 2022 13:33:56 +0200 Subject: [PATCH 16/32] :sparkles: Set consistent max body size And make it configurable --- backend/src/app/http.clj | 2 +- backend/src/app/main.clj | 4 +++- docker/devenv/files/nginx.conf | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 38c9340d09..282c286a52 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -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))) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 455964fdb7..df77de7b85 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -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) diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 2d62807818..ad9b488ea0 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -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; From 701a98fab61b38c2d4430dd5781481404b5993a6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 1 Apr 2022 15:55:28 +0200 Subject: [PATCH 17/32] :sparkles: Improve backend and worker error handling --- backend/src/app/http/errors.clj | 20 ++++++--- backend/src/app/http/middleware.clj | 30 ++++++++++--- backend/src/app/main.clj | 6 +-- frontend/src/app/main/errors.cljs | 59 +++++++++++++++++-------- frontend/src/app/main/repo.cljs | 4 ++ frontend/src/app/worker.cljs | 8 ++-- frontend/src/app/worker/thumbnails.cljs | 32 ++++++++++---- 7 files changed, 112 insertions(+), 47 deletions(-) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index bd40ded1d9..fee8f23368 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -49,12 +49,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] diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index bdc997c051..af7f140a86 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -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 diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index df77de7b85..9fe00155ed 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -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] diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 549813a408..163f703081 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -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,7 +19,6 @@ [app.util.i18n :refer [tr]] [app.util.router :as rt] [app.util.timers :as ts] - [fipp.edn :as fpp] [potok.core :as ptk])) (defn on-error @@ -59,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:") @@ -78,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 @@ -110,10 +132,9 @@ (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) @@ -139,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 diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 6ed957f117..2e3ffb9faa 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -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) diff --git a/frontend/src/app/worker.cljs b/frontend/src/app/worker.cljs index a37bce9589..5889a8cd97 100644 --- a/frontend/src/app/worker.cljs +++ b/frontend/src/app/worker.cljs @@ -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)}}))) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index e5e9d77de9..66ac64dfb1 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -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] @@ -88,6 +101,7 @@ (->> (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 From ca02999ae9a5280efdada27da846e68f659fe886 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 1 Apr 2022 17:16:59 +0200 Subject: [PATCH 18/32] :sparkles: Improve error reporting --- backend/src/app/http/errors.clj | 25 +++++++++++++++++++----- backend/src/app/rpc.clj | 8 ++++++-- common/src/app/common/exceptions.cljc | 28 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index fee8f23368..f7533306ec 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -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) @@ -137,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))) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 3cdef422c3..9e53af92eb 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -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." diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index b3b73e5a9f..d76f943e54 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -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)) From df4b92fb6b3eb83a27631439fe731a63703e6752 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Apr 2022 09:26:05 +0200 Subject: [PATCH 19/32] :sparkles: Improve logging ordering of message parts --- common/src/app/common/logging.cljc | 102 +++++++++++++++-------------- frontend/src/debug.cljs | 7 ++ 2 files changed, 61 insertions(+), 48 deletions(-) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index 3a5fc364fd..7e2970cb31 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -6,13 +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] [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 @@ -21,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))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -30,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 @@ -44,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] @@ -131,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? @@ -166,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")] @@ -183,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))))))) @@ -283,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] @@ -303,7 +309,7 @@ :else (recur (next kvpairs) - (assoc message k v) + (conj message (str/concat (d/name k) "=" (pr-str v))) specials))))))) #?(:cljs @@ -319,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 @@ -328,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 diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index e7b48bbf0e..23cb8670fd 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -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] @@ -25,6 +26,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 From 7a38b08506b1d09adbca38b7f92446b8638d4a41 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Apr 2022 10:52:50 +0200 Subject: [PATCH 20/32] :bug: Fix default configuration --- backend/scripts/repl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/scripts/repl b/backend/scripts/repl index 73f4d97041..2f221f129a 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -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" From 4adc5d25a738128c5fed88e967ba1be11a2dba57 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Apr 2022 13:07:11 +0200 Subject: [PATCH 21/32] :paperclip: Fix review issues --- exporter/src/app/renderer/svg.cljs | 4 ++++ frontend/src/app/util/dom.cljs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 9a0b681b02..73d5187798 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -330,6 +330,10 @@ (p/let [xmldata (extract-svg page object) txtdata (extract-txt-nodes page object) result (replace-text-nodes xmldata txtdata) + + ;; SVG standard don't allow the entity + ;; nbsp.   is equivalent but compatible + ;; with SVG. result (str/replace result " " " ")] ;; (println "------- ORIGIN:") diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 695cd2ed15..a6b92c6d4d 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -467,7 +467,7 @@ (let [link (create-element "a") extension (mtype->extension mtype) filename (if (and extension (not (str/ends-with? filename extension))) - (str/concat filename "." extension) + (str/concat filename extension) filename)] (obj/set! link "href" uri) (obj/set! link "download" filename) From 39fa939f58396a91f70ce09ebd32570c09064abd Mon Sep 17 00:00:00 2001 From: Eva Date: Tue, 5 Apr 2022 13:51:14 +0200 Subject: [PATCH 22/32] :bug: Fix gap between contiguous shapes --- CHANGES.md | 1 + frontend/src/app/main/ui/workspace/viewport.cljs | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 21acf48903..0765e53d56 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ ### :bug: Bugs fixed +- 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) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 900dd304b6..4d70a247a4 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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) From 52994658649aa9df1514233119178e4b344a5f1f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 6 Apr 2022 08:28:57 +0200 Subject: [PATCH 23/32] :bug: Setting in-progress to false when export fails --- frontend/src/app/main/data/exports.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs index 46dd9ca1f5..75712fe663 100644 --- a/frontend/src/app/main/data/exports.cljs +++ b/frontend/src/app/main/data/exports.cljs @@ -137,7 +137,7 @@ (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?)))) From 654e12a2c31e0cb1fc2c45d54860f24014ad75dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Wed, 6 Apr 2022 09:16:22 +0200 Subject: [PATCH 24/32] :bug: Fix multi user not working --- CHANGES.md | 1 + frontend/src/app/main/data/workspace/notifications.cljs | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0765e53d56..c8a9c4a846 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -83,6 +83,7 @@ - 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!) diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index e457b6a3ad..40f6f8ae1a 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -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 From 5edbebcfec4855674cd83e5a08ae095527efc189 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 6 Apr 2022 09:37:12 +0200 Subject: [PATCH 25/32] :bug: 'Show in exports' is showing in multiselections --- CHANGES.md | 1 + .../src/app/main/ui/workspace/sidebar/options/menus/fill.cljs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c8a9c4a846..7e14b2bc3c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ ### :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) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 4156c2c56c..8741f3dce7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -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" From fd9b4420751a3ed31aaa3b6c8fafaa734a61f321 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Apr 2022 09:40:20 +0200 Subject: [PATCH 26/32] :sparkles: Improve email console logging And invitation console logging --- backend/src/app/emails.clj | 29 +++++++++++++++---------- backend/src/app/rpc/mutations/teams.clj | 4 ++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/backend/src/app/emails.clj b/backend/src/app/emails.clj index 6721c299ca..1a5aebb638 100644 --- a/backend/src/app/emails.clj +++ b/backend/src/app/emails.clj @@ -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)))) + (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))) diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index ce5626d165..586a9ad602 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -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 From 2af057a79fcc5fe2e15059f6961513d1883ea1ce Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Apr 2022 09:47:54 +0200 Subject: [PATCH 27/32] :arrow_up: Update backend and docker dependencies --- backend/deps.edn | 2 +- backend/src/app/emails.clj | 2 +- docker/devenv/Dockerfile | 4 ++-- docker/images/config.env | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/deps.edn b/backend/deps.edn index 0911ba205f..4b7b62eb0a 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -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]} diff --git a/backend/src/app/emails.clj b/backend/src/app/emails.clj index 1a5aebb638..23bc103100 100644 --- a/backend/src/app/emails.clj +++ b/backend/src/app/emails.clj @@ -173,7 +173,7 @@ (send-console! cfg props))))) (defn- send-console! - [cfg email] + [_ email] (let [body (:body email) out (with-out-str (println "email console dump:") diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 2a25753555..98ae0514fa 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive -ENV NODE_VERSION=v16.14.0 \ +ENV NODE_VERSION=v16.14.2 \ CLOJURE_VERSION=1.11.0.1100 \ CLJKONDO_VERSION=2022.03.09 \ - BABASHKA_VERSION=0.7.8 \ + BABASHKA_VERSION=0.8.0 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 diff --git a/docker/images/config.env b/docker/images/config.env index e77b85d686..0174efe90a 100644 --- a/docker/images/config.env +++ b/docker/images/config.env @@ -17,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 From 0bb20197f162df2ecdb121d4397eeae7d99d65cd Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 1 Apr 2022 11:21:50 +0200 Subject: [PATCH 28/32] :zap: Improved performance of refs --- frontend/deps.edn | 4 +- .../main/data/workspace/state_helpers.cljs | 38 ++++---- frontend/src/app/main/refs.cljs | 18 +++- .../src/app/main/ui/workspace/shapes.cljs | 7 +- .../ui/workspace/shapes/bounding_box.cljs | 90 ------------------- frontend/src/app/util/dom.cljs | 27 ++++-- frontend/src/debug.cljs | 7 ++ 7 files changed, 67 insertions(+), 124 deletions(-) delete mode 100644 frontend/src/app/main/ui/workspace/shapes/bounding_box.cljs diff --git a/frontend/deps.edn b/frontend/deps.edn index bffc02ba1e..7e87cafce2 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -8,9 +8,9 @@ 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.03.28-131"} + funcool/rumext {:mvn/version "2022.03.31-133"} funcool/tubax {:mvn/version "2021.05.20-0"} instaparse/instaparse {:mvn/version "1.4.10"} diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs index 708c3d1d3f..4da32f002b 100644 --- a/frontend/src/app/main/data/workspace/state_helpers.cljs +++ b/frontend/src/app/main/data/workspace/state_helpers.cljs @@ -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." diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 14a4a92592..accbc9e133 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -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] diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index d45dd91490..adff4aa4ce 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/workspace/shapes/bounding_box.cljs b/frontend/src/app/main/ui/workspace/shapes/bounding_box.cljs deleted file mode 100644 index 7e05a78811..0000000000 --- a/frontend/src/app/main/ui/workspace/shapes/bounding_box.cljs +++ /dev/null @@ -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}]]])) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index a6b92c6d4d..1c1b4854ff 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -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] @@ -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)))) diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index 23cb8670fd..1cbd600bf3 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -18,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] @@ -340,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})) From 76abd6796e57655c76c9154070d55cf0ee935254 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 1 Apr 2022 11:26:47 +0200 Subject: [PATCH 29/32] :bug: Fix import problems --- frontend/src/app/util/import/parser.cljs | 6 ++-- frontend/src/app/util/path/parser.cljs | 23 +++++++------- frontend/src/app/worker/import.cljs | 40 ++++++++++++++---------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index b00485efab..28767b265c 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -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)) diff --git a/frontend/src/app/util/path/parser.cljs b/frontend/src/app/util/path/parser.cljs index 2057ae05fc..172089caa8 100644 --- a/frontend/src/app/util/path/parser.cljs +++ b/frontend/src/app/util/path/parser.cljs @@ -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))))) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index cd45b67819..634cc5c7e6 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -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] @@ -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] From ea38d12a73710f27a8de3445e014ae3f9783094d Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 1 Apr 2022 21:26:58 +0200 Subject: [PATCH 30/32] :bug: Fix problem with exported text --- frontend/src/app/main/ui/shapes/text/svg_text.cljs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs index b2747ab37a..f9849c9d0e 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -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) From b2607b28ffcd973f7aeb54469ba4dbaf7dbfa07f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Apr 2022 11:14:18 +0200 Subject: [PATCH 31/32] :tada: Add build date and changelog to the bundle --- backend/deps.edn | 3 +++ backend/scripts/build | 1 + backend/src/app/http.clj | 1 + backend/src/app/http/debug.clj | 18 +++++++++++++++++- frontend/resources/templates/index.mustache | 1 + frontend/scripts/build | 3 ++- frontend/src/app/config.cljs | 8 ++++++++ frontend/src/app/main.cljs | 5 ++++- 8 files changed, 37 insertions(+), 3 deletions(-) diff --git a/backend/deps.edn b/backend/deps.edn index 4b7b62eb0a..34e62ccb9d 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -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"}} diff --git a/backend/scripts/build b/backend/scripts/build index d2b7b5966a..1537b7f91e 100755 --- a/backend/scripts/build +++ b/backend/scripts/build @@ -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 diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 282c286a52..95631738dd 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -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)}] diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 2a90ccf7f8..f49c4347d3 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -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)}) diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index 0b0dfd2790..bf6ab809d6 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -23,6 +23,7 @@ window.penpotTranslations = JSON.parse({{& translations}}); window.penpotThemes = {{& themes}}; window.penpotVersion = "%version%"; + window.penpotBuildDate = "%buildDate%"; {{# manifest}} diff --git a/frontend/scripts/build b/frontend/scripts/build index c7a45d1b4a..434a261e81 100755 --- a/frontend/scripts/build +++ b/frontend/scripts/build @@ -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; diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 96cd3baaf1..7cf0ede1a1 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -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))) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index bddddc9bf3..287e3fe039 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -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) From a61301c698570c2522c982e5bafc1cfb5aee2ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Wed, 6 Apr 2022 12:53:41 +0200 Subject: [PATCH 32/32] :bug: Fix call to exporter and exporter setup in devenv --- docker/devenv/files/start-tmux.sh | 1 + frontend/src/app/main/data/exports.cljs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/devenv/files/start-tmux.sh b/docker/devenv/files/start-tmux.sh index 2891f18bc9..cb3048ddc5 100755 --- a/docker/devenv/files/start-tmux.sh +++ b/docker/devenv/files/start-tmux.sh @@ -14,6 +14,7 @@ yarn install popd pushd ~/penpot/exporter/ yarn install +npx playwright install chromium popd tmux -2 new-session -d -s penpot diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs index 75712fe663..768108d8e6 100644 --- a/frontend/src/app/main/data/exports.cljs +++ b/frontend/src/app/main/data/exports.cljs @@ -165,7 +165,7 @@ :wait true}] (rx/concat (rx/of ::dwp/force-persist) - (->> (rp/query! :export-shapes params) + (->> (rp/query! :exporter params) (rx/mapcat (fn [{:keys [id filename]}] (->> (rp/query! :exporter {:cmd :get-resource :blob? true :id id}) (rx/map (fn [data]