From d70fc3368963353d02fcadc103ad201581dac8c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 30 Jan 2024 15:35:59 +0100 Subject: [PATCH 01/37] :sparkles: Show loading message in Libraries modal --- .../src/app/main/ui/workspace/libraries.cljs | 27 +++++++++++-------- frontend/translations/en.po | 4 +++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 8d6b7dcc93..6232f7c231 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -123,11 +123,12 @@ shared-libraries (mf/with-memo [shared-libraries linked-libraries file-id search-term] - (->> shared-libraries - (remove #(= (:id %) file-id)) - (remove #(contains? linked-libraries (:id %))) - (filter #(matches-search (:name %) search-term)) - (sort-by (comp str/lower :name)))) + (when shared-libraries + (->> shared-libraries + (remove #(= (:id %) file-id)) + (remove #(contains? linked-libraries (:id %))) + (filter #(matches-search (:name %) search-term)) + (sort-by (comp str/lower :name))))) linked-libraries (mf/with-memo [linked-libraries] @@ -275,12 +276,17 @@ :on-click link-library} i/add-refactor]])] - [:div {:class (stl/css :section-list-empty)} - (if (nil? shared-libraries) - i/loader-pencil - (if (str/empty? search-term) + (when (empty? shared-libraries) + [:div {:class (stl/css :section-list-empty)} + (cond + (nil? shared-libraries) + (tr "workspace.libraries.loading") + + (str/empty? search-term) (tr "workspace.libraries.no-shared-libraries-available") - (tr "workspace.libraries.no-matches-for" search-term)))])]])) + + :else + (tr "workspace.libraries.no-matches-for" search-term))]))]])) (defn- extract-assets [file-data library summary?] @@ -519,4 +525,3 @@ [:& updates-tab {:file-id file-id :file-data file-data :libraries libraries}]]]]]]]])) - diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 0affa485fa..69cad92bf2 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3593,6 +3593,10 @@ msgstr "Search shared libraries" msgid "workspace.libraries.shared-libraries" msgstr "SHARED LIBRARIES" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.loading" +msgstr "Loading…" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography" msgstr "Multiple typographies" From 2661d6c122731d5d23763ea5647faa7159d06a1d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 31 Jan 2024 12:25:46 +0100 Subject: [PATCH 02/37] :bug: Fix team photo handling on binfile/v2 export-import operation --- backend/src/app/binfile/v2.clj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/app/binfile/v2.clj b/backend/src/app/binfile/v2.clj index 8ec2e19210..1a5f103425 100644 --- a/backend/src/app/binfile/v2.clj +++ b/backend/src/app/binfile/v2.clj @@ -129,6 +129,9 @@ :id (str team-id) :fonts (count fonts)) + (when-let [photo-id (:photo-id team)] + (vswap! bfc/*state* update :storage-objects conj photo-id)) + (vswap! bfc/*state* update :teams conj team-id) (vswap! bfc/*state* bfc/collect-storage-objects fonts) From 41d6261ef377d980280a8b7278e2d2714f6844e2 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 25 Jan 2024 09:51:46 +0100 Subject: [PATCH 03/37] :bug: Fix duplicate component --- frontend/src/app/main/data/workspace/libraries_helpers.cljs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index a60301109f..322c4c637e 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -159,7 +159,9 @@ component (:data library) position - components-v2) + components-v2 + ;; The position can generate a frame calculation inside the base component so we force the frame-id + {:force-frame-id frame-id}) first-shape (cond-> (first new-shapes) (not (nil? parent-id)) From f7ad3e37a4c0891463b77b224c649d545257b756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 31 Jan 2024 15:31:23 +0100 Subject: [PATCH 04/37] :bug: Fix selected text not being visible --- frontend/resources/styles/common/refactor/basic-rules.scss | 3 ++- frontend/resources/styles/common/refactor/color-defs.scss | 2 ++ .../resources/styles/common/refactor/design-tokens.scss | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index 638867c89e..f5164e4cd0 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -29,7 +29,8 @@ } ::selection { - background-color: var(--color-accent-primary-muted); + background: var(--text-editor-selection-background-color); + color: var(--text-editor-selection-foreground-color); } } diff --git a/frontend/resources/styles/common/refactor/color-defs.scss b/frontend/resources/styles/common/refactor/color-defs.scss index 24b66209b9..b2e0cc7ab4 100644 --- a/frontend/resources/styles/common/refactor/color-defs.scss +++ b/frontend/resources/styles/common/refactor/color-defs.scss @@ -27,6 +27,7 @@ --da-secondary: #bb97d8; --da-tertiary: #00d1b8; --da-tertiary-10: #{color.change(#00d1b8, $alpha: 0.1)}; + --da-tertiary-70: #{color.change(#00d1b8, $alpha: 0.7)}; --da-quaternary: #ff6fe0; // LIGHT @@ -50,6 +51,7 @@ --la-secondary: #1345aa; --la-tertiary: #8c33eb; --la-tertiary-10: #{color.change(#8c33eb, $alpha: 0.1)}; + --la-tertiary-70: #{color.change(#8c33eb, $alpha: 0.7)}; --la-quaternary: #ff6fe0; // STATUS COLOR diff --git a/frontend/resources/styles/common/refactor/design-tokens.scss b/frontend/resources/styles/common/refactor/design-tokens.scss index 0e80da0588..45c677671a 100644 --- a/frontend/resources/styles/common/refactor/design-tokens.scss +++ b/frontend/resources/styles/common/refactor/design-tokens.scss @@ -357,6 +357,10 @@ --viewer-thumbnails-control-foreground-color: var(--color-foreground-secondary); --viewer-thumbnail-border-color: var(--color-accent-primary); --viewer-thumbnail-background-color-selected: var(--color-accent-primary-muted); + + // TEXT SELECTION + --text-editor-selection-background-color: var(--da-tertiary-70); + --text-editor-selection-foreground-color: var(--app-white); } #app { @@ -383,4 +387,6 @@ --assets-item-name-background-color: var(--color-background-primary); --assets-item-name-foreground-color: var(--color-foreground-primary); + + --text-editor-selection-background-color: var(--la-tertiary-70); } From 051859969c3e310a567daab9941b85db822faca1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 29 Jan 2024 16:43:01 +0100 Subject: [PATCH 05/37] :bug: Fix problem when creating frames contining paths --- frontend/src/app/main/data/workspace/drawing/common.cljs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index 4865d0c4c0..fffc73b009 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -81,7 +81,8 @@ :page-id page-id :rect (:selrect shape) :include-frames? true - :full-frame? true}) + :full-frame? true + :using-selrect? true}) (rx/map #(cfh/clean-loops objects %)) (rx/map #(dwsh/move-shapes-into-frame (:id shape) %))) (rx/of (dwu/commit-undo-transaction (:id shape)))) From 994d08b4792bf29806df0598edeebfd7cb282f87 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 29 Jan 2024 16:43:13 +0100 Subject: [PATCH 06/37] :bug: Fix problem refreshing layouts --- common/src/app/common/geom/shapes/tree_seq.cljc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/src/app/common/geom/shapes/tree_seq.cljc b/common/src/app/common/geom/shapes/tree_seq.cljc index 37fa09543b..b846a21296 100644 --- a/common/src/app/common/geom/shapes/tree_seq.cljc +++ b/common/src/app/common/geom/shapes/tree_seq.cljc @@ -17,7 +17,11 @@ [id objects] (->> (tree-seq #(d/not-empty? (dm/get-in objects [% :shapes])) - #(dm/get-in objects [% :shapes]) + (fn [id] + (let [shape (get objects id)] + (cond->> (:shapes shape) + (and (ctl/flex-layout? shape) (ctl/reverse? shape)) + (reverse)))) id) (map #(get objects %)))) From 2b715851e187c36c3802cf31247a95783d136492 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 30 Jan 2024 16:00:22 +0100 Subject: [PATCH 07/37] :bug: Fix proportional scaling with grid layout --- common/src/app/common/types/modifiers.cljc | 3 +++ common/src/app/common/types/shape/layout.cljc | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/common/src/app/common/types/modifiers.cljc b/common/src/app/common/types/modifiers.cljc index 5b2b2fe5df..4d893118c5 100644 --- a/common/src/app/common/types/modifiers.cljc +++ b/common/src/app/common/types/modifiers.cljc @@ -756,6 +756,9 @@ (ctl/flex-layout? shape) (ctl/update-flex-scale value) + (ctl/grid-layout? shape) + (ctl/update-grid-scale value) + :always (ctl/update-flex-child value)))] diff --git a/common/src/app/common/types/shape/layout.cljc b/common/src/app/common/types/shape/layout.cljc index 104fb9bc71..823f57407c 100644 --- a/common/src/app/common/types/shape/layout.cljc +++ b/common/src/app/common/types/shape/layout.cljc @@ -601,6 +601,16 @@ (d/update-in-when [:layout-padding :p3] * scale) (d/update-in-when [:layout-padding :p4] * scale))) +(defn update-grid-scale + [shape scale] + (letfn [(scale-track [track] + (cond-> track + (= (:type track) :fixed) + (update :value * scale)))] + (-> shape + (update :layout-grid-columns #(mapv scale-track %)) + (update :layout-grid-rows #(mapv scale-track %))))) + (defn update-flex-child [shape scale] (-> shape From 02ab545cda868187b1e57ebe694fe26578dbad4b Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 30 Jan 2024 16:34:15 +0100 Subject: [PATCH 08/37] :bug: Fix problem with flex layout controls for padding, gap and margin --- frontend/src/app/main/ui/flex_controls/gap.cljs | 6 +++--- frontend/src/app/main/ui/flex_controls/margin.cljs | 6 +++--- frontend/src/app/main/ui/flex_controls/padding.cljs | 7 +++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/main/ui/flex_controls/gap.cljs b/frontend/src/app/main/ui/flex_controls/gap.cljs index bd38eba107..e04024d862 100644 --- a/frontend/src/app/main/ui/flex_controls/gap.cljs +++ b/frontend/src/app/main/ui/flex_controls/gap.cljs @@ -121,9 +121,9 @@ pill-height (/ fcc/flex-display-pill-height zoom) workspace-modifiers (mf/deref refs/workspace-modifiers) gap-selected (mf/deref refs/workspace-gap-selected) - hover (mf/use-var nil) - hover-value (mf/use-var 0) - mouse-pos (mf/use-var nil) + hover (mf/use-state nil) + hover-value (mf/use-state 0) + mouse-pos (mf/use-state nil) padding (:layout-padding frame) gap (:layout-gap frame) {:keys [width height x1 y1]} (:selrect frame) diff --git a/frontend/src/app/main/ui/flex_controls/margin.cljs b/frontend/src/app/main/ui/flex_controls/margin.cljs index 5ee7d33c55..b764972e0f 100644 --- a/frontend/src/app/main/ui/flex_controls/margin.cljs +++ b/frontend/src/app/main/ui/flex_controls/margin.cljs @@ -89,9 +89,9 @@ pill-width (/ fcc/flex-display-pill-width zoom) pill-height (/ fcc/flex-display-pill-height zoom) margins-selected (mf/deref refs/workspace-margins-selected) - hover-value (mf/use-var 0) - mouse-pos (mf/use-var nil) - hover (mf/use-var nil) + hover-value (mf/use-state 0) + mouse-pos (mf/use-state nil) + hover (mf/use-state nil) hover-all? (and (not (nil? @hover)) alt?) hover-v? (and (or (= @hover :m1) (= @hover :m3)) shift?) hover-h? (and (or (= @hover :m2) (= @hover :m4)) shift?) diff --git a/frontend/src/app/main/ui/flex_controls/padding.cljs b/frontend/src/app/main/ui/flex_controls/padding.cljs index 5b64c94fb3..96e0c07d8f 100644 --- a/frontend/src/app/main/ui/flex_controls/padding.cljs +++ b/frontend/src/app/main/ui/flex_controls/padding.cljs @@ -77,7 +77,6 @@ :y (:y rect-data) :width (max 0 (:width rect-data)) :height (max 0 (:height rect-data)) - :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave :on-pointer-move on-pointer-move @@ -115,9 +114,9 @@ [{:keys [frame zoom alt? shift? on-move-selected on-context-menu]}] (let [frame-id (:id frame) paddings-selected (mf/deref refs/workspace-paddings-selected) - hover-value (mf/use-var 0) - mouse-pos (mf/use-var nil) - hover (mf/use-var nil) + hover-value (mf/use-state 0) + mouse-pos (mf/use-state nil) + hover (mf/use-state nil) hover-all? (and (not (nil? @hover)) alt?) hover-v? (and (or (= @hover :p1) (= @hover :p3)) shift?) hover-h? (and (or (= @hover :p2) (= @hover :p4)) shift?) From f6b182a3b56d1ab412fab19d2f1664008b027f47 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 30 Jan 2024 18:02:25 +0100 Subject: [PATCH 09/37] :bug: Fix problem calculating selrect for certain paths --- common/src/app/common/geom/shapes/path.cljc | 44 +++++++++++---------- frontend/src/app/main/data/workspace.cljs | 23 +++++------ 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index 84f0b52418..14571d65a4 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -340,29 +340,31 @@ (grc/points->rect points)))) (defn content->selrect [content] - (let [calc-extremities - (fn [command prev] - (case (:command command) - :move-to [(command->point command)] + (let [extremities + (loop [points #{} + from-p nil + move-p nil + content (seq content)] + (if content + (let [command (first content) + to-p (command->point command) - ;; If it's a line we add the beginning point and endpoint - :line-to [(command->point prev) - (command->point command)] + [from-p move-p command-pts] + (case (:command command) + :move-to [to-p to-p [to-p]] + :close-path [move-p move-p [move-p]] + :line-to [to-p move-p [from-p to-p]] + :curve-to [to-p move-p + (let [c1 (command->point command :c1) + c2 (command->point command :c2) + curve [from-p to-p c1 c2]] + (into [from-p to-p] + (->> (curve-extremities curve) + (map #(curve-values curve %)))))] + [to-p move-p []])] - ;; We return the bezier extremities - :curve-to (into [(command->point prev) - (command->point command)] - (let [curve [(command->point prev) - (command->point command) - (command->point command :c1) - (command->point command :c2)]] - (->> (curve-extremities curve) - (map #(curve-values curve %))))) - [])) - - extremities (mapcat calc-extremities - content - (concat [nil] content))] + (recur (apply conj points command-pts) from-p move-p (next content))) + points))] (grc/points->rect extremities))) (defn move-content [content move-vec] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 5418a24e70..46d8477949 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1013,20 +1013,21 @@ (d/ordered-set)))] (rx/of (dws/select-shapes shapes-to-select))) - (let [{:keys [id type shapes]} (get objects (first selected))] - (case type - :text - (rx/of (dwe/start-edition-mode id)) + (when (d/not-empty? selected) + (let [{:keys [id type shapes]} (get objects (first selected))] + (case type + :text + (rx/of (dwe/start-edition-mode id)) - (:group :bool :frame) - (let [shapes-ids (into (d/ordered-set) shapes)] - (rx/of (dws/select-shapes shapes-ids))) + (:group :bool :frame) + (let [shapes-ids (into (d/ordered-set) shapes)] + (rx/of (dws/select-shapes shapes-ids))) - :svg-raw - nil + :svg-raw + nil - (rx/of (dwe/start-edition-mode id) - (dwdp/start-path-edit id))))))))) + (rx/of (dwe/start-edition-mode id) + (dwdp/start-path-edit id)))))))))) (defn select-parent-layer [] From 14584ef920484824f5553895c8ae4464e0792c64 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 31 Jan 2024 10:07:47 +0100 Subject: [PATCH 10/37] :bug: Fix problem with debug panel and light theme --- .../src/app/main/ui/workspace/sidebar/debug_shape_info.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss index 827c10938f..ea6f438317 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss @@ -10,7 +10,7 @@ display: flex; flex-direction: column; background-color: var(--panel-background-color); - color: white; + color: $df-primary; font-size: $fs-12; user-select: text; } From 1f2f70fcd4ab0c1942c8dbb13419c4a988154b2e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 31 Jan 2024 10:10:18 +0100 Subject: [PATCH 11/37] :sparkles: New menu entry for change theme --- common/src/app/common/geom/shapes/path.cljc | 27 +- .../app/main/ui/workspace/left_header.cljs | 718 +---------------- .../app/main/ui/workspace/left_header.scss | 94 --- .../src/app/main/ui/workspace/main_menu.cljs | 747 ++++++++++++++++++ .../src/app/main/ui/workspace/main_menu.scss | 101 +++ frontend/translations/en.po | 6 + frontend/translations/es.po | 6 + 7 files changed, 889 insertions(+), 810 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/main_menu.cljs create mode 100644 frontend/src/app/main/ui/workspace/main_menu.scss diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index 14571d65a4..d3a00953d2 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -351,21 +351,32 @@ [from-p move-p command-pts] (case (:command command) - :move-to [to-p to-p [to-p]] - :close-path [move-p move-p [move-p]] - :line-to [to-p move-p [from-p to-p]] + :move-to [to-p to-p (when to-p [to-p])] + :close-path [move-p move-p (when move-p [move-p])] + :line-to [to-p move-p (when (and from-p to-p) [from-p to-p])] :curve-to [to-p move-p (let [c1 (command->point command :c1) c2 (command->point command :c2) curve [from-p to-p c1 c2]] - (into [from-p to-p] - (->> (curve-extremities curve) - (map #(curve-values curve %)))))] + (when (and from-p to-p c1 c2) + (into [from-p to-p] + (->> (curve-extremities curve) + (map #(curve-values curve %))))))] [to-p move-p []])] (recur (apply conj points command-pts) from-p move-p (next content))) - points))] - (grc/points->rect extremities))) + points)) + + ;; We haven't found any extremes so we turn the commands to points + extremities + (if (empty? extremities) + (->> content (keep command->point)) + extremities)] + + ;; If no points are returned we return an empty rect. + (if (d/not-empty? extremities) + (grc/points->rect extremities) + (grc/make-rect)))) (defn move-content [content move-vec] (let [dx (:x move-vec) diff --git a/frontend/src/app/main/ui/workspace/left_header.cljs b/frontend/src/app/main/ui/workspace/left_header.cljs index f8c1bf53ce..0c1cd49305 100644 --- a/frontend/src/app/main/ui/workspace/left_header.cljs +++ b/frontend/src/app/main/ui/workspace/left_header.cljs @@ -8,727 +8,28 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] - [app.common.files.helpers :as cfh] - [app.common.uuid :as uuid] - [app.config :as cf] - [app.main.data.common :as dcm] - [app.main.data.events :as ev] - [app.main.data.exports :as de] [app.main.data.modal :as modal] - [app.main.data.shortcuts :as scd] [app.main.data.workspace :as dw] [app.main.data.workspace.colors :as dc] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.shortcuts :as sc] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]] [app.main.ui.context :as ctx] - [app.main.ui.hooks.resize :as r] [app.main.ui.icons :as i] + [app.main.ui.workspace.main-menu :as main-menu] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.router :as rt] [cuerdas.core :as str] - [potok.v2.core :as ptk] [rumext.v2 :as mf])) -;; --- Header menu and submenus - -(mf/defc help-info-menu - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [{:keys [layout on-close]}] - (let [nav-to-helpc-center - (mf/use-fn #(dom/open-new-window "https://help.penpot.app")) - - nav-to-community - (mf/use-fn #(dom/open-new-window "https://community.penpot.app")) - - nav-to-youtube - (mf/use-fn #(dom/open-new-window "https://www.youtube.com/c/Penpot")) - - nav-to-templates - (mf/use-fn #(dom/open-new-window "https://penpot.app/libraries-templates")) - - nav-to-github - (mf/use-fn #(dom/open-new-window "https://github.com/penpot/penpot")) - - nav-to-terms - (mf/use-fn #(dom/open-new-window "https://penpot.app/terms")) - - nav-to-feedback - (mf/use-fn #(st/emit! (rt/nav-new-window* {:rname :settings-feedback}))) - - show-shortcuts - (mf/use-fn - (mf/deps layout) - (fn [] - (when (contains? layout :collapse-left-sidebar) - (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) - - (st/emit! - (-> (dw/toggle-layout-flag :shortcuts) - (vary-meta assoc ::ev/origin "workspace-header"))))) - - show-release-notes - (mf/use-fn - (fn [event] - (let [version (:main cf/version)] - (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) - (if (and (kbd/alt? event) (kbd/mod? event)) - (st/emit! (modal/show {:type :onboarding})) - (st/emit! (modal/show {:type :release-notes :version version}))))))] - - [:& dropdown-menu {:show true - :on-close on-close - :list-class (stl/css-case :sub-menu true - :help-info true)} - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click nav-to-helpc-center - :on-key-down (fn [event] - (when (kbd/enter? event) - (nav-to-helpc-center event))) - :id "file-menu-help-center"} - [:span {:class (stl/css :item-name)} (tr "labels.help-center")]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click nav-to-community - :on-key-down (fn [event] - (when (kbd/enter? event) - (nav-to-community event))) - :id "file-menu-community"} - [:span {:class (stl/css :item-name)} (tr "labels.community")]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click nav-to-youtube - :on-key-down (fn [event] - (when (kbd/enter? event) - (nav-to-youtube event))) - :id "file-menu-youtube"} - [:span {:class (stl/css :item-name)} (tr "labels.tutorials")]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click show-release-notes - :on-key-down (fn [event] - (when (kbd/enter? event) - (show-release-notes event))) - :id "file-menu-release-notes"} - [:span {:class (stl/css :item-name)} (tr "labels.release-notes")]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click nav-to-templates - :on-key-down (fn [event] - (when (kbd/enter? event) - (nav-to-templates event))) - :id "file-menu-templates"} - [:span {:class (stl/css :item-name)} (tr "labels.libraries-and-templates")]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click nav-to-github - :on-key-down (fn [event] - (when (kbd/enter? event) - (nav-to-github event))) - :id "file-menu-github"} - [:span {:class (stl/css :item-name)} (tr "labels.github-repo")]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click nav-to-terms - :on-key-down (fn [event] - (when (kbd/enter? event) - (nav-to-terms event))) - :id "file-menu-terms"} - [:span {:class (stl/css :item-name)} (tr "auth.terms-of-service")]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click show-shortcuts - :on-key-down (fn [event] - (when (kbd/enter? event) - (show-shortcuts event))) - :id "file-menu-shortcuts"} - [:span {:class (stl/css :item-name)} (tr "label.shortcuts")] - [:span {:class (stl/css :shortcut)} - - (for [sc (scd/split-sc (sc/get-tooltip :show-shortcuts))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - (when (contains? cf/flags :user-feedback) - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click nav-to-feedback - :on-key-down (fn [event] - (when (kbd/enter? event) - (nav-to-feedback event))) - :id "file-menu-feedback"} - [:span {:class (stl/css-case :feedback true - :item-name true)} (tr "labels.give-feedback")]])])) - -(mf/defc preferences-menu - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [{:keys [layout toggle-flag on-close]}] - (let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))] - - [:& dropdown-menu {:show true - :list-class (stl/css-case :sub-menu true - :preferences true) - :on-close on-close} - [:> dropdown-menu-item* {:on-click toggle-flag - :class (stl/css :submenu-item) - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-flag event))) - :data-test "scale-text" - :id "file-menu-scale-text"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :scale-text) - (tr "workspace.header.menu.disable-scale-content") - (tr "workspace.header.menu.enable-scale-content"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-scale-text))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - [:> dropdown-menu-item* {:on-click toggle-flag - :class (stl/css :submenu-item) - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-flag event))) - :data-test "snap-guides" - :id "file-menu-snap-guides"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :snap-guides) - (tr "workspace.header.menu.disable-snap-guides") - (tr "workspace.header.menu.enable-snap-guides"))] - [:span {:class (stl/css :shortcut)} - - (for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-guide))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - [:> dropdown-menu-item* {:on-click toggle-flag - :class (stl/css :submenu-item) - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-flag event))) - :data-test "snap-grid" - :id "file-menu-snap-grid"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :snap-grid) - (tr "workspace.header.menu.disable-snap-grid") - (tr "workspace.header.menu.enable-snap-grid"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-grid))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - [:> dropdown-menu-item* {:on-click toggle-flag - :class (stl/css :submenu-item) - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-flag event))) - :data-test "dynamic-alignment" - :id "file-menu-dynamic-alignment"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :dynamic-alignment) - (tr "workspace.header.menu.disable-dynamic-alignment") - (tr "workspace.header.menu.enable-dynamic-alignment"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-alignment))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - [:> dropdown-menu-item* {:on-click toggle-flag - :class (stl/css :submenu-item) - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-flag event))) - :data-test "snap-pixel-grid" - :id "file-menu-pixel-grid"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :snap-pixel-grid) - (tr "workspace.header.menu.disable-snap-pixel-grid") - (tr "workspace.header.menu.enable-snap-pixel-grid"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :snap-pixel-grid))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - [:> dropdown-menu-item* {:on-click show-nudge-options - :class (stl/css :submenu-item) - :on-key-down (fn [event] - (when (kbd/enter? event) - (show-nudge-options event))) - :data-test "snap-pixel-grid" - :id "file-menu-nudge"} - [:span {:class (stl/css :item-name)} (tr "modals.nudge-title")]]])) - -(mf/defc view-menu - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [{:keys [layout toggle-flag on-close]}] - (let [read-only? (mf/use-ctx ctx/workspace-read-only?) - - toggle-color-palette - (mf/use-fn - (fn [] - (r/set-resize-type! :bottom) - (st/emit! (dw/remove-layout-flag :textpalette) - (-> (dw/toggle-layout-flag :colorpalette) - (vary-meta assoc ::ev/origin "workspace-menu"))))) - - toggle-text-palette - (mf/use-fn - (fn [] - (r/set-resize-type! :bottom) - (st/emit! (dw/remove-layout-flag :colorpalette) - (-> (dw/toggle-layout-flag :textpalette) - (vary-meta assoc ::ev/origin "workspace-menu")))))] - - [:& dropdown-menu {:show true - :list-class (stl/css-case :sub-menu true - :view true) - :on-close on-close} - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click toggle-flag - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-flag event))) - :data-test "rules" - :id "file-menu-rules"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :rules) - (tr "workspace.header.menu.hide-rules") - (tr "workspace.header.menu.show-rules"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-rules))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click toggle-flag - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-flag event))) - :data-test "display-grid" - :id "file-menu-grid"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :display-grid) - (tr "workspace.header.menu.hide-grid") - (tr "workspace.header.menu.show-grid"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-grid))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - - (when-not ^boolean read-only? - [:* - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click toggle-color-palette - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-color-palette event))) - :id "file-menu-color-palette"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :colorpalette) - (tr "workspace.header.menu.hide-palette") - (tr "workspace.header.menu.show-palette"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-colorpalette))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click toggle-text-palette - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-text-palette event))) - :id "file-menu-text-palette"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :textpalette) - (tr "workspace.header.menu.hide-textpalette") - (tr "workspace.header.menu.show-textpalette"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-textpalette))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]]]) - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click toggle-flag - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-flag event))) - :data-test "display-artboard-names" - :id "file-menu-artboards"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :display-artboard-names) - (tr "workspace.header.menu.hide-artboard-names") - (tr "workspace.header.menu.show-artboard-names"))]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click toggle-flag - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-flag event))) - :data-test "show-pixel-grid" - :id "file-menu-pixel-grid"} - [:span {:class (stl/css :item-name)} - (if (contains? layout :show-pixel-grid) - (tr "workspace.header.menu.hide-pixel-grid") - (tr "workspace.header.menu.show-pixel-grid"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :show-pixel-grid))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click toggle-flag - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-flag event))) - :data-test "hide-ui" - :id "file-menu-hide-ui"} - [:span {:class (stl/css :item-name)} - (tr "workspace.shape.menu.hide-ui")] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :hide-ui))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]]])) - -(mf/defc edit-menu - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [{:keys [on-close]}] - (let [select-all (mf/use-fn #(st/emit! (dw/select-all))) - undo (mf/use-fn #(st/emit! dwc/undo)) - redo (mf/use-fn #(st/emit! dwc/redo))] - [:& dropdown-menu {:show true - :list-class (stl/css-case :sub-menu true - :edit true) - :on-close on-close} - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click select-all - :on-key-down (fn [event] - (when (kbd/enter? event) - (select-all event))) - :id "file-menu-select-all"} - [:span {:class (stl/css :item-name)} - (tr "workspace.header.menu.select-all")] - [:span {:class (stl/css :shortcut)} - - (for [sc (scd/split-sc (sc/get-tooltip :select-all))] - [:span {:class (stl/css :shortcut-key) - :key sc} - sc])]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click undo - :on-key-down (fn [event] - (when (kbd/enter? event) - (undo event))) - :id "file-menu-undo"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.undo")] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :undo))] - [:span {:class (stl/css :shortcut-key) - :key sc} - sc])]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click redo - :on-key-down (fn [event] - (when (kbd/enter? event) - (redo event))) - :id "file-menu-redo"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.redo")] - [:span {:class (stl/css :shortcut)} - - (for [sc (scd/split-sc (sc/get-tooltip :redo))] - [:span {:class (stl/css :shortcut-key) - :key sc} - sc])]]])) - -(mf/defc file-menu - {::mf/wrap-props false} - [{:keys [on-close file]}] - (let [file-id (:id file) - shared? (:is-shared file) - - objects (mf/deref refs/workspace-page-objects) - frames (->> (cfh/get-immediate-children objects uuid/zero) - (filterv cfh/frame-shape?)) - - on-remove-shared - (mf/use-fn - (mf/deps file-id) - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (modal/show! - {:type :delete-shared-libraries - :origin :unpublish - :ids #{file-id} - :on-accept #(st/emit! (dwl/set-file-shared file-id false)) - :count-libraries 1}))) - - on-remove-shared-key-down - (mf/use-fn - (mf/deps on-remove-shared) - (fn [event] - (when (kbd/enter? event) - (on-remove-shared event)))) - - on-add-shared - (mf/use-fn - (mf/deps file-id) - (fn [_event] - (let [on-accept #(st/emit! (dwl/set-file-shared file-id true))] - (st/emit! (dcm/show-shared-dialog file-id on-accept))))) - - on-add-shared-key-down - (mf/use-fn - (mf/deps on-add-shared) - (fn [event] - (when (kbd/enter? event) - (on-add-shared event)))) - - on-export-shapes - (mf/use-fn #(st/emit! (de/show-workspace-export-dialog))) - - on-export-shapes-key-down - (mf/use-fn - (mf/deps on-export-shapes) - (fn [event] - (when (kbd/enter? event) - (on-export-shapes event)))) - - on-export-file - (mf/use-fn - (mf/deps file) - (fn [event] - (let [target (dom/get-current-target event) - binary? (= (dom/get-data target "binary") "true") - evname (if binary? - "export-binary-files" - "export-standard-files")] - (st/emit! - (ptk/event ::ev/event {::ev/name evname - ::ev/origin "workspace" - :num-files 1}) - (dcm/export-files [file] binary?))))) - - on-export-file-key-down - (mf/use-fn - (mf/deps on-export-file) - (fn [event] - (when (kbd/enter? event) - (on-export-file event)))) - - on-export-frames - (mf/use-fn - (mf/deps frames) - (fn [_] - (st/emit! (de/show-workspace-export-frames-dialog (reverse frames))))) - - on-export-frames-key-down - (mf/use-fn - (mf/deps on-export-frames) - (fn [event] - (when (kbd/enter? event) - (on-export-frames event))))] - - [:& dropdown-menu {:show true - :list-class (stl/css-case :sub-menu true - :file true) - :on-close on-close} - - (if ^boolean shared? - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click on-remove-shared - :on-key-down on-remove-shared-key-down - :id "file-menu-remove-shared"} - [:span {:class (stl/css :item-name)} (tr "dashboard.unpublish-shared")]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click on-add-shared - :on-key-down on-add-shared-key-down - :id "file-menu-add-shared"} - [:span {:class (stl/css :item-name)} (tr "dashboard.add-shared")]]) - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click on-export-shapes - :on-key-down on-export-shapes-key-down - :id "file-menu-export-shapes"} - [:span {:class (stl/css :item-name)} (tr "dashboard.export-shapes")] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :export-shapes))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click on-export-file - :on-key-down on-export-file-key-down - :data-binary true - :id "file-menu-binary-file"} - [:span {:class (stl/css :item-name)} - (tr "dashboard.download-binary-file")]] - - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click on-export-file - :on-key-down on-export-file-key-down - :data-binary false - :id "file-menu-standard-file"} - [:span {:class (stl/css :item-name)} - (tr "dashboard.download-standard-file")]] - - (when (seq frames) - [:> dropdown-menu-item* {:class (stl/css :submenu-item) - :on-click on-export-frames - :on-key-down on-export-frames-key-down - :id "file-menu-export-frames"} - [:span {:class (stl/css :item-name)} - (tr "dashboard.export-frames")]])])) - -(mf/defc menu - {::mf/wrap-props false} - [{:keys [layout file]}] - (let [show-menu* (mf/use-state false) - show-menu? (deref show-menu*) - sub-menu* (mf/use-state false) - sub-menu (deref sub-menu*) - - open-menu - (mf/use-fn - (fn [event] - (dom/stop-propagation event) - (reset! show-menu* true))) - - close-menu - (mf/use-fn - (fn [event] - (dom/stop-propagation event) - (reset! show-menu* false))) - - close-sub-menu - (mf/use-fn - (fn [event] - (dom/stop-propagation event) - (reset! sub-menu* nil))) - - on-menu-click - (mf/use-fn - (fn [event] - (dom/stop-propagation event) - (let [menu (-> (dom/get-current-target event) - (dom/get-data "test") - (keyword))] - (reset! sub-menu* menu)))) - - toggle-flag - (mf/use-fn - (fn [event] - (dom/stop-propagation event) - (let [flag (-> (dom/get-current-target event) - (dom/get-data "test") - (keyword))] - (st/emit! - (-> (dw/toggle-layout-flag flag) - (vary-meta assoc ::ev/origin "workspace-menu"))) - (reset! show-menu* false) - (reset! sub-menu* nil))))] - - - [:* - [:div {:on-click open-menu - :class (stl/css :menu-btn)} i/menu-refactor] - - [:& dropdown-menu {:show show-menu? - :on-close close-menu - :list-class (stl/css :menu)} - - [:> dropdown-menu-item* {:class (stl/css :menu-item) - :on-click on-menu-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-menu-click event))) - :on-pointer-enter on-menu-click - :data-test "file" - :id "file-menu-file"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.file")] - [:span {:class (stl/css :open-arrow)} i/arrow-refactor]] - - [:> dropdown-menu-item* {:class (stl/css :menu-item) - :on-click on-menu-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-menu-click event))) - :on-pointer-enter on-menu-click - :data-test "edit" - :id "file-menu-edit"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.edit")] - [:span {:class (stl/css :open-arrow)} i/arrow-refactor]] - - [:> dropdown-menu-item* {:class (stl/css :menu-item) - :on-click on-menu-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-menu-click event))) - :on-pointer-enter on-menu-click - :data-test "view" - :id "file-menu-view"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.view")] - [:span {:class (stl/css :open-arrow)} i/arrow-refactor]] - - [:> dropdown-menu-item* {:class (stl/css :menu-item) - :on-click on-menu-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-menu-click event))) - :on-pointer-enter on-menu-click - :data-test "preferences" - :id "file-menu-preferences"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.preferences")] - [:span {:class (stl/css :open-arrow)} i/arrow-refactor]] - [:div {:class (stl/css :separator)}] - [:> dropdown-menu-item* {:class (stl/css-case :menu-item true) - :on-click on-menu-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-menu-click event))) - :on-pointer-enter on-menu-click - :data-test "help-info" - :id "file-menu-help-info"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.help-info")] - [:span {:class (stl/css :open-arrow)} i/arrow-refactor]]] - - (case sub-menu - :file - [:& file-menu - {:file file - :on-close close-sub-menu}] - - :edit - [:& edit-menu - {:on-close close-sub-menu}] - - :view - [:& view-menu - {:layout layout - :toggle-flag toggle-flag - :on-close close-sub-menu}] - - :preferences - [:& preferences-menu - {:layout layout - :toggle-flag toggle-flag - :on-close close-sub-menu}] - - :help-info - [:& help-info-menu - {:layout layout - :on-close close-sub-menu}] - - nil)])) - ;; --- Header Component (mf/defc left-header {::mf/wrap-props false} [{:keys [file layout project page-id class]}] - (let [file-id (:id file) + (let [profile (mf/deref refs/profile) + file-id (:id file) file-name (:name file) project-id (:id project) team-id (:team-id project) @@ -809,9 +110,10 @@ (when ^boolean shared? [:span {:class (stl/css :shared-badge)} i/library-refactor]) [:div {:class (stl/css :menu-section)} - [:& menu {:layout layout - :file file - :read-only? read-only? - :team-id team-id - :page-id page-id}]]])) - + [:& main-menu/menu + {:layout layout + :file file + :profile profile + :read-only? read-only? + :team-id team-id + :page-id page-id}]]])) diff --git a/frontend/src/app/main/ui/workspace/left_header.scss b/frontend/src/app/main/ui/workspace/left_header.scss index 6b0e5016e0..1b441f83bb 100644 --- a/frontend/src/app/main/ui/workspace/left_header.scss +++ b/frontend/src/app/main/ui/workspace/left_header.scss @@ -77,97 +77,3 @@ width: $s-16; } } - -.menu-btn { - @extend .button-tertiary; - height: $s-32; - width: calc($s-24 + $s-4); - padding: 0; - border-radius: $br-8; - svg { - @extend .button-icon; - stroke: var(--icon-foreground); - } -} - -.menu { - @extend .menu-dropdown; - top: $s-48; - left: calc(var(--width, $s-256) - $s-16); - width: $s-192; - margin: 0; -} - -.menu-item { - @extend .menu-item-base; - cursor: pointer; - .open-arrow { - @include flexCenter; - svg { - @extend .button-icon; - stroke: var(--icon-foreground); - } - } - &:hover { - color: var(--menu-foreground-color-hover); - .open-arrow { - svg { - stroke: var(--menu-foreground-color-hover); - } - } - .shortcut-key { - color: var(--menu-shortcut-foreground-color-hover); - } - } -} - -.separator { - margin-top: $s-8; - height: $s-4; - border-top: $s-1 solid $db-secondary; -} - -.shortcut { - @extend .shortcut-base; -} -.shortcut-key { - @extend .shortcut-key-base; -} - -.sub-menu { - @extend .menu-dropdown; - left: calc(var(--width, $s-256) + $s-180); - width: $s-192; - min-width: calc($s-272 - $s-2); - width: 110%; - - .submenu-item { - @extend .menu-item-base; - &:hover { - color: var(--menu-foreground-color-hover); - .shortcut-key { - color: var(--menu-shortcut-foreground-color-hover); - } - } - } - - &.file { - top: $s-48; - } - - &.edit { - top: $s-76; - } - - &.view { - top: $s-116; - } - - &.preferences { - top: $s-148; - } - - &.help-info { - top: $s-196; - } -} diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs new file mode 100644 index 0000000000..42d17f70b1 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -0,0 +1,747 @@ +;; 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) KALEIDOS INC + +(ns app.main.ui.workspace.main-menu + (:require-macros [app.main.style :as stl]) + (:require + [app.common.files.helpers :as cfh] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.main.data.common :as dcm] + [app.main.data.events :as ev] + [app.main.data.exports :as de] + [app.main.data.modal :as modal] + [app.main.data.shortcuts :as scd] + [app.main.data.users :as du] + [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.shortcuts :as sc] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]] + [app.main.ui.context :as ctx] + [app.main.ui.hooks.resize :as r] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [app.util.router :as rt] + [potok.v2.core :as ptk] + [rumext.v2 :as mf])) + +;; --- Header menu and submenus + +(mf/defc help-info-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [layout on-close]}] + (let [nav-to-helpc-center + (mf/use-fn #(dom/open-new-window "https://help.penpot.app")) + + nav-to-community + (mf/use-fn #(dom/open-new-window "https://community.penpot.app")) + + nav-to-youtube + (mf/use-fn #(dom/open-new-window "https://www.youtube.com/c/Penpot")) + + nav-to-templates + (mf/use-fn #(dom/open-new-window "https://penpot.app/libraries-templates")) + + nav-to-github + (mf/use-fn #(dom/open-new-window "https://github.com/penpot/penpot")) + + nav-to-terms + (mf/use-fn #(dom/open-new-window "https://penpot.app/terms")) + + nav-to-feedback + (mf/use-fn #(st/emit! (rt/nav-new-window* {:rname :settings-feedback}))) + + show-shortcuts + (mf/use-fn + (mf/deps layout) + (fn [] + (when (contains? layout :collapse-left-sidebar) + (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) + + (st/emit! + (-> (dw/toggle-layout-flag :shortcuts) + (vary-meta assoc ::ev/origin "workspace-header"))))) + + show-release-notes + (mf/use-fn + (fn [event] + (let [version (:main cf/version)] + (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) + (if (and (kbd/alt? event) (kbd/mod? event)) + (st/emit! (modal/show {:type :onboarding})) + (st/emit! (modal/show {:type :release-notes :version version}))))))] + + [:& dropdown-menu {:show true + :on-close on-close + :list-class (stl/css-case :sub-menu true + :help-info true)} + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-helpc-center + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-helpc-center event))) + :id "file-menu-help-center"} + [:span {:class (stl/css :item-name)} (tr "labels.help-center")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-community + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-community event))) + :id "file-menu-community"} + [:span {:class (stl/css :item-name)} (tr "labels.community")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-youtube + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-youtube event))) + :id "file-menu-youtube"} + [:span {:class (stl/css :item-name)} (tr "labels.tutorials")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click show-release-notes + :on-key-down (fn [event] + (when (kbd/enter? event) + (show-release-notes event))) + :id "file-menu-release-notes"} + [:span {:class (stl/css :item-name)} (tr "labels.release-notes")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-templates + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-templates event))) + :id "file-menu-templates"} + [:span {:class (stl/css :item-name)} (tr "labels.libraries-and-templates")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-github + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-github event))) + :id "file-menu-github"} + [:span {:class (stl/css :item-name)} (tr "labels.github-repo")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-terms + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-terms event))) + :id "file-menu-terms"} + [:span {:class (stl/css :item-name)} (tr "auth.terms-of-service")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click show-shortcuts + :on-key-down (fn [event] + (when (kbd/enter? event) + (show-shortcuts event))) + :id "file-menu-shortcuts"} + [:span {:class (stl/css :item-name)} (tr "label.shortcuts")] + [:span {:class (stl/css :shortcut)} + + (for [sc (scd/split-sc (sc/get-tooltip :show-shortcuts))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + (when (contains? cf/flags :user-feedback) + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-feedback + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-feedback event))) + :id "file-menu-feedback"} + [:span {:class (stl/css-case :feedback true + :item-name true)} (tr "labels.give-feedback")]])])) + +(mf/defc preferences-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [layout profile toggle-flag on-close toggle-theme]}] + (let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))] + + [:& dropdown-menu {:show true + :list-class (stl/css-case :sub-menu true + :preferences true) + :on-close on-close} + [:> dropdown-menu-item* {:on-click toggle-flag + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "scale-text" + :id "file-menu-scale-text"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :scale-text) + (tr "workspace.header.menu.disable-scale-content") + (tr "workspace.header.menu.enable-scale-content"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-scale-text))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:on-click toggle-flag + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "snap-guides" + :id "file-menu-snap-guides"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :snap-guides) + (tr "workspace.header.menu.disable-snap-guides") + (tr "workspace.header.menu.enable-snap-guides"))] + [:span {:class (stl/css :shortcut)} + + (for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-guide))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:on-click toggle-flag + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "snap-grid" + :id "file-menu-snap-grid"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :snap-grid) + (tr "workspace.header.menu.disable-snap-grid") + (tr "workspace.header.menu.enable-snap-grid"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-grid))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:on-click toggle-flag + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "dynamic-alignment" + :id "file-menu-dynamic-alignment"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :dynamic-alignment) + (tr "workspace.header.menu.disable-dynamic-alignment") + (tr "workspace.header.menu.enable-dynamic-alignment"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-alignment))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:on-click toggle-flag + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "snap-pixel-grid" + :id "file-menu-pixel-grid"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :snap-pixel-grid) + (tr "workspace.header.menu.disable-snap-pixel-grid") + (tr "workspace.header.menu.enable-snap-pixel-grid"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :snap-pixel-grid))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:on-click show-nudge-options + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (show-nudge-options event))) + :data-test "snap-pixel-grid" + :id "file-menu-nudge"} + [:span {:class (stl/css :item-name)} (tr "modals.nudge-title")]] + + + [:> dropdown-menu-item* {:on-click toggle-theme + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-theme event))) + :data-test "toggle-theme" + :id "file-menu-toggle-theme"} + [:span {:class (stl/css :item-name)} + (if (= (:theme profile) "default") + (tr "workspace.header.menu.toggle-light-theme") + (tr "workspace.header.menu.toggle-dark-theme"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-theme))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]]])) + +(mf/defc view-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [layout toggle-flag on-close]}] + (let [read-only? (mf/use-ctx ctx/workspace-read-only?) + + toggle-color-palette + (mf/use-fn + (fn [] + (r/set-resize-type! :bottom) + (st/emit! (dw/remove-layout-flag :textpalette) + (-> (dw/toggle-layout-flag :colorpalette) + (vary-meta assoc ::ev/origin "workspace-menu"))))) + + toggle-text-palette + (mf/use-fn + (fn [] + (r/set-resize-type! :bottom) + (st/emit! (dw/remove-layout-flag :colorpalette) + (-> (dw/toggle-layout-flag :textpalette) + (vary-meta assoc ::ev/origin "workspace-menu")))))] + + [:& dropdown-menu {:show true + :list-class (stl/css-case :sub-menu true + :view true) + :on-close on-close} + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "rules" + :id "file-menu-rules"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :rules) + (tr "workspace.header.menu.hide-rules") + (tr "workspace.header.menu.show-rules"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-rules))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "display-grid" + :id "file-menu-grid"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :display-grid) + (tr "workspace.header.menu.hide-grid") + (tr "workspace.header.menu.show-grid"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-grid))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + + (when-not ^boolean read-only? + [:* + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-color-palette + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-color-palette event))) + :id "file-menu-color-palette"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :colorpalette) + (tr "workspace.header.menu.hide-palette") + (tr "workspace.header.menu.show-palette"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-colorpalette))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-text-palette + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-text-palette event))) + :id "file-menu-text-palette"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :textpalette) + (tr "workspace.header.menu.hide-textpalette") + (tr "workspace.header.menu.show-textpalette"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-textpalette))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]]]) + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "display-artboard-names" + :id "file-menu-artboards"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :display-artboard-names) + (tr "workspace.header.menu.hide-artboard-names") + (tr "workspace.header.menu.show-artboard-names"))]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "show-pixel-grid" + :id "file-menu-pixel-grid"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :show-pixel-grid) + (tr "workspace.header.menu.hide-pixel-grid") + (tr "workspace.header.menu.show-pixel-grid"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :show-pixel-grid))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "hide-ui" + :id "file-menu-hide-ui"} + [:span {:class (stl/css :item-name)} + (tr "workspace.shape.menu.hide-ui")] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :hide-ui))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]]])) + +(mf/defc edit-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [on-close]}] + (let [select-all (mf/use-fn #(st/emit! (dw/select-all))) + undo (mf/use-fn #(st/emit! dwc/undo)) + redo (mf/use-fn #(st/emit! dwc/redo))] + [:& dropdown-menu {:show true + :list-class (stl/css-case :sub-menu true + :edit true) + :on-close on-close} + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click select-all + :on-key-down (fn [event] + (when (kbd/enter? event) + (select-all event))) + :id "file-menu-select-all"} + [:span {:class (stl/css :item-name)} + (tr "workspace.header.menu.select-all")] + [:span {:class (stl/css :shortcut)} + + (for [sc (scd/split-sc (sc/get-tooltip :select-all))] + [:span {:class (stl/css :shortcut-key) + :key sc} + sc])]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click undo + :on-key-down (fn [event] + (when (kbd/enter? event) + (undo event))) + :id "file-menu-undo"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.undo")] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :undo))] + [:span {:class (stl/css :shortcut-key) + :key sc} + sc])]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click redo + :on-key-down (fn [event] + (when (kbd/enter? event) + (redo event))) + :id "file-menu-redo"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.redo")] + [:span {:class (stl/css :shortcut)} + + (for [sc (scd/split-sc (sc/get-tooltip :redo))] + [:span {:class (stl/css :shortcut-key) + :key sc} + sc])]]])) + +(mf/defc file-menu + {::mf/wrap-props false} + [{:keys [on-close file]}] + (let [file-id (:id file) + shared? (:is-shared file) + + objects (mf/deref refs/workspace-page-objects) + frames (->> (cfh/get-immediate-children objects uuid/zero) + (filterv cfh/frame-shape?)) + + on-remove-shared + (mf/use-fn + (mf/deps file-id) + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (modal/show! + {:type :delete-shared-libraries + :origin :unpublish + :ids #{file-id} + :on-accept #(st/emit! (dwl/set-file-shared file-id false)) + :count-libraries 1}))) + + on-remove-shared-key-down + (mf/use-fn + (mf/deps on-remove-shared) + (fn [event] + (when (kbd/enter? event) + (on-remove-shared event)))) + + on-add-shared + (mf/use-fn + (mf/deps file-id) + (fn [_event] + (let [on-accept #(st/emit! (dwl/set-file-shared file-id true))] + (st/emit! (dcm/show-shared-dialog file-id on-accept))))) + + on-add-shared-key-down + (mf/use-fn + (mf/deps on-add-shared) + (fn [event] + (when (kbd/enter? event) + (on-add-shared event)))) + + on-export-shapes + (mf/use-fn #(st/emit! (de/show-workspace-export-dialog))) + + on-export-shapes-key-down + (mf/use-fn + (mf/deps on-export-shapes) + (fn [event] + (when (kbd/enter? event) + (on-export-shapes event)))) + + on-export-file + (mf/use-fn + (mf/deps file) + (fn [event] + (let [target (dom/get-current-target event) + binary? (= (dom/get-data target "binary") "true") + evname (if binary? + "export-binary-files" + "export-standard-files")] + (st/emit! + (ptk/event ::ev/event {::ev/name evname + ::ev/origin "workspace" + :num-files 1}) + (dcm/export-files [file] binary?))))) + + on-export-file-key-down + (mf/use-fn + (mf/deps on-export-file) + (fn [event] + (when (kbd/enter? event) + (on-export-file event)))) + + on-export-frames + (mf/use-fn + (mf/deps frames) + (fn [_] + (st/emit! (de/show-workspace-export-frames-dialog (reverse frames))))) + + on-export-frames-key-down + (mf/use-fn + (mf/deps on-export-frames) + (fn [event] + (when (kbd/enter? event) + (on-export-frames event))))] + + [:& dropdown-menu {:show true + :list-class (stl/css-case :sub-menu true + :file true) + :on-close on-close} + + (if ^boolean shared? + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-remove-shared + :on-key-down on-remove-shared-key-down + :id "file-menu-remove-shared"} + [:span {:class (stl/css :item-name)} (tr "dashboard.unpublish-shared")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-add-shared + :on-key-down on-add-shared-key-down + :id "file-menu-add-shared"} + [:span {:class (stl/css :item-name)} (tr "dashboard.add-shared")]]) + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-export-shapes + :on-key-down on-export-shapes-key-down + :id "file-menu-export-shapes"} + [:span {:class (stl/css :item-name)} (tr "dashboard.export-shapes")] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :export-shapes))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-export-file + :on-key-down on-export-file-key-down + :data-binary true + :id "file-menu-binary-file"} + [:span {:class (stl/css :item-name)} + (tr "dashboard.download-binary-file")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-export-file + :on-key-down on-export-file-key-down + :data-binary false + :id "file-menu-standard-file"} + [:span {:class (stl/css :item-name)} + (tr "dashboard.download-standard-file")]] + + (when (seq frames) + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-export-frames + :on-key-down on-export-frames-key-down + :id "file-menu-export-frames"} + [:span {:class (stl/css :item-name)} + (tr "dashboard.export-frames")]])])) + +(mf/defc menu + {::mf/wrap-props false} + [{:keys [layout file profile]}] + (let [show-menu* (mf/use-state false) + show-menu? (deref show-menu*) + sub-menu* (mf/use-state false) + sub-menu (deref sub-menu*) + + open-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! show-menu* true))) + + close-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! show-menu* false))) + + close-sub-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! sub-menu* nil))) + + on-menu-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (let [menu (-> (dom/get-current-target event) + (dom/get-data "test") + (keyword))] + (reset! sub-menu* menu)))) + + toggle-flag + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (let [flag (-> (dom/get-current-target event) + (dom/get-data "test") + (keyword))] + (st/emit! + (-> (dw/toggle-layout-flag flag) + (vary-meta assoc ::ev/origin "workspace-menu"))) + (reset! show-menu* false) + (reset! sub-menu* nil)))) + + + toggle-theme + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (st/emit! (du/toggle-theme))))] + + + [:* + [:div {:on-click open-menu + :class (stl/css :menu-btn)} i/menu-refactor] + + [:& dropdown-menu {:show show-menu? + :on-close close-menu + :list-class (stl/css :menu)} + + [:> dropdown-menu-item* {:class (stl/css :menu-item) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-test "file" + :id "file-menu-file"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.file")] + [:span {:class (stl/css :open-arrow)} i/arrow-refactor]] + + [:> dropdown-menu-item* {:class (stl/css :menu-item) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-test "edit" + :id "file-menu-edit"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.edit")] + [:span {:class (stl/css :open-arrow)} i/arrow-refactor]] + + [:> dropdown-menu-item* {:class (stl/css :menu-item) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-test "view" + :id "file-menu-view"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.view")] + [:span {:class (stl/css :open-arrow)} i/arrow-refactor]] + + [:> dropdown-menu-item* {:class (stl/css :menu-item) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-test "preferences" + :id "file-menu-preferences"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.preferences")] + [:span {:class (stl/css :open-arrow)} i/arrow-refactor]] + [:div {:class (stl/css :separator)}] + [:> dropdown-menu-item* {:class (stl/css-case :menu-item true) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-test "help-info" + :id "file-menu-help-info"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.help-info")] + [:span {:class (stl/css :open-arrow)} i/arrow-refactor]]] + + (case sub-menu + :file + [:& file-menu + {:file file + :on-close close-sub-menu}] + + :edit + [:& edit-menu + {:on-close close-sub-menu}] + + :view + [:& view-menu + {:layout layout + :toggle-flag toggle-flag + :on-close close-sub-menu}] + + :preferences + [:& preferences-menu + {:layout layout + :profile profile + :toggle-flag toggle-flag + :toggle-theme toggle-theme + :on-close close-sub-menu}] + + :help-info + [:& help-info-menu + {:layout layout + :on-close close-sub-menu}] + + nil)])) diff --git a/frontend/src/app/main/ui/workspace/main_menu.scss b/frontend/src/app/main/ui/workspace/main_menu.scss new file mode 100644 index 0000000000..55732dab29 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/main_menu.scss @@ -0,0 +1,101 @@ +// 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) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.menu-btn { + @extend .button-tertiary; + height: $s-32; + width: calc($s-24 + $s-4); + padding: 0; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.menu { + @extend .menu-dropdown; + top: $s-48; + left: calc(var(--width, $s-256) - $s-16); + width: $s-192; + margin: 0; +} + +.menu-item { + @extend .menu-item-base; + cursor: pointer; + .open-arrow { + @include flexCenter; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + &:hover { + color: var(--menu-foreground-color-hover); + .open-arrow { + svg { + stroke: var(--menu-foreground-color-hover); + } + } + .shortcut-key { + color: var(--menu-shortcut-foreground-color-hover); + } + } +} + +.separator { + margin-top: $s-8; + height: $s-4; + border-top: $s-1 solid $db-secondary; +} + +.shortcut { + @extend .shortcut-base; +} +.shortcut-key { + @extend .shortcut-key-base; +} + +.sub-menu { + @extend .menu-dropdown; + left: calc(var(--width, $s-256) + $s-180); + width: $s-192; + min-width: calc($s-272 - $s-2); + width: 110%; + + .submenu-item { + @extend .menu-item-base; + &:hover { + color: var(--menu-foreground-color-hover); + .shortcut-key { + color: var(--menu-shortcut-foreground-color-hover); + } + } + } + + &.file { + top: $s-48; + } + + &.edit { + top: $s-76; + } + + &.view { + top: $s-116; + } + + &.preferences { + top: $s-148; + } + + &.help-info { + top: $s-196; + } +} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 69cad92bf2..46f2e63c17 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3442,6 +3442,12 @@ msgstr "Show fonts palette" msgid "workspace.header.menu.undo" msgstr "Undo" +msgid "workspace.header.menu.toggle-light-theme" +msgstr "Switch to light theme" + +msgid "workspace.header.menu.toggle-dark-theme" +msgstr "Switch to dark theme" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Reset" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index fbd44e23a4..8af585f2bf 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3504,6 +3504,12 @@ msgstr "Mostrar paleta de textos" msgid "workspace.header.menu.undo" msgstr "Deshacer" +msgid "workspace.header.menu.toggle-light-theme" +msgstr "Cambiar a tema claro" + +msgid "workspace.header.menu.toggle-dark-theme" +msgstr "Cambiar a tema oscuro" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Restablecer" From a853314e3fa6008874112ca0a7209cf783d18a36 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 31 Jan 2024 13:09:43 +0100 Subject: [PATCH 12/37] :bug: Fix problem with text editor alignment --- .../main/ui/workspace/shapes/text/editor.cljs | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index 60e1e0ad91..df56144cb0 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.shapes.text.editor (:require ["draft-js" :as draft] + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -48,7 +49,7 @@ (let [children (obj/get props "children")] [:span {:style {:background "#ccc" :display "inline-block"}} children])) -(defn render-block +(defn- render-block [block shape] (let [type (ted/get-editor-block-type block)] (case type @@ -59,7 +60,7 @@ :shape shape}} nil))) -(defn styles-fn [shape styles content] +(defn- styles-fn [shape styles content] (let [data (if (= (.getText content) "") (-> (.getData content) (.toJS) @@ -73,19 +74,27 @@ (def empty-editor-state (ted/create-editor-state nil default-decorator)) -(defn get-blocks-to-setup [block-changes] +(defn- get-blocks-to-setup [block-changes] (->> block-changes (filter (fn [[_ v]] (nil? (:old v)))) (mapv first))) -(defn get-blocks-to-add-styles +(defn- get-blocks-to-add-styles [block-changes] (->> block-changes (filter (fn [[_ v]] (and (not= (:old v) (:new v)) (= (:old v) "")))) (mapv first))) +(defn- shape->justify + [{:keys [content]}] + (case (d/nilv (:vertical-align content) "top") + "center" "center" + "top" "flex-start" + "bottom" "flex-end" + nil)) + (mf/defc text-shape-edit-html {::mf/wrap [mf/memo] ::mf/wrap-props false @@ -247,7 +256,8 @@ :custom-style-fn (partial styles-fn shape) :block-renderer-fn #(render-block % shape) :ref on-editor - :editor-state state}]])) + :editor-state state + :style #js {:border "1px solid red"}}]])) (defn translate-point-from-viewport "Translate a point in the viewport into client coordinates" @@ -303,7 +313,19 @@ (dm/get-prop shape :height)) style - (cond-> #js {:pointer-events "all"} + (cond-> #js {:pointerEvents "all"} + + (not (cf/check-browser? :safari)) + (obj/merge! + #js {:transform (dm/fmt "translate(%px, %px)" (- (dm/get-prop shape :x) x) (- (dm/get-prop shape :y) y))}) + + (cf/check-browser? :safari-17) + (obj/merge! + #js {:height "100%" + :display "flex" + :flexDirection "column" + :justifyContent (shape->justify shape)}) + (cf/check-browser? :safari-16) (obj/merge! #js {:position "fixed" From cea096f06ccbb6906e20330dbc25b52148ca18a3 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 31 Jan 2024 14:19:08 +0100 Subject: [PATCH 13/37] :sparkles: Add debug renderer for grid-layout cells --- .../workspace/sidebar/debug_shape_info.cljs | 32 ++++++++++++++++--- .../workspace/sidebar/debug_shape_info.scss | 5 +++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs index 1213477f38..663f84b3e8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs @@ -61,7 +61,8 @@ :transform :matrix-render :transform-inverse :matrix-render :selrect :rect-render - :points :points-render}) + :points :points-render + :layout-grid-cells :cells-render}) (mf/defc shape-link [{:keys [id objects]}] @@ -69,6 +70,25 @@ :on-click #(st/emit! (dw/select-shape id))} (dm/str (dm/get-in objects [id :name]) " #" id)]) +(mf/defc cells-render + [{:keys [cells objects]}] + [:div {:class (stl/css :cells-render)} + (for [[id cell] cells] + + [:div {:key (dm/str "cell-" id) + :class (stl/css :cell-container)} + [:div {:class (stl/css :cell-position)} + (dm/fmt "(%, %) -> (%, %)" + (:row cell) + (:column cell) + (+ (:row cell) (dec (:row-span cell))) + (+ (:column cell) (dec (:column-span cell))))] + + [:div {:class (stl/css :cell-shape)} + (if (empty? (:shapes cell)) + [:div ""] + [:& shape-link {:id (first (:shapes cell)) :objects objects}])]])]) + (mf/defc debug-shape-attr [{:keys [attr value objects]}] @@ -79,7 +99,8 @@ :shape-list [:div {:class (stl/css :shape-list)} (for [id value] - [:& shape-link {:id id :objects objects}])] + [:& shape-link {:key (dm/str "child-" id) + :id id :objects objects}])] :matrix-render [:div (dm/str (gmt/format-precision value 2))] @@ -89,8 +110,11 @@ :points-render [:div {:class (stl/css :point-list)} - (for [point value] - [:div (dm/fmt "(%, %)" (:x point) (:y point))])] + (for [[idx point] (d/enumerate value)] + [:div {:key (dm/str "point-" idx)} (dm/fmt "(%, %)" (:x point) (:y point))])] + + :cells-render + [:& cells-render {:cells value :objects objects}] [:div {:class (stl/css :attrs-container-value)} (str value)])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss index ea6f438317..3b231ce612 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss @@ -97,3 +97,8 @@ display: flex; gap: $s-8; } + +.cell-container { + display: grid; + grid-template-columns: 100px 1fr; +} From ace890c8097fed33fceffd35c35bc229c2a761d2 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 31 Jan 2024 16:42:56 +0100 Subject: [PATCH 14/37] :bug: Fix problem when changing main component with grid elements --- common/src/app/common/types/shape_tree.cljc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index 064a3eae0f..0b22b1a848 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -400,9 +400,21 @@ :parent-id parent-id :frame-id frame-id) + :always + ;; Store in the meta the old id so we can do a remap afterwards + ;; in the parent + (with-meta {::old-id (:id shape)}) + (some? (:shapes shape)) (assoc :shapes (mapv :id new-direct-children))) + ;; For a GRID layout remap the cells shapes' old-id to the new id given in the clone + new-shape + (if (ctl/grid-layout? new-shape) + (let [ids-map (into {} (map #(vector (-> % meta ::old-id) (:id %))) new-children)] + (ctl/remap-grid-cells new-shape ids-map)) + new-shape) + new-shape (update-new-shape new-shape shape) new-shapes (into [new-shape] new-children) From 1de9171d500c7fb8a15be4824642d92115ee7045 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 29 Jan 2024 18:32:29 +0100 Subject: [PATCH 15/37] :sparkles: Add mask-type style parsing (react now supports it) --- common/src/app/common/svg.cljc | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/common/src/app/common/svg.cljc b/common/src/app/common/svg.cljc index 4ea6278cc9..92a500a6e9 100644 --- a/common/src/app/common/svg.cljc +++ b/common/src/app/common/svg.cljc @@ -517,10 +517,6 @@ :text :view}) -;; Props not supported by react we need to keep them lowercase -(def non-react-props - #{:mask-type}) - ;; Defaults for some tags per spec https://www.w3.org/TR/SVG11/single-page.html ;; they are basically the defaults that can be percents and we need to replace because ;; otherwise won't work as expected in the workspace @@ -622,10 +618,9 @@ res)) :else - (let [k (if (contains? non-react-props k) - k - (-> k d/name camelize keyword))] + (let [k (-> k d/name camelize keyword)] (assoc res k v))) + res)) {} attrs))) From 457feedec40dab8af3c43ccae290972fdc9ac08c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 31 Jan 2024 16:41:57 +0100 Subject: [PATCH 16/37] :bug: Fix many issues svg/attrs->props function --- common/src/app/common/svg.cljc | 182 ++++++++++--------- frontend/src/app/main/ui/shapes/svg_raw.cljs | 2 +- 2 files changed, 93 insertions(+), 91 deletions(-) diff --git a/common/src/app/common/svg.cljc b/common/src/app/common/svg.cljc index 92a500a6e9..6a486f5768 100644 --- a/common/src/app/common/svg.cljc +++ b/common/src/app/common/svg.cljc @@ -35,8 +35,18 @@ (def tags-to-remove #{:linearGradient :radialGradient :metadata :mask :clipPath :filter :title}) +(defn- camelize + [s] + (when (string? s) + (let [vendor? (str/starts-with? s "-") + result #?(:cljs (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", s) + :clj (str/camel s))] + (if ^boolean vendor? + (str/capital result) + result)))) + ;; https://www.w3.org/TR/SVG11/eltindex.html -(def svg-tags-list +(def svg-tags #{:a :altGlyph :altGlyphDef @@ -118,7 +128,7 @@ :vkern}) ;; https://www.w3.org/TR/SVG11/attindex.html -(def svg-attr-list +(def svg-attrs #{:accent-height :accumulate :additive @@ -212,26 +222,6 @@ :name :numOctaves :offset - ;; We don't support events - ;;:onabort - ;;:onactivate - ;;:onbegin - ;;:onclick - ;;:onend - ;;:onerror - ;;:onfocusin - ;;:onfocusout - ;;:onload - ;;:onmousedown - ;;:onmousemove - ;;:onmouseout - ;;:onmouseover - ;;:onmouseup - ;;:onrepeat - ;;:onresize - ;;:onscroll - ;;:onunload - ;;:onzoom :operator :order :orient @@ -336,7 +326,8 @@ :z :zoomAndPan}) -(def svg-present-list +(def svg-presentation-attrs + "A set of presentation SVG attributes as per SVG spec." #{:alignment-baseline :baseline-shift :clip-path @@ -399,52 +390,52 @@ :mask-type}) (def inheritable-props - [:style - :clip-rule - :color - :color-interpolation - :color-interpolation-filters - :color-profile - :color-rendering - :cursor - :direction - :dominant-baseline - :fill - :fill-opacity - :fill-rule - :font - :font-family - :font-size - :font-size-adjust - :font-stretch - :font-style - :font-variant - :font-weight - :glyph-orientation-horizontal - :glyph-orientation-vertical - :image-rendering - :letter-spacing - :marker - :marker-end - :marker-mid - :marker-start - :paint-order - :pointer-events - :shape-rendering - :stroke - :stroke-dasharray - :stroke-dashoffset - :stroke-linecap - :stroke-linejoin - :stroke-miterlimit - :stroke-opacity - :stroke-width - :text-anchor - :text-rendering - :transform - :visibility - :word-spacing - :writing-mode]) + #{:style + :clip-rule + :color + :color-interpolation + :color-interpolation-filters + :color-profile + :color-rendering + :cursor + :direction + :dominant-baseline + :fill + :fill-opacity + :fill-rule + :font + :font-family + :font-size + :font-size-adjust + :font-stretch + :font-style + :font-variant + :font-weight + :glyph-orientation-horizontal + :glyph-orientation-vertical + :image-rendering + :letter-spacing + :marker + :marker-end + :marker-mid + :marker-start + :paint-order + :pointer-events + :shape-rendering + :stroke + :stroke-dasharray + :stroke-dashoffset + :stroke-linecap + :stroke-linejoin + :stroke-miterlimit + :stroke-opacity + :stroke-width + :text-anchor + :text-rendering + :transform + :visibility + :word-spacing + :writing-mode}) (def gradient-tags #{:linearGradient @@ -517,6 +508,29 @@ :text :view}) +(defn prop-key + "Convert an attr key to a react compatible prop key. Returns nil if key is empty or invalid" + [k] + (let [kn (cond + (string? k) k + (keyword? k) (name k))] + (case kn + ("" nil) nil + "class" :className + "for" :htmlFor + (let [kn1 (subs kn 0 1)] + (if (= kn1 (str/upper kn1)) + (-> kn camelize str/capital keyword) + (-> kn camelize keyword)))))) + +(def svg-props + "A set of all attrs (including the presentation) converted to + camelCase for make it React compatible." + (let [xf (map prop-key)] + (-> #{} + (into xf svg-attrs) + (into xf svg-presentation-attrs)))) + ;; Defaults for some tags per spec https://www.w3.org/TR/SVG11/single-page.html ;; they are basically the defaults that can be percents and we need to replace because ;; otherwise won't work as expected in the workspace @@ -560,16 +574,6 @@ :else num-str)) -(defn- camelize - [s] - (when (string? s) - (let [vendor? (str/starts-with? s "-") - result #?(:cljs (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", s) - :clj (str/camel s))] - (if ^boolean vendor? - (str/capital result) - result)))) - (defn parse-style [style] (reduce (fn [res item] @@ -600,15 +604,13 @@ ([attrs whitelist?] (reduce-kv (fn [res k v] - (if (or (not whitelist?) - (contains? svg-attr-list k) - (contains? svg-present-list k)) + (let [k (prop-key k)] (cond - (nil? v) + (nil? k) res - (= k :class) - (assoc res :className v) + (nil? v) + res (= k :style) (let [v (if (string? v) (parse-style v) v) @@ -618,10 +620,10 @@ res)) :else - (let [k (-> k d/name camelize keyword)] - (assoc res k v))) - - res)) + (if (or (not whitelist?) (contains? svg-props k)) + (let [v (if (string? v) (str/trim v) v)] + (assoc res k v)) + res)))) {} attrs))) @@ -669,7 +671,7 @@ (let [remove-node? (fn [{:keys [tag]}] (and (some? tag) (or (contains? tags-to-remove tag) - (not (contains? svg-tags-list tag))))) + (not (contains? svg-tags tag))))) rec-result (->> (:content node) (map extract-defs)) node (assoc node :content (->> rec-result (map second) (filterv (comp not remove-node?)))) diff --git a/frontend/src/app/main/ui/shapes/svg_raw.cljs b/frontend/src/app/main/ui/shapes/svg_raw.cljs index c1b2f5d1be..49578d08d3 100644 --- a/frontend/src/app/main/ui/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/shapes/svg_raw.cljs @@ -104,7 +104,7 @@ svg-root? (and (map? content) (= tag :svg)) svg-tag? (map? content) svg-leaf? (string? content) - valid-tag? (contains? csvg/svg-tags-list tag)] + valid-tag? (contains? csvg/svg-tags tag)] (cond ^boolean svg-root? From e474accb610f366cac631df07143ad44333ab4d4 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 31 Jan 2024 13:04:21 +0100 Subject: [PATCH 17/37] :bug: Fix problem with components thumbnails single column --- .../src/app/main/ui/workspace/sidebar/assets/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss index 79fe504da0..1b2789457a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss @@ -11,7 +11,7 @@ } .asset-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax($s-112, 1fr)); + grid-template-columns: repeat(auto-fill, minmax($s-96, 1fr)); grid-auto-rows: $s-112; max-width: 100%; gap: $s-4; From 188f5c616716b6206bbe91db03a2b1f350f299d9 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 31 Jan 2024 13:05:02 +0100 Subject: [PATCH 18/37] :bug: Fix problem with snap points --- common/src/app/common/geom/snap.cljc | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/common/src/app/common/geom/snap.cljc b/common/src/app/common/geom/snap.cljc index ff35d16743..a2cffe09f1 100644 --- a/common/src/app/common/geom/snap.cljc +++ b/common/src/app/common/geom/snap.cljc @@ -15,15 +15,16 @@ (defn rect->snap-points [rect] - (let [x (dm/get-prop rect :x) - y (dm/get-prop rect :y) - w (dm/get-prop rect :width) - h (dm/get-prop rect :height)] - #{(gpt/point x y) - (gpt/point (+ x w) y) - (gpt/point (+ x w) (+ y h)) - (gpt/point x (+ y h)) - (grc/rect->center rect)})) + (when (some? rect) + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + #{(gpt/point x y) + (gpt/point (+ x w) y) + (gpt/point (+ x w) (+ y h)) + (gpt/point x (+ y h)) + (grc/rect->center rect)}))) (defn- frame->snap-points [frame] From 334d1fd9b3f8aa66ced59773e5836aeb5b844812 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 31 Jan 2024 17:55:53 +0100 Subject: [PATCH 19/37] :bug: Change order of contraints options panel --- .../main/ui/workspace/sidebar/options/shapes/bool.cljs | 8 ++++---- .../ui/workspace/sidebar/options/shapes/circle.cljs | 8 ++++---- .../ui/workspace/sidebar/options/shapes/frame.cljs | 7 +++---- .../ui/workspace/sidebar/options/shapes/group.cljs | 6 +++--- .../ui/workspace/sidebar/options/shapes/image.cljs | 8 ++++---- .../ui/workspace/sidebar/options/shapes/multiple.cljs | 6 +++--- .../main/ui/workspace/sidebar/options/shapes/path.cljs | 8 ++++---- .../main/ui/workspace/sidebar/options/shapes/rect.cljs | 8 ++++---- .../ui/workspace/sidebar/options/shapes/svg_raw.cljs | 8 ++++---- .../main/ui/workspace/sidebar/options/shapes/text.cljs | 10 +++++----- 10 files changed, 38 insertions(+), 39 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs index 4b5e528467..4cf2ef29fd 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs @@ -56,10 +56,6 @@ :values measure-values :shape shape}] - (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) - [:& constraints-menu {:ids ids - :values constraint-values}]) - [:& layout-container-menu {:type type :ids [(:id shape)] @@ -81,6 +77,10 @@ :is-grid-parent? is-grid-parent? :shape shape}]) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) + [:& constraints-menu {:ids ids + :values constraint-values}]) + [:& fill-menu {:ids ids :type type :values (select-keys shape fill-attrs)}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs index d37d6a4de8..6bc6394b45 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs @@ -58,10 +58,6 @@ :values measure-values :shape shape}] - (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) - [:& constraints-menu {:ids ids - :values constraint-values}]) - [:& layout-container-menu {:type type :ids [(:id shape)] @@ -83,6 +79,10 @@ :is-grid-parent? is-grid-parent? :shape shape}]) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) + [:& constraints-menu {:ids ids + :values constraint-values}]) + [:& fill-menu {:ids ids :type type :values (select-keys shape fill-attrs)}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index e792751a2b..1332faedea 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -65,10 +65,6 @@ [:& component-menu {:shapes [shape]}] - (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) - [:& constraints-menu {:ids ids - :values constraint-values}]) - [:& layout-container-menu {:type type :ids [(:id shape)] @@ -91,6 +87,9 @@ :is-layout-container? is-layout-container? :shape shape}]) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) + [:& constraints-menu {:ids ids + :values constraint-values}]) [:& fill-menu {:ids ids :type type diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs index 558e86b416..a892a87e0d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs @@ -72,9 +72,6 @@ [:& measures-menu {:type type :ids measure-ids :values measure-values :shape shape}] [:& component-menu {:shapes [shape]}] ;;remove this in components-v2 - (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) - [:& constraints-menu {:ids constraint-ids :values constraint-values}]) - [:& layout-container-menu {:type type :ids [(:id shape)] @@ -96,6 +93,9 @@ :is-grid-parent? is-grid-parent? :values layout-item-values}]) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) + [:& constraints-menu {:ids constraint-ids :values constraint-values}]) + (when-not (empty? fill-ids) [:& fill-menu {:type type :ids fill-ids :values fill-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs index 6ae27dc027..a05e465757 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs @@ -58,10 +58,6 @@ :values measure-values :shape shape}] - (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) - [:& constraints-menu {:ids ids - :values constraint-values}]) - [:& layout-container-menu {:type type :ids [(:id shape)] @@ -83,6 +79,10 @@ :is-grid-parent? is-grid-parent? :shape shape}]) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) + [:& constraints-menu {:ids ids + :values constraint-values}]) + [:& fill-menu {:ids ids :type type :values fill-values}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index f669bc4d9d..af81ea6528 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -364,9 +364,6 @@ (when-not (empty? components) [:& component-menu {:shapes components}]) - (when-not (or (empty? constraint-ids) ^boolean is-layout-child?) - [:& constraints-menu {:ids constraint-ids :values constraint-values}]) - [:& layout-container-menu {:type type :ids layout-container-ids @@ -383,6 +380,9 @@ :is-grid-parent? is-grid-parent? :values layout-item-values}]) + (when-not (or (empty? constraint-ids) ^boolean is-layout-child?) + [:& constraints-menu {:ids constraint-ids :values constraint-values}]) + (when-not (empty? text-ids) [:& ot/text-menu {:type type :ids text-ids :values text-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs index b803916beb..db0aba0094 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs @@ -57,10 +57,6 @@ :values measure-values :shape shape}] - (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) - [:& constraints-menu {:ids ids - :values constraint-values}]) - [:& layout-container-menu {:type type :ids [(:id shape)] @@ -82,6 +78,10 @@ :is-grid-parent? is-grid-parent? :shape shape}]) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) + [:& constraints-menu {:ids ids + :values constraint-values}]) + [:& fill-menu {:ids ids :type type :values (select-keys shape fill-attrs)}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs index aed851d0f0..b7406ddca0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs @@ -60,10 +60,6 @@ :values measure-values :shape shape}] - (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) - [:& constraints-menu {:ids ids - :values constraint-values}]) - [:& layout-container-menu {:type type :ids ids @@ -85,6 +81,10 @@ :is-grid-parent? is-grid-parent? :shape shape}]) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) + [:& constraints-menu {:ids ids + :values constraint-values}]) + [:& fill-menu {:ids ids :type type :values fill-values}] 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 87a2620e2c..bf13cf76c8 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 @@ -129,10 +129,6 @@ :values measure-values :shape shape}] - (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) - [:& constraints-menu {:ids ids - :values constraint-values}]) - [:& layout-container-menu {:type type :ids [(:id shape)] @@ -154,6 +150,10 @@ :is-grid-parent? is-grid-parent? :shape shape}]) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) + [:& constraints-menu {:ids ids + :values constraint-values}]) + [:& fill-menu {:ids ids :type type :values fill-values}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 955db7652b..a3b9f3c4e0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -93,11 +93,6 @@ :values (select-keys shape measure-attrs) :shape shape}] - (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) - [:& constraints-menu - {:ids ids - :values (select-keys shape constraint-attrs)}]) - [:& layout-container-menu {:type type :ids [(:id shape)] @@ -119,6 +114,11 @@ :is-grid-parent? is-grid-parent? :shape shape}]) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) + [:& constraints-menu + {:ids ids + :values (select-keys shape constraint-attrs)}]) + [:& text-menu {:ids ids :type type From 497b5815769258724e3b9bbeb94990a3884a5500 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 30 Jan 2024 14:55:55 +0100 Subject: [PATCH 20/37] :sparkles: Change drag component to instantiate on enter the viewport --- .../app/main/data/workspace/libraries.cljs | 57 +++++++++-------- .../app/main/data/workspace/transforms.cljs | 2 +- frontend/src/app/main/ui/hooks.cljs | 9 +-- .../workspace/sidebar/assets/components.cljs | 26 +++++--- .../sidebar/options/menus/component.cljs | 4 +- .../src/app/main/ui/workspace/viewport.cljs | 8 ++- .../main/ui/workspace/viewport/actions.cljs | 61 ++++++++++++++----- frontend/src/app/util/dom/dnd.cljs | 19 ++++-- frontend/src/app/util/rxops.cljs | 2 +- 9 files changed, 121 insertions(+), 67 deletions(-) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 14669414aa..59864afbbc 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -36,6 +36,7 @@ [app.main.data.workspace.specialized-panel :as dwsp] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] + [app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.undo :as dwu] [app.main.features :as features] [app.main.features.pointer-map :as fpmap] @@ -534,34 +535,38 @@ (defn instantiate-component "Create a new shape in the current page, from the component with the given id in the given file library. Then selects the newly created instance." - [file-id component-id position] - (dm/assert! (uuid? file-id)) - (dm/assert! (uuid? component-id)) - (dm/assert! (gpt/point? position)) - (ptk/reify ::instantiate-component - ptk/WatchEvent - (watch [it state _] - (let [page (wsh/lookup-page state) - libraries (wsh/get-libraries state) + ([file-id component-id position] + (instantiate-component file-id component-id position nil)) + ([file-id component-id position {:keys [start-move? initial-point]}] + (dm/assert! (uuid? file-id)) + (dm/assert! (uuid? component-id)) + (dm/assert! (gpt/point? position)) + (ptk/reify ::instantiate-component + ptk/WatchEvent + (watch [it state _] + (let [page (wsh/lookup-page state) + libraries (wsh/get-libraries state) - objects (:objects page) - changes (-> (pcb/empty-changes it (:id page)) - (pcb/with-objects objects)) + objects (:objects page) + changes (-> (pcb/empty-changes it (:id page)) + (pcb/with-objects objects)) - [new-shape changes] - (dwlh/generate-instantiate-component changes - objects - file-id - component-id - position - page - libraries) - undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (ptk/data-event :layout/update [(:id new-shape)]) - (dws/select-shapes (d/ordered-set (:id new-shape))) - (dwu/commit-undo-transaction undo-id)))))) + [new-shape changes] + (dwlh/generate-instantiate-component changes + objects + file-id + component-id + position + page + libraries) + undo-id (js/Symbol)] + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (ptk/data-event :layout/update [(:id new-shape)]) + (dws/select-shapes (d/ordered-set (:id new-shape))) + (when start-move? + (dwtr/start-move initial-point #{(:id new-shape)})) + (dwu/commit-undo-transaction undo-id))))))) (defn detach-component "Remove all references to components in the shape with the given id, diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index dda92054b9..f47b71b609 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -496,7 +496,7 @@ (when-let [node (dom/get-element-by-class "ghost-outline")] (dom/set-property! node "transform" (gmt/translate-matrix move-vector)))))) -(defn- start-move +(defn start-move ([from-position] (start-move from-position nil)) ([from-position ids] (ptk/reify ::start-move diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index d97fdad92f..944d32ac7b 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -50,13 +50,6 @@ (fn [] (st/emit! (dsc/pop-shortcuts key)))))) -(defn invisible-image - [] - (let [img (js/Image.) - imd "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="] - (set! (.-src img) imd) - img)) - (defn- set-timer [state ms func] (assoc state :timer (ts/schedule ms func))) @@ -128,7 +121,7 @@ (do (dom/stop-propagation event) (dnd/set-data! event data-type data) - (dnd/set-drag-image! event (invisible-image)) + (dnd/set-drag-image! event (dnd/invisible-image)) (dnd/set-allowed-effect! event "move") (when (fn? on-drag) (on-drag data))))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs index 3175edcf99..54d9e2d333 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs @@ -472,13 +472,26 @@ (mf/use-fn (mf/deps file-id) (fn [component event] - ;; dnd api only allow to acces to the dataTransfer data on on-drop (https://html.spec.whatwg.org/dev/dnd.html#concept-dnd-p) - ;; We need to know if the dragged element is from the local library on on-drag-enter, so we need to keep the info elsewhere - (set-drag-data! {:local? local?}) - (dnd/set-data! event "penpot/component" {:file-id file-id - :component component}) - (dnd/set-allowed-effect! event "move"))) + (let [file-data + (d/nilv (dm/get-in @refs/workspace-libraries [file-id :data]) @refs/workspace-data) + + shape-main + (ctf/get-component-root file-data component)] + + ;; dnd api only allow to acces to the dataTransfer data on on-drop (https://html.spec.whatwg.org/dev/dnd.html#concept-dnd-p) + ;; We need to know if the dragged element is from the local library on on-drag-enter, so we need to keep the info elsewhere + (set-drag-data! {:file-id file-id + :component component + :shape shape-main + :local? local?}) + + (dnd/set-data! event "penpot/component" true) + + ;; Remove the ghost image for componentes because we're going to instantiate it on the viewport + (dnd/set-drag-image! event (dnd/invisible-image)) + + (dnd/set-allowed-effect! event "move")))) on-show-main (mf/use-fn @@ -569,4 +582,3 @@ {:option-name (tr "workspace.shape.menu.show-main") :id "assets-show-main-component" :option-handler on-show-main})]}]]])) - diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index ac499058f9..8e4e04d4d4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -84,7 +84,7 @@ (dom/focus! textarea)))) on-delete-annotation (mf/use-callback - (mf/deps shape) + (mf/deps (:id shape)) (fn [event] (dom/stop-propagation event) (st/emit! (modal/show @@ -98,7 +98,7 @@ (dw/update-component-annotation component-id nil)))}))))] (mf/use-effect - (mf/deps shape) + (mf/deps (:id shape)) (fn [] (initialize) (when (and (not creating?) (:id-for-create workspace-annotations)) ;; cleanup set-annotations-id-for-create if we aren't on the marked component diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 41cb816869..181e5f5da6 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -174,9 +174,12 @@ on-click (actions/on-click hover selected edition drawing-path? drawing-tool space? selrect z?) on-context-menu (actions/on-context-menu hover hover-ids workspace-read-only?) on-double-click (actions/on-double-click hover hover-ids hover-top-frame-id drawing-path? base-objects edition drawing-tool z? workspace-read-only?) - on-drag-enter (actions/on-drag-enter) + + comp-inst-ref (mf/use-ref false) + on-drag-enter (actions/on-drag-enter comp-inst-ref) on-drag-over (actions/on-drag-over move-stream) - on-drop (actions/on-drop file) + on-drag-end (actions/on-drag-over comp-inst-ref) + on-drop (actions/on-drop file comp-inst-ref) on-pointer-down (actions/on-pointer-down @hover selected edition drawing-tool text-editing? node-editing? grid-editing? drawing-path? create-comment? space? panning z? workspace-read-only?) @@ -365,6 +368,7 @@ :on-double-click on-double-click :on-drag-enter on-drag-enter :on-drag-over on-drag-over + :on-drag-end on-drag-end :on-drop on-drop :on-pointer-down on-pointer-down :on-pointer-enter on-pointer-enter diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 30fa24840f..ca982acd7a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -21,6 +21,7 @@ [app.main.data.workspace.specialized-panel :as-alias dwsp] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.workspace.sidebar.assets.components :as wsac] [app.main.ui.workspace.viewport.viewport-ref :as uwvv] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] @@ -28,7 +29,8 @@ [app.util.keyboard :as kbd] [app.util.mouse :as mse] [app.util.object :as obj] - [app.util.timers :as timers] + [app.util.rxops :refer [throttle-fn]] + [app.util.timers :as ts] [app.util.webapi :as wapi] [beicon.v2.core :as rx] [cuerdas.core :as str] @@ -216,7 +218,7 @@ (st/emit! (mse/->MouseEvent :double-click ctrl? shift? alt? meta?)) ;; Emit asynchronously so the double click to exit shapes won't break - (timers/schedule + (ts/schedule (fn [] (when (and (not drawing-path?) shape) (cond @@ -244,7 +246,7 @@ workspace-read-only?) (let [position (dom/get-client-position event)] ;; Delayed callback because we need to wait to the previous context menu to be closed - (timers/schedule + (ts/schedule #(st/emit! (if (some? @hover) (dw/show-shape-context-menu {:position position @@ -290,7 +292,7 @@ ;; We store this so in Firefox the middle button won't do a paste of the content (reset! disable-paste true) - (timers/schedule #(reset! disable-paste false))) + (ts/schedule #(reset! disable-paste false))) (st/emit! (dw/finish-panning) (dw/finish-zooming)))))) @@ -400,9 +402,28 @@ (st/emit! (dw/update-viewport-position {:x #(+ % (/ delta-x zoom)) :y #(+ % (/ delta-y zoom))})))))))))) -(defn on-drag-enter [] +(defn on-drag-enter + [comp-inst-ref] (mf/use-callback (fn [e] + (let [component-inst? (mf/ref-val comp-inst-ref)] + (when (and (dnd/has-type? e "penpot/component") + (dom/class? (dom/get-target e) "viewport-controls") + (not component-inst?)) + (let [point (gpt/point (.-clientX e) (.-clientY e)) + viewport-coord (uwvv/point->viewport point) + {:keys [component file-id shape]} @wsac/drag-data* + + ;; shape (get-in component [:objects (:id component)]) + final-x (- (:x viewport-coord) (/ (:width shape) 2)) + final-y (- (:y viewport-coord) (/ (:height shape) 2))] + + (mf/set-ref-val! comp-inst-ref true) + (st/emit! (dwl/instantiate-component + file-id + (:id component) + (gpt/point final-x final-y) + {:start-move? true :initial-point viewport-coord}))))) (when (or (dnd/has-type? e "penpot/shape") (dnd/has-type? e "penpot/component") (dnd/has-type? e "Files") @@ -410,8 +431,19 @@ (dnd/has-type? e "text/asset-id")) (dom/prevent-default e))))) +(defn on-drag-end + [comp-inst-ref] + (mf/use-callback + (fn [] + (mf/set-ref-val! comp-inst-ref false)))) + (defn on-drag-over [move-stream] - (let [on-pointer-move (on-pointer-move move-stream)] + (let [on-pointer-move (on-pointer-move move-stream) + + ;; Drag-over is not the same as pointer-move. Drag over is fired less frequently so we need + ;; to create a throttle so the events that cannot be processed at a certain path are + ;; discarded. + on-pointer-move (throttle-fn 50 (fn [e] (ts/raf #(on-pointer-move e))))] (mf/use-callback (fn [e] (when (or (dnd/has-type? e "penpot/shape") @@ -423,7 +455,7 @@ (dom/prevent-default e)))))) (defn on-drop - [file] + [file comp-inst-ref] (mf/use-fn (fn [event] (dom/prevent-default event) @@ -443,13 +475,13 @@ (assoc :y final-y))))) (dnd/has-type? event "penpot/component") - (let [{:keys [component file-id]} (dnd/get-data event "penpot/component") - shape (get-in component [:objects (:id component)]) - final-x (- (:x viewport-coord) (/ (:width shape) 2)) - final-y (- (:y viewport-coord) (/ (:height shape) 2))] - (st/emit! (dwl/instantiate-component file-id - (:id component) - (gpt/point final-x final-y)))) + (let [event (.-nativeEvent event) + ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + alt? (kbd/alt? event) + meta? (kbd/meta? event)] + (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)) + (mf/set-ref-val! comp-inst-ref false)) ;; Will trigger when the user drags an image from a browser ;; to the viewport (firefox and chrome do it a bit different @@ -517,4 +549,3 @@ (not @disable-paste) (not workspace-read-only?)) (st/emit! (dw/paste-from-event event @in-viewport?))))))) - diff --git a/frontend/src/app/util/dom/dnd.cljs b/frontend/src/app/util/dom/dnd.cljs index 5ea91a22a9..0f29caab50 100644 --- a/frontend/src/app/util/dom/dnd.cljs +++ b/frontend/src/app/util/dom/dnd.cljs @@ -62,6 +62,13 @@ (.setData dt data-type data)) e))) +(defn invisible-image + [] + (let [img (js/Image.) + imd "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="] + (set! (.-src img) imd) + img)) + (defn set-drag-image! ([e image] (set-drag-image! e image 0 0)) @@ -108,11 +115,13 @@ ([e] (get-data e "penpot/data")) ([e data-type] - (let [dt (.-dataTransfer e)] - (if (or (str/starts-with? data-type "penpot") - (= data-type "application/json")) - (t/decode-str (.getData dt data-type)) - (.getData dt data-type))))) + (let [dt (.-dataTransfer e) + data (.getData dt data-type)] + (cond-> data + (and (some? data) (not= data "") + (or (str/starts-with? data-type "penpot") + (= data-type "application/json"))) + (t/decode-str))))) (defn get-files [e] diff --git a/frontend/src/app/util/rxops.cljs b/frontend/src/app/util/rxops.cljs index 0b08a23aeb..05732f3d03 100644 --- a/frontend/src/app/util/rxops.cljs +++ b/frontend/src/app/util/rxops.cljs @@ -8,7 +8,7 @@ (:require [beicon.v2.core :as rx])) -(defn- throttle-fn +(defn throttle-fn [delay f] (let [state #js {:lastExecTime 0 From b0d723282b6e6048bebe6648af5012d2597f1fa4 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 1 Feb 2024 10:32:44 +0100 Subject: [PATCH 21/37] :bug: Fix problem when export not getting new change --- common/src/app/common/geom/shapes/bounds.cljc | 2 ++ frontend/src/app/main/data/exports.cljs | 8 ++++++++ frontend/src/app/main/refs.cljs | 3 +++ frontend/src/app/main/ui/components/copy_button.scss | 1 + 4 files changed, 14 insertions(+) diff --git a/common/src/app/common/geom/shapes/bounds.cljc b/common/src/app/common/geom/shapes/bounds.cljc index bb14ea3458..c60840d7a5 100644 --- a/common/src/app/common/geom/shapes/bounds.cljc +++ b/common/src/app/common/geom/shapes/bounds.cljc @@ -111,6 +111,7 @@ shadow-width (->> (:shadow shape) + (remove :hidden) (map #(case (:style % :drop-shadow) :drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10) 0)) @@ -118,6 +119,7 @@ shadow-height (->> (:shadow shape) + (remove :hidden) (map #(case (:style % :drop-shadow) :drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10) 0)) diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs index 675f1d4d96..23eb509563 100644 --- a/frontend/src/app/main/data/exports.cljs +++ b/frontend/src/app/main/data/exports.cljs @@ -10,6 +10,7 @@ [app.main.data.modal :as modal] [app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.state-helpers :as wsh] + [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.util.dom :as dom] @@ -166,6 +167,13 @@ :wait true}] (rx/concat (rx/of ::dwp/force-persist) + + ;; Wait the persist to be succesfull + (->> (rx/from-atom refs/persistence-state {:emit-current-value? true}) + (rx/filter #(or (nil? %) (= :saved %))) + (rx/first) + (rx/timeout 400 (rx/empty))) + (->> (rp/cmd! :export params) (rx/mapcat (fn [{:keys [id filename]}] (->> (rp/cmd! :export {:cmd :get-resource :blob? true :id id}) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 1a63a5032b..434bca57e9 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -591,3 +591,6 @@ (def updating-library (l/derived :updating-library st/state)) + +(def persistence-state + (l/derived (comp :status :workspace-persistence) st/state)) diff --git a/frontend/src/app/main/ui/components/copy_button.scss b/frontend/src/app/main/ui/components/copy_button.scss index 1e07f6455c..c81487f458 100644 --- a/frontend/src/app/main/ui/components/copy_button.scss +++ b/frontend/src/app/main/ui/components/copy_button.scss @@ -57,6 +57,7 @@ width: 100%; height: fit-content; text-align: left; + border: 1px solid transparent; .icon-btn { position: absolute; display: flex; From f1768c5a07f46d7da039b0f18a7043271b6c01ff Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 1 Feb 2024 11:32:35 +0100 Subject: [PATCH 22/37] :bug: Fix problems with inspect and texts --- common/src/app/common/text.cljc | 6 +++++- .../app/main/ui/viewer/inspect/attributes/common.cljs | 9 +++------ .../app/main/ui/viewer/inspect/attributes/common.scss | 10 +++++++++- .../main/ui/viewer/inspect/attributes/geometry.cljs | 6 ++++-- .../app/main/ui/viewer/inspect/attributes/text.scss | 3 +-- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/common/src/app/common/text.cljc b/common/src/app/common/text.cljc index b5b12c5d5a..f8297843dc 100644 --- a/common/src/app/common/text.cljc +++ b/common/src/app/common/text.cljc @@ -53,6 +53,9 @@ (def text-transform-attrs [:text-transform]) +(def text-fills + [:fills]) + (def shape-attrs [:grow-type]) @@ -70,7 +73,8 @@ text-font-attrs text-spacing-attrs text-decoration-attrs - text-transform-attrs)) + text-transform-attrs + text-fills)) (def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs)) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs index 1fa8103403..76001fe464 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs @@ -79,11 +79,9 @@ [:span {:class (stl/css-case :color-value-wrapper true :gradient-name (:gradient color))} (if (:gradient color) - [:& cbn/color-name {:color color - :size 80}] + [:& cbn/color-name {:color color :size 80}] (case format - :hex [:& cbn/color-name {:color color - :size 80}] + :hex [:& cbn/color-name {:color color}] :rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))] [:* (str/fmt "%s, %s, %s, %s" r g b a)]) :hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color)) @@ -105,8 +103,7 @@ [:span {:class (stl/css-case :color-value-wrapper true :gradient-name (:gradient color))} (if (:gradient color) - [:& cbn/color-name {:color color - :size 80}] + [:& cbn/color-name {:color color}] (case format :hex [:& cbn/color-name {:color color :size 80}] diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss index bb33fa99c4..f05e0faa40 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss @@ -58,6 +58,11 @@ } } +.name-opacity { + display: flex; + align-items: baseline; +} + .color-name-wrapper { @include titleTipography; @include flexColumn; @@ -113,8 +118,11 @@ .color-value-wrapper { @include inspectValue; text-transform: uppercase; - max-width: $s-124; + max-width: $s-80; padding-right: $s-8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; &.gradient-name { text-transform: none; } diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/geometry.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/geometry.cljs index 58e39ca5f8..4735502f76 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/geometry.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/geometry.cljs @@ -8,6 +8,7 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.ui.components.copy-button :refer [copy-button]] [app.main.ui.components.title-bar :refer [title-bar]] [app.util.code-gen.style-css :as css] @@ -19,9 +20,10 @@ (mf/defc geometry-block [{:keys [objects shape]}] [:* - (for [property properties] + (for [[idx property] (d/enumerate properties)] (when-let [value (css/get-css-value objects shape property)] - [:div {:class (stl/css :geometry-row)} + [:div {:key (dm/str "block-" idx "-" (d/name property)) + :class (stl/css :geometry-row)} [:div {:class (stl/css :global/attr-label)} (d/name property)] [:div {:class (stl/css :global/attr-value)} [:& copy-button {:data (css/get-css-property objects shape property)} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/text.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/text.scss index 9f111adad4..a506af1b4a 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/text.scss +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/text.scss @@ -30,7 +30,6 @@ } .attributes-content-row { - width: $s-252; max-width: $s-252; min-height: calc($s-2 + $s-32); border-radius: $br-8; @@ -39,7 +38,7 @@ .content { @include titleTipography; width: 100%; - padding: 0; + padding: $s-4 0; color: var(--color-foreground-secondary); } From 0b3cff1a9f32f2633533bc81d315049d67e568ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 1 Feb 2024 14:29:08 +0100 Subject: [PATCH 23/37] :bug: Fix spacing in Design tab / Text options --- .../main/ui/workspace/sidebar/options/menus/typography.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss index 85a8222c41..420a35dcd0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss @@ -247,14 +247,13 @@ .text-options { @include flexColumn; - margin-bottom: $s-8; &:not(.text-options-full-size) { position: relative; } .font-option { @include titleTipography; @extend .asset-element; - padding-right: 0; + padding: $s-8 0 $s-8 $s-8; cursor: pointer; .name { flex-grow: 1; @@ -292,6 +291,7 @@ padding: 0; .numeric-input { @extend .input-base; + padding-inline-start: $s-8; } } From 669d928bbf41ae824914facd948d9fa03ae6c4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 1 Feb 2024 15:09:04 +0100 Subject: [PATCH 24/37] :bug: Fix font-selector not autofocusing and remove its inner drop shadow --- frontend/src/app/main/ui/components/search_bar.cljs | 2 ++ frontend/src/app/main/ui/components/search_bar.scss | 1 + .../app/main/ui/workspace/sidebar/options/menus/typography.cljs | 1 + .../app/main/ui/workspace/sidebar/options/menus/typography.scss | 1 - 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/components/search_bar.cljs b/frontend/src/app/main/ui/components/search_bar.cljs index 24f362cd26..f6f70a8b91 100644 --- a/frontend/src/app/main/ui/components/search_bar.cljs +++ b/frontend/src/app/main/ui/components/search_bar.cljs @@ -21,6 +21,7 @@ on-clear (unchecked-get props "clear-action") placeholder (unchecked-get props "placeholder") icon (unchecked-get props "icon") + autofocus (unchecked-get props "auto-focus") handle-change (mf/use-fn @@ -52,6 +53,7 @@ icon [:input {:on-change handle-change :value value + :auto-focus autofocus :placeholder placeholder :on-key-down handle-key-down}] (when (not= "" value) diff --git a/frontend/src/app/main/ui/components/search_bar.scss b/frontend/src/app/main/ui/components/search_bar.scss index ae5cbe1e1c..b67317ba21 100644 --- a/frontend/src/app/main/ui/components/search_bar.scss +++ b/frontend/src/app/main/ui/components/search_bar.scss @@ -30,6 +30,7 @@ background-color: var(--input-background-color); font-size: $fs-12; color: var(--input-foreground-color); + border-radius: $br-8; &:focus { outline: none; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 1f9d39d1b8..f637274cb4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -176,6 +176,7 @@ [:div {:class (stl/css :header)} [:& search-bar {:on-change on-filter-change :value (:term @state) + :auto-focus true :placeholder (tr "workspace.options.search-font")}] (when (and recent-fonts show-recent) [:section {:class (stl/css :show-recent)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss index 420a35dcd0..2b5d617ef3 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss @@ -418,7 +418,6 @@ } .fonts-list { - @include menuShadow; position: relative; display: flex; flex-direction: column; From 229825237979ff8088ed4d3e1d9421796687030f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 1 Feb 2024 16:01:57 +0100 Subject: [PATCH 25/37] :bug: Fix font-selector current font tick being misaligned in full size dropdown --- .../sidebar/options/menus/typography.scss | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss index 2b5d617ef3..f69ab2804f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss @@ -375,39 +375,40 @@ padding: $s-12; } } +} - .font-wrapper { - padding-bottom: $s-4; - cursor: pointer; - .font-item { - @extend .asset-element; - margin-bottom: $s-4; - border-radius: $br-8; - display: flex; - .icon { - @include flexCenter; - height: $s-28; - width: $s-28; - svg { - @extend .button-icon-small; - stroke: var(--icon-foreground); - } - } - &.selected { - color: var(--assets-item-name-foreground-color-hover); - .icon { - svg { - stroke: var(--assets-item-name-foreground-color-hover); - } - } - } +.font-wrapper { + padding-bottom: $s-4; + cursor: pointer; +} - .label { - @include titleTipography; - flex-grow: 1; +.font-item { + @extend .asset-element; + margin-bottom: $s-4; + border-radius: $br-8; + display: flex; + .icon { + @include flexCenter; + height: $s-28; + width: $s-28; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + &.selected { + color: var(--assets-item-name-foreground-color-hover); + .icon { + svg { + stroke: var(--assets-item-name-foreground-color-hover); } } } + + .label { + @include titleTipography; + flex-grow: 1; + } } .font-selector-dropdown-full-size { @@ -438,4 +439,10 @@ border-start-start-radius: 0; border-start-end-radius: 0; border: $s-1 solid var(--color-background-quaternary); + + // TODO: this should belong to typography-entry , but atm we don't have a clear + // way of accessing whether we are in fullsize mode or not + .selected { + padding-inline-end: 0; + } } From a5239c1cb69642bcff7d917fd8289e26b6a3d249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 1 Feb 2024 16:14:27 +0100 Subject: [PATCH 26/37] :bug: Fix bad background for new team button in light theme --- frontend/resources/styles/common/refactor/design-tokens.scss | 4 ++++ frontend/src/app/main/ui/dashboard/sidebar.scss | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/resources/styles/common/refactor/design-tokens.scss b/frontend/resources/styles/common/refactor/design-tokens.scss index 45c677671a..9b8d5bfffd 100644 --- a/frontend/resources/styles/common/refactor/design-tokens.scss +++ b/frontend/resources/styles/common/refactor/design-tokens.scss @@ -361,6 +361,10 @@ // TEXT SELECTION --text-editor-selection-background-color: var(--da-tertiary-70); --text-editor-selection-foreground-color: var(--app-white); + + // NEW TEAM BUTTON + // TODO: we should not put these functional tokens here, but rather in the components they belong to + --new-team-button-background-color: var(--color-background-primary); } #app { diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index 27b31d569b..7a7f69a7db 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -232,13 +232,10 @@ width: $s-168; } - .new-team { - background-color: $db-quaternary; - } - &.action { .team-icon { background-color: #2e3434; + background-color: var(--new-team-button-background-color); border-radius: 50%; height: $s-24; margin-right: $s-12; From 7fa47d68a8fce8ed21203ab9fb125ea79fab0e1e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 1 Feb 2024 16:30:54 +0100 Subject: [PATCH 27/37] :bug: Fix problems with text gradients --- .../app/common/geom/shapes/transforms.cljc | 37 +++++++------ common/src/app/common/text.cljc | 22 +++++++- frontend/src/app/main/ui/shapes/fills.cljs | 52 ++++++++++++++----- .../src/app/main/ui/shapes/text/styles.cljs | 2 +- .../main/ui/workspace/viewport/gradients.cljs | 28 +++++----- 5 files changed, 95 insertions(+), 46 deletions(-) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index f9be02adaf..f6ee1ea00c 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -140,6 +140,28 @@ (gmt/translate (gpt/negate shape-center))))) +(defn inverse-transform-matrix + ([shape] + (inverse-transform-matrix shape nil)) + + ([shape params] + (inverse-transform-matrix shape params (or (gco/shape->center shape) (gpt/point 0 0)))) + + ([{:keys [flip-x flip-y transform-inverse] :as shape} {:keys [no-flip]} shape-center] + (-> (gmt/matrix) + (gmt/translate shape-center) + + (cond-> (and flip-x no-flip) + (gmt/scale (gpt/point -1 1))) + + (cond-> (and flip-y no-flip) + (gmt/scale (gpt/point 1 -1))) + + (cond-> (some? transform-inverse) + (gmt/multiply transform-inverse)) + + (gmt/translate (gpt/negate shape-center))))) + (defn transform-str ([shape] (transform-str shape nil)) @@ -152,21 +174,6 @@ (dm/str (transform-matrix shape params)) ""))) -;; FIXME: performance -(defn inverse-transform-matrix - ([shape] - (let [shape-center (or (gco/shape->center shape) - (gpt/point 0 0))] - (inverse-transform-matrix shape shape-center))) - ([{:keys [flip-x flip-y] :as shape} center] - (-> (gmt/matrix) - (gmt/translate center) - (cond-> - flip-x (gmt/scale (gpt/point -1 1)) - flip-y (gmt/scale (gpt/point 1 -1))) - (gmt/multiply (:transform-inverse shape (gmt/matrix))) - (gmt/translate (gpt/negate center))))) - ;; FIXME: move to geom rect? (defn transform-rect "Transform a rectangles and changes its attributes" diff --git a/common/src/app/common/text.cljc b/common/src/app/common/text.cljc index f8297843dc..8b301d2d40 100644 --- a/common/src/app/common/text.cljc +++ b/common/src/app/common/text.cljc @@ -244,6 +244,21 @@ (run! #(.appendCodePoint sb (int %)) (subvec cpoints start end)) (.toString sb)))) +(defn- fix-gradients + "Conversion from draft doesn't convert correctly the fills gradient types. This + function change the type from string to keyword of the gradient type" + [data] + (letfn [(fix-type [type] + (cond-> type + (string? type) keyword)) + + (update-fill [fill] + (d/update-in-when fill [:fill-color-gradient :type] fix-type)) + + (update-all-fills [fills] + (mapv update-fill fills))] + (d/update-when data :fills update-all-fills))) + (defn convert-from-draft [content] (letfn [(extract-text [cpoints part] @@ -251,7 +266,9 @@ end (inc (first (last part))) text (code-points->text cpoints start end) attrs (second (first part))] - (assoc attrs :text text))) + (-> attrs + (fix-gradients) + (assoc :text text)))) (split-texts [text styles] (let [cpoints (text->code-points text) @@ -268,7 +285,8 @@ (let [key (get block :key) text (get block :text) styles (get block :inlineStyleRanges) - data (get block :data)] + data (->> (get block :data) + fix-gradients)] (-> data (assoc :key key) (assoc :type "paragraph") diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index 518222bcff..e4cb1eee34 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -8,7 +8,10 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.text :as gst] [app.config :as cf] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.gradients :as grad] @@ -28,7 +31,12 @@ fills (get shape :fills []) selrect (dm/get-prop shape :selrect) + + bounds (when (cfh/text-shape? shape) + (gst/shape->rect shape)) + metadata (get shape :metadata) + x (dm/get-prop selrect :x) y (dm/get-prop selrect :y) width (dm/get-prop selrect :width) @@ -62,32 +70,50 @@ (obj/set! pat-props "patternTransform" transform) pat-props)] - (for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))] - [:* {:key (dm/str shape-index)} - (for [[fill-index value] (reverse (d/enumerate (get shape :fills [])))] + (for [[obj-index obj] (d/enumerate (or (:position-data shape) [shape]))] + [:* {:key (dm/str obj-index)} + (for [[fill-index value] (reverse (d/enumerate (get obj :fills [])))] (when (some? (:fill-color-gradient value)) (let [gradient (:fill-color-gradient value) + + from-p (-> (gpt/point (+ x (* width (:start-x gradient))) + (+ y (* height (:start-y gradient))))) + to-p (-> (gpt/point (+ x (* width (:end-x gradient))) + (+ y (* height (:end-y gradient))))) + + gradient + (cond-> gradient + (some? bounds) + (assoc + :start-x (/ (- (:x from-p) (:x bounds)) (:width bounds)) + :start-y (/ (- (:y from-p) (:y bounds)) (:height bounds)) + :end-x (/ (- (:x to-p) (:x bounds)) (:width bounds)) + :end-y (/ (- (:y to-p) (:y bounds)) (:height bounds)))) + props #js {:id (dm/str "fill-color-gradient-" render-id "-" fill-index) :key (dm/str fill-index) :gradient gradient - :shape shape}] - (case (:type gradient) - :linear [:> grad/linear-gradient props] - :radial [:> grad/radial-gradient props])))) + :shape obj}] + (case (d/name (:type gradient)) + "linear" [:> grad/linear-gradient props] + "radial" [:> grad/radial-gradient props])))) - (let [fill-id (dm/str "fill-" shape-index "-" render-id)] + (let [fill-id (dm/str "fill-" obj-index "-" render-id)] [:> :pattern (-> (obj/clone pat-props) (obj/set! "id" fill-id) - (cond-> has-image? + (cond-> (and has-image? (nil? bounds)) (-> (obj/set! "width" (* width no-repeat-padding)) - (obj/set! "height" (* height no-repeat-padding))))) + (obj/set! "height" (* height no-repeat-padding)))) + (cond-> (some? bounds) + (-> (obj/set! "width" (:width bounds)) + (obj/set! "height" (:height bounds))))) [:g - (for [[fill-index value] (reverse (d/enumerate (get shape :fills [])))] + (for [[fill-index value] (reverse (d/enumerate (get obj :fills [])))] (let [style (attrs/get-fill-style value fill-index render-id type) props #js {:key (dm/str fill-index) - :width width - :height height + :width (d/nilv (:width bounds) width) + :height (d/nilv (:height bounds) height) :style style}] (if (:fill-image value) (let [uri (cf/resolve-file-media (:fill-image value)) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 64a8f34efb..0b4e325d93 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -93,7 +93,7 @@ :textTransform text-transform :color (if (and show-text? (not gradient?)) text-color "transparent") :background (when (and show-text? gradient?) text-color) - :caretColor (or text-color "black") + :caretColor (if (and (not gradient?) text-color) text-color "black") :overflowWrap "initial" :lineBreak "auto" :whiteSpace "break-spaces" diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs index e2bf517d1a..c035b31341 100644 --- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs @@ -118,7 +118,7 @@ :on-pointer-up on-pointer-up}]]) (mf/defc gradient-handler-transformed - [{:keys [from-p to-p width-p from-color to-color zoom editing transform + [{:keys [from-p to-p width-p from-color to-color zoom editing on-change-start on-change-finish on-change-width]}] (let [moving-point (mf/use-var nil) angle (+ 90 (gpt/angle from-p to-p)) @@ -151,7 +151,7 @@ (reset! moving-point nil))] (mf/use-effect - (mf/deps @moving-point from-p to-p width-p transform) + (mf/deps @moving-point from-p to-p width-p) (fn [] (let [subs (->> st/stream (rx/filter mse/pointer-event?) @@ -159,18 +159,17 @@ (rx/map mse/get-pointer-position) (rx/subs! (fn [pt] - (let [pt (gpt/transform pt transform)] - (case @moving-point - :from-p (when on-change-start (on-change-start pt)) - :to-p (when on-change-finish (on-change-finish pt)) - :width-p (when on-change-width - (let [width-v (gpt/unit (gpt/to-vec from-p width-p)) - distance (gpt/point-line-distance pt from-p to-p) - new-width-p (gpt/add - from-p - (gpt/multiply width-v (gpt/point distance)))] - (on-change-width new-width-p))) - nil)))))] + (case @moving-point + :from-p (when on-change-start (on-change-start pt)) + :to-p (when on-change-finish (on-change-finish pt)) + :width-p (when on-change-width + (let [width-v (gpt/unit (gpt/to-vec from-p width-p)) + distance (gpt/point-line-distance pt from-p to-p) + new-width-p (gpt/add + from-p + (gpt/multiply width-v (gpt/point distance)))] + (on-change-width new-width-p))) + nil))))] (fn [] (rx/dispose! subs))))) [:g.gradient-handlers [:defs @@ -296,7 +295,6 @@ :width-p (when (= :radial (:type gradient)) width-p) :from-color {:value start-color :opacity start-opacity} :to-color {:value end-color :opacity end-opacity} - :transform transform :zoom zoom :on-change-start on-change-start :on-change-finish on-change-finish From 3a260825b90bc34ced789ff8532fbf8c4a6015df Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 1 Feb 2024 16:58:06 +0100 Subject: [PATCH 28/37] :bug: Fix problem with multiplayer cursors --- frontend/src/app/main/data/workspace/notifications.cljs | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 6efa574390..54397b2f30 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -83,6 +83,7 @@ ;; position changes. (->> stream (rx/filter mse/pointer-event?) + (rx/filter #(= :viewport (mse/get-pointer-source %))) (rx/pipe (rxs/throttle 100)) (rx/map #(handle-pointer-send file-id (:pt %))))) From a9e7ed57d92289f767d869a73c711d204b39dd02 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 26 Jan 2024 11:04:03 +0100 Subject: [PATCH 29/37] :sparkles: Use proper exceptions on internal db functions --- backend/src/app/db.clj | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 6e7407e17d..097ada50a1 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -237,8 +237,7 @@ (jdbc/get-connection system-or-pool) (if (map? system-or-pool) (open (::pool system-or-pool)) - (ex/raise :type :internal - :code :unable-resolve-pool)))) + (throw (IllegalArgumentException. "unable to resolve connection pool"))))) (defn get-update-count [result] @@ -250,9 +249,7 @@ cfg-or-conn (if (map? cfg-or-conn) (get-connection (::conn cfg-or-conn)) - (ex/raise :type :internal - :code :unable-resolve-connection - :hint "expected conn or system map")))) + (throw (IllegalArgumentException. "unable to resolve connection"))))) (defn connection-map? "Check if the provided value is a map like data structure that @@ -260,15 +257,15 @@ [o] (and (map? o) (connection? (::conn o)))) -(defn- get-connectable +(defn get-connectable + "Resolve to a connection or connection pool instance; if it is not + possible, raises an exception" [o] (cond (connection? o) o (pool? o) o (map? o) (get-connectable (or (::conn o) (::pool o))) - :else (ex/raise :type :internal - :code :unable-resolve-connectable - :hint "expected conn, pool or system"))) + :else (throw (IllegalArgumentException. "unable to resolve connectable")))) (def ^:private params-mapping {::return-keys? :return-keys From 3001476dbcec8c869db874de79895c169a2bffb3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 26 Jan 2024 11:04:36 +0100 Subject: [PATCH 30/37] :sparkles: Do not wrap in sm/define on rpc methods Because is redundant operation --- backend/src/app/rpc.clj | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 69f9d84fbc..f9c36515a3 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -139,15 +139,10 @@ (f cfg (us/conform spec params))) f))) -;; TODO: integrate with sm/define - (defn- wrap-params-validation [_ f mdata] (if-let [schema (::sm/params mdata)] - (let [schema (if (sm/lazy-schema? schema) - schema - (sm/define schema)) - validate (sm/validator schema) + (let [validate (sm/validator schema) explain (sm/explainer schema) decode (sm/decoder schema)] (fn [cfg params] From e7a27759e6eb773b285c95e6fbbeabb281afbf86 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 26 Jan 2024 12:29:03 +0100 Subject: [PATCH 31/37] :bug: Fix react warning on isPinned unrecognized prop --- frontend/src/app/main/ui/dashboard/pin_button.cljs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/pin_button.cljs b/frontend/src/app/main/ui/dashboard/pin_button.cljs index 9319be947c..be27a05f09 100644 --- a/frontend/src/app/main/ui/dashboard/pin_button.cljs +++ b/frontend/src/app/main/ui/dashboard/pin_button.cljs @@ -12,15 +12,21 @@ (:require [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [tr]] + [app.util.object :as obj] [rumext.v2 :as mf])) -(def pin-icon (icon-xref :pin-refactor (stl/css :icon))) +(def ^:private pin-icon + (icon-xref :pin-refactor (stl/css :icon))) (mf/defc pin-button* {::mf/props :obj} [{:keys [aria-label is-pinned class] :as props}] (let [aria-label (or aria-label (tr "dashboard.pin-unpin")) class (dm/str (or class "") " " (stl/css-case :button true :button-active is-pinned)) - props (mf/spread-props props {:class class - :aria-label aria-label})] - [:> "button" props pin-icon])) \ No newline at end of file + + props (-> (obj/clone props) + (obj/unset! "isPinned") + (obj/set! "className" class) + (obj/set! "aria-label" aria-label))] + + [:> "button" props pin-icon])) From 5accbd511ff5570c6fdea1558272f9c14ecf8b64 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 26 Jan 2024 14:48:58 +0100 Subject: [PATCH 32/37] :sparkles: Improve quote data structure validation --- backend/src/app/rpc/quotes.clj | 46 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/backend/src/app/rpc/quotes.clj b/backend/src/app/rpc/quotes.clj index 4cdc3800d8..3244bd03f9 100644 --- a/backend/src/app/rpc/quotes.clj +++ b/backend/src/app/rpc/quotes.clj @@ -7,8 +7,10 @@ (ns app.rpc.quotes "Penpot resource usage quotes." (:require + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.logging :as l] + [app.common.schema :as sm] [app.common.spec :as us] [app.config :as cf] [app.db :as db] @@ -23,21 +25,15 @@ ;; PUBLIC API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::conn ::db/pool-or-conn) -(s/def ::file-id ::us/uuid) -(s/def ::team-id ::us/uuid) -(s/def ::project-id ::us/uuid) -(s/def ::profile-id ::us/uuid) -(s/def ::incr (s/and int? pos?)) -(s/def ::target ::us/string) - -(s/def ::quote - (s/keys :req [::id ::profile-id] - :opt [::conn - ::team-id - ::project-id - ::file-id - ::incr])) +(def ^:private schema:quote + (sm/define + [:map {:title "Quote"} + [::team-id {:optional true} ::sm/uuid] + [::project-id {:optional true} ::sm/uuid] + [::file-id {:optional true} ::sm/uuid] + [::incr {:optional true} [:int {:min 0}]] + [::id :keyword] + [::profile-id ::sm/uuid]])) (def ^:private enabled (volatile! true)) @@ -52,15 +48,22 @@ (vswap! enabled (constantly false))) (defn check-quote! - [conn quote] - (us/assert! ::db/pool-or-conn conn) - (us/assert! ::quote quote) + [ds quote] + (dm/assert! + "expected valid quote map" + (sm/validate schema:quote quote)) + (when (contains? cf/flags :quotes) (when @enabled - (check-quote (assoc quote ::conn conn ::target (name (::id quote))))))) + ;; This approach add flexibility on how and where the + ;; check-quote! can be called (in or out of transaction) + (db/run! ds (fn [cfg] + (-> (merge cfg quote) + (assoc ::target (name (::id quote))) + (check-quote))))))) (defn- send-notification! - [{:keys [::conn] :as params}] + [{:keys [::db/conn] :as params}] (l/warn :hint "max quote reached" :target (::target params) :profile-id (some-> params ::profile-id str) @@ -93,7 +96,7 @@ :content content}]})))) (defn- generic-check! - [{:keys [::conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}] + [{:keys [::db/conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}] (let [quote (->> (db/exec! conn quote-sql) (map :quote) (reduce max (- Integer/MAX_VALUE))) @@ -347,7 +350,6 @@ (assoc ::count-sql [sql:get-comments-per-file file-id]) (generic-check!))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; QUOTE: DEFAULT ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; From 82b10ecb87284014033981c33fdc84e8008712b5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 26 Jan 2024 14:49:23 +0100 Subject: [PATCH 33/37] :sparkles: Refactor comments RPC methods to use schema instead of spec --- backend/src/app/rpc/commands/comments.clj | 457 ++++++++++++---------- 1 file changed, 241 insertions(+), 216 deletions(-) diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index 9e1a9d4365..4949f1a435 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -9,7 +9,7 @@ [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.uuid :as uuid] [app.db :as db] [app.db.sql :as sql] @@ -24,18 +24,21 @@ [app.rpc.retry :as rtry] [app.util.pointer-map :as pmap] [app.util.services :as sv] - [app.util.time :as dt] - [clojure.spec.alpha :as s])) + [app.util.time :as dt])) ;; --- GENERAL PURPOSE INTERNAL HELPERS -(defn decode-row +(defn- decode-row [{:keys [participants position] :as row}] (cond-> row (db/pgpoint? position) (assoc :position (db/decode-pgpoint position)) (db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants)))) -(def sql:get-file +(def xf-decode-row + (map decode-row)) + +(def ^:privateqpage-name + sql:get-file "select f.id, f.modified_at, f.revn, f.features, f.project_id, p.team_id, f.data from file as f @@ -45,17 +48,19 @@ (defn- get-file "A specialized version of get-file for comments module." - [{:keys [::db/conn] :as cfg} file-id page-id] - (if-let [{:keys [data] :as file} (some-> (db/exec-one! conn [sql:get-file file-id]) - (files/decode-row))] - (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] - (-> file - (assoc :page-name (dm/get-in data [:pages-index page-id :name])) - (assoc :page-id page-id))) + [cfg file-id page-id] + (let [file (db/exec-one! cfg [sql:get-file file-id])] + (when-not file + (ex/raise :type :not-found + :code :object-not-found + :hint "file not found")) - (ex/raise :type :not-found - :code :object-not-found - :hint "file not found"))) + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] + (let [{:keys [data] :as file} (files/decode-row file)] + (-> file + (assoc :page-name (dm/get-in data [:pages-index page-id :name])) + (assoc :page-id page-id) + (dissoc :data)))))) (defn- get-comment-thread [conn thread-id & {:as opts}] @@ -93,23 +98,25 @@ (declare ^:private get-comment-threads) -(s/def ::team-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::share-id (s/nilable ::us/uuid)) - -(s/def ::get-comment-threads - (s/and (s/keys :req [::rpc/profile-id] - :opt-un [::file-id ::share-id ::team-id]) - #(or (:file-id %) (:team-id %)))) +(def ^:private + schema:get-comment-threads + [:and + [:map {:title "get-comment-threads"} + [:file-id {:optional true} ::sm/uuid] + [:team-id {:optional true} ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]] + [::sm/contains-any #{:file-id :team-id}]]) (sv/defmethod ::get-comment-threads - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] - (dm/with-open [conn (db/open pool)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (get-comment-threads conn profile-id file-id))) + {::doc/added "1.15" + ::sm/params schema:get-comment-threads} + [cfg {:keys [::rpc/profile-id file-id share-id] :as params}] -(def sql:comment-threads + (db/run! cfg (fn [{:keys [::db/conn]}] + (files/check-comment-permissions! conn profile-id file-id share-id) + (get-comment-threads conn profile-id file-id)))) + +(def ^:private sql:comment-threads "select distinct on (ct.id) ct.*, f.name as file_name, @@ -134,23 +141,24 @@ (defn- get-comment-threads [conn profile-id file-id] (->> (db/exec! conn [sql:comment-threads profile-id file-id]) - (into [] (map decode-row)))) + (into [] xf-decode-row))) ;; --- COMMAND: Get Unread Comment Threads (declare ^:private get-unread-comment-threads) -(s/def ::team-id ::us/uuid) -(s/def ::get-unread-comment-threads - (s/keys :req [::rpc/profile-id] - :req-un [::team-id])) +(def ^:private + schema:get-unread-comment-threads + [:map {:title "get-unread-comment-threads"} + [:team-id ::sm/uuid]]) (sv/defmethod ::get-unread-comment-threads - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] - (dm/with-open [conn (db/open pool)] - (teams/check-read-permissions! conn profile-id team-id) - (get-unread-comment-threads conn profile-id team-id))) + {::doc/added "1.15" + ::sm/params schema:get-unread-comment-threads} + [cfg {:keys [::rpc/profile-id team-id] :as params}] + (db/run! cfg (fn [{:keys [::db/conn]}] + (teams/check-read-permissions! conn profile-id team-id) + (get-unread-comment-threads conn profile-id team-id)))) (def sql:comment-threads-by-team "select distinct on (ct.id) @@ -182,62 +190,60 @@ (defn- get-unread-comment-threads [conn profile-id team-id] (->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id]) - (into [] (map decode-row)))) - + (into [] xf-decode-row))) ;; --- COMMAND: Get Single Comment Thread -(s/def ::get-comment-thread - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::us/id] - :opt-un [::share-id])) +(def ^:private + schema:get-comment-thread + [:map {:title "get-comment-thread"} + [:file-id ::sm/uuid] + [:id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::get-comment-thread - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}] - (dm/with-open [conn (db/open pool)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (let [sql (str "with threads as (" sql:comment-threads ")" - "select * from threads where id = ?")] - (-> (db/exec-one! conn [sql profile-id file-id id]) - (decode-row))))) + {::doc/added "1.15" + ::sm/params schema:get-comment-thread} + [cfg {:keys [::rpc/profile-id file-id id share-id] :as params}] + (db/run! cfg (fn [{:keys [::db/conn]}] + (files/check-comment-permissions! conn profile-id file-id share-id) + (let [sql (str "with threads as (" sql:comment-threads ")" + "select * from threads where id = ?")] + (-> (db/exec-one! conn [sql profile-id file-id id]) + (decode-row)))))) ;; --- COMMAND: Retrieve Comments (declare ^:private get-comments) -(s/def ::thread-id ::us/uuid) -(s/def ::get-comments - (s/keys :req [::rpc/profile-id] - :req-un [::thread-id] - :opt-un [::share-id])) +(def ^:private + schema:get-comments + [:map {:title "get-comments"} + [:thread-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::get-comments - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}] - (dm/with-open [conn (db/open pool)] - (let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (get-comments conn thread-id)))) - -(def sql:comments - "select c.* from comment as c - where c.thread_id = ? - order by c.created_at asc") + {::doc/added "1.15" + ::sm/params schema:get-comments} + [cfg {:keys [::rpc/profile-id thread-id share-id]}] + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (get-comments conn thread-id))))) (defn- get-comments [conn thread-id] (->> (db/query conn :comment {:thread-id thread-id} {:order-by [[:created-at :asc]]}) - (into [] (map decode-row)))) + (into [] xf-decode-row))) ;; --- COMMAND: Get file comments users ;; All the profiles that had comment the file, plus the current ;; profile. -(def sql:file-comment-users +(def ^:private sql:file-comment-users "WITH available_profiles AS ( SELECT DISTINCT owner_id AS id FROM comment @@ -256,20 +262,22 @@ [conn file-id profile-id] (db/exec! conn [sql:file-comment-users file-id profile-id])) -(s/def ::get-profiles-for-file-comments - (s/keys :req [::rpc/profile-id] - :req-un [::file-id] - :opt-un [::share-id])) +(def ^:private + schema:get-profiles-for-file-comments + [:map {:title "get-profiles-for-file-comments"} + [:file-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::get-profiles-for-file-comments "Retrieves a list of profiles with limited set of properties of all participants on comment threads of the file." {::doc/added "1.15" - ::doc/changes ["1.15" "Imported from queries and renamed."]} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}] - (dm/with-open [conn (db/open pool)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (get-file-comments-users conn file-id profile-id))) + ::doc/changes ["1.15" "Imported from queries and renamed."] + ::sm/params schema:get-profiles-for-file-comments} + [cfg {:keys [::rpc/profile-id file-id share-id]}] + (db/run! cfg (fn [{:keys [::db/conn]}] + (files/check-comment-permissions! conn profile-id file-id share-id) + (get-file-comments-users conn file-id profile-id)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MUTATION COMMANDS @@ -279,52 +287,52 @@ ;; --- COMMAND: Create Comment Thread -(s/def ::page-id ::us/uuid) -(s/def ::position ::gpt/point) -(s/def ::content ::us/string) -(s/def ::frame-id ::us/uuid) - -(s/def ::create-comment-thread - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::position ::content ::page-id ::frame-id] - :opt-un [::share-id])) +(def ^:private + schema:create-comment-thread + [:map {:title "create-comment-thread"} + [:file-id ::sm/uuid] + [:position ::gpt/point] + [:content :string] + [:page-id ::sm/uuid] + [:frame-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::create-comment-thread {::doc/added "1.15" - ::webhooks/event? true} + ::webhooks/event? true + ::rtry/enabled true + ::rtry/when rtry/conflict-exception? + ::sm/params schema:create-comment-thread} [cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}] - (db/tx-run! cfg - (fn [{:keys [::db/conn] :as cfg}] - (files/check-comment-permissions! conn profile-id file-id share-id) - (let [{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)] - (run! (partial quotes/check-quote! conn) - (list {::quotes/id ::quotes/comment-threads-per-file - ::quotes/profile-id profile-id - ::quotes/team-id team-id - ::quotes/project-id project-id - ::quotes/file-id file-id} - {::quotes/id ::quotes/comments-per-file - ::quotes/profile-id profile-id - ::quotes/team-id team-id - ::quotes/project-id project-id - ::quotes/file-id file-id})) + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (files/check-comment-permissions! cfg profile-id file-id share-id) + (let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)] + (run! (partial quotes/check-quote! cfg) + (list {::quotes/id ::quotes/comment-threads-per-file + ::quotes/profile-id profile-id + ::quotes/team-id team-id + ::quotes/project-id project-id + ::quotes/file-id file-id} + {::quotes/id ::quotes/comments-per-file + ::quotes/profile-id profile-id + ::quotes/team-id team-id + ::quotes/project-id project-id + ::quotes/file-id file-id})) - (-> cfg - (assoc ::rtry/when rtry/conflict-exception?) - (assoc ::rtry/label "create-comment-thread") - (rtry/invoke create-comment-thread {:created-at request-at - :profile-id profile-id - :file-id file-id - :page-id page-id - :page-name page-name - :position position - :content content - :frame-id frame-id})))))) + (create-comment-thread conn {:created-at request-at + :profile-id profile-id + :file-id file-id + :page-id page-id + :page-name page-name + :position position + :content content + :frame-id frame-id}))))) (defn- create-comment-thread - [{:keys [::db/conn]} {:keys [profile-id file-id page-id page-name created-at position content frame-id]}] + [conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}] + (let [;; NOTE: we take the next seq number from a separate query because the whole ;; operation can be retried on conflict, and in this case the new seq shold be ;; retrieved from the database. @@ -364,68 +372,72 @@ ;; --- COMMAND: Update Comment Thread Status -(s/def ::id ::us/uuid) -(s/def ::share-id (s/nilable ::us/uuid)) - -(s/def ::update-comment-thread-status - (s/keys :req [::rpc/profile-id] - :req-un [::id] - :opt-un [::share-id])) +(def ^:private + schema:update-comment-thread-status + [:map {:title "update-comment-thread-status"} + [:id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment-thread-status - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (upsert-comment-thread-status! conn profile-id id)))) + {::doc/added "1.15" + ::sm/params schema:update-comment-thread-status} + [cfg {:keys [::rpc/profile-id id share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (upsert-comment-thread-status! conn profile-id id))))) ;; --- COMMAND: Update Comment Thread -(s/def ::is-resolved ::us/boolean) -(s/def ::update-comment-thread - (s/keys :req [::rpc/profile-id] - :req-un [::id ::is-resolved] - :opt-un [::share-id])) +(def ^:private + schema:update-comment-thread + [:map {:title "update-comment-thread"} + [:id ::sm/uuid] + [:is-resolved :boolean] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment-thread - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (db/update! conn :comment-thread - {:is-resolved is-resolved} - {:id id}) - nil))) + {::doc/added "1.15" + ::sm/params schema:update-comment-thread} + [cfg {:keys [::rpc/profile-id id is-resolved share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (db/update! conn :comment-thread + {:is-resolved is-resolved} + {:id id}) + nil)))) ;; --- COMMAND: Add Comment (declare ^:private get-comment-thread) -(s/def ::create-comment - (s/keys :req [::rpc/profile-id] - :req-un [::thread-id ::content] - :opt-un [::share-id])) +(def ^:private + schema:create-comment + [:map {:title "create-comment"} + [:thread-id ::sm/uuid] + [:content :string] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::create-comment {::doc/added "1.15" - ::webhooks/event? true} + ::webhooks/event? true + ::sm/params schema:create-comment} [cfg {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content]}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true) {:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)] - (files/check-comment-permissions! conn profile-id (:id file) share-id) + (files/check-comment-permissions! conn profile-id file-id share-id) (quotes/check-quote! conn {::quotes/id ::quotes/comments-per-file ::quotes/profile-id profile-id ::quotes/team-id team-id ::quotes/project-id project-id - ::quotes/file-id (:id file)}) + ::quotes/file-id file-id}) ;; Update the page-name cached attribute on comment thread table. (when (not= page-name (:page-name thread)) @@ -461,15 +473,17 @@ ;; --- COMMAND: Update Comment -(s/def ::update-comment - (s/keys :req [::rpc/profile-id] - :req-un [::id ::content] - :opt-un [::share-id])) +(def ^:private + schema:update-comment + [:map {:title "update-comment"} + [:id ::sm/uuid] + [:content :string] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment - {::doc/added "1.15"} + {::doc/added "1.15" + ::sm/params schema:update-comment} [cfg {:keys [::rpc/profile-id ::rpc/request-at id share-id content]}] - (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::sql/for-update true) @@ -482,7 +496,7 @@ (ex/raise :type :validation :code :not-allowed)) - (let [{:keys [page-name] :as file} (get-file cfg file-id page-id)] + (let [{:keys [page-name]} (get-file cfg file-id page-id)] (db/update! conn :comment {:content content :modified-at request-at} @@ -496,79 +510,90 @@ ;; --- COMMAND: Delete Comment Thread -(s/def ::delete-comment-thread - (s/keys :req [::rpc/profile-id] - :req-un [::id] - :opt-un [::share-id])) +(def ^:private + schema:delete-comment-thread + [:map {:title "delete-comment-thread"} + [:id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::delete-comment-thread - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id]}] - (db/with-atomic [conn pool] - (let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (when-not (= owner-id profile-id) - (ex/raise :type :validation - :code :not-allowed)) + {::doc/added "1.15" + ::sm/params schema:delete-comment-thread} + [cfg {:keys [::rpc/profile-id id share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (when-not (= owner-id profile-id) + (ex/raise :type :validation + :code :not-allowed)) - (db/delete! conn :comment-thread {:id id}) - nil))) + (db/delete! conn :comment-thread {:id id}) + nil)))) ;; --- COMMAND: Delete comment -(s/def ::delete-comment - (s/keys :req [::rpc/profile-id] - :req-un [::id] - :opt-un [::share-id])) +(def ^:private + schema:delete-comment + [:map {:title "delete-comment"} + [:id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::delete-comment - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::sql/for-update true) - {:keys [file-id] :as thread} (get-comment-thread conn thread-id)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (when-not (= owner-id profile-id) - (ex/raise :type :validation - :code :not-allowed)) - (db/delete! conn :comment {:id id}) - nil))) + {::doc/added "1.15" + ::sm/params schema:delete-comment} + [cfg {:keys [::rpc/profile-id id share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::sql/for-update true) + {:keys [file-id] :as thread} (get-comment-thread conn thread-id)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (when-not (= owner-id profile-id) + (ex/raise :type :validation + :code :not-allowed)) + (db/delete! conn :comment {:id id}) + nil)))) ;; --- COMMAND: Update comment thread position -(s/def ::update-comment-thread-position - (s/keys :req [::rpc/profile-id] - :req-un [::id ::position ::frame-id] - :opt-un [::share-id])) +(def ^:private + schema:update-comment-thread-position + [:map {:title "update-comment-thread-position"} + [:id ::sm/uuid] + [:position ::gpt/point] + [:frame-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment-thread-position - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}] - (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (db/update! conn :comment-thread - {:modified-at request-at - :position (db/pgpoint position) - :frame-id frame-id} - {:id (:id thread)}) - nil))) + {::doc/added "1.15" + ::sm/params schema:update-comment-thread-position} + [cfg {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (db/update! conn :comment-thread + {:modified-at request-at + :position (db/pgpoint position) + :frame-id frame-id} + {:id (:id thread)}) + nil)))) ;; --- COMMAND: Update comment frame -(s/def ::update-comment-thread-frame - (s/keys :req [::rpc/profile-id] - :req-un [::id ::frame-id] - :opt-un [::share-id])) +(def ^:private + schema:update-comment-thread-frame + [:map {:title "update-comment-thread-frame"} + [:id ::sm/uuid] + [:frame-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment-thread-frame - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}] - (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (db/update! conn :comment-thread - {:modified-at request-at - :frame-id frame-id} - {:id id}) - nil))) + {::doc/added "1.15" + ::sm/params schema:update-comment-thread-frame} + [cfg {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (db/update! conn :comment-thread + {:modified-at request-at + :frame-id frame-id} + {:id id}) + nil)))) From 16a051d7e0ebd7e0220d5a10c467e61d8259d37a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 26 Jan 2024 15:04:46 +0100 Subject: [PATCH 34/37] :sparkles: Improve efficiency of thumbnails creation RPC methods Moving the retry mechanism out of the transaction --- .../src/app/rpc/commands/files_thumbnails.clj | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index 712c212047..3736838403 100644 --- a/backend/src/app/rpc/commands/files_thumbnails.clj +++ b/backend/src/app/rpc/commands/files_thumbnails.clj @@ -285,26 +285,29 @@ (sv/defmethod ::create-file-object-thumbnail {::doc/added "1.19" ::doc/module :files - ::climit/id :file-thumbnail-ops + ::climit/id :file-thumbnail-ops/by-profile ::climit/key-fn ::rpc/profile-id + + ::rtry/enabled true + ::rtry/when rtry/conflict-exception? + ::audit/skip true ::sm/params schema:create-file-object-thumbnail} [cfg {:keys [::rpc/profile-id file-id object-id media tag]}] + (media/validate-media-type! media) + (media/validate-media-size! media) + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (files/check-edition-permissions! conn profile-id file-id) - (media/validate-media-type! media) - (media/validate-media-size! media) - (when-not (db/read-only? conn) (let [cfg (-> cfg (update ::sto/storage media/configure-assets-storage) (assoc ::rtry/when rtry/conflict-exception?) (assoc ::rtry/max-retries 5) (assoc ::rtry/label "create-file-object-thumbnail"))] - (rtry/invoke cfg create-file-object-thumbnail! - file-id object-id media (or tag "frame"))))))) + (create-file-object-thumbnail! cfg file-id object-id media (or tag "frame"))))))) ;; --- MUTATION COMMAND: delete-file-object-thumbnail @@ -400,6 +403,8 @@ ::audit/skip true ::climit/id :file-thumbnail-ops ::climit/key-fn ::rpc/profile-id + ::rtry/enabled true + ::rtry/when rtry/conflict-exception? ::sm/params [:map {:title "create-file-thumbnail"} [:file-id ::sm/uuid] [:revn :int] @@ -409,10 +414,6 @@ (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (files/check-edition-permissions! conn profile-id file-id) (when-not (db/read-only? conn) - (let [cfg (-> cfg - (update ::sto/storage media/configure-assets-storage) - (assoc ::rtry/when rtry/conflict-exception?) - (assoc ::rtry/max-retries 5) - (assoc ::rtry/label "create-thumbnail")) - media (rtry/invoke cfg create-file-thumbnail! params)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage) + media (create-file-thumbnail! cfg params)] {:uri (files/resolve-public-uri (:id media))}))))) From dabb9d0a82b8b5daa4d348d62bd56f36c89ae8c9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 26 Jan 2024 15:09:01 +0100 Subject: [PATCH 35/37] :sparkles: Improve internal API of retry mechanism --- backend/src/app/loggers/audit.clj | 23 +++++------ backend/src/app/rpc/retry.clj | 59 ++++++++++++--------------- common/src/app/common/exceptions.cljc | 7 ++-- 3 files changed, 38 insertions(+), 51 deletions(-) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 3626971fc0..e7a0184ac7 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -200,22 +200,15 @@ ;; NOTE: this operation may cause primary key conflicts on inserts ;; because of the timestamp precission (two concurrent requests), in ;; this case we just retry the operation. - (let [cfg (-> cfg - (assoc ::rtry/when rtry/conflict-exception?) - (assoc ::rtry/max-retries 6) - (assoc ::rtry/label "persist-audit-log")) + (let [tnow (dt/now) params (-> params + (assoc :created-at tnow) + (assoc :tracked-at tnow) (update :props db/tjson) (update :context db/tjson) (update :ip-addr db/inet) (assoc :source "backend"))] - - (rtry/invoke cfg (fn [cfg] - (let [tnow (dt/now) - params (-> params - (assoc :created-at tnow) - (assoc :tracked-at tnow))] - (db/insert! cfg :audit-log params)))))) + (db/insert! cfg :audit-log params))) (when (and (contains? cf/flags :webhooks) (::webhooks/event? event)) @@ -246,9 +239,13 @@ "Submit audit event to the collector." [cfg params] (try - (let [event (d/without-nils params)] + (let [event (d/without-nils params) + cfg (-> cfg + (assoc ::rtry/when rtry/conflict-exception?) + (assoc ::rtry/max-retries 6) + (assoc ::rtry/label "persist-audit-log"))] (us/verify! ::event event) - (db/tx-run! cfg handle-event! event)) + (rtry/invoke! cfg db/tx-run! handle-event! event)) (catch Throwable cause (l/error :hint "unexpected error processing event" :cause cause)))) diff --git a/backend/src/app/rpc/retry.clj b/backend/src/app/rpc/retry.clj index bd9c3ea075..3745b9d8f1 100644 --- a/backend/src/app/rpc/retry.clj +++ b/backend/src/app/rpc/retry.clj @@ -6,8 +6,8 @@ (ns app.rpc.retry (:require + [app.common.exceptions :as ex] [app.common.logging :as l] - [app.db :as db] [app.util.services :as sv]) (:import org.postgresql.util.PSQLException)) @@ -15,12 +15,29 @@ (defn conflict-exception? "Check if exception matches a insertion conflict on postgresql." [e] - (and (instance? PSQLException e) - (= "23505" (.getSQLState ^PSQLException e)))) + (when-let [cause (ex/instance? PSQLException e)] + (= "23505" (.getSQLState ^PSQLException cause)))) (def ^:private always-false (constantly false)) +(defn invoke! + [{:keys [::max-retries] :or {max-retries 3} :as cfg} f & args] + (loop [rnum 1] + (let [match? (get cfg ::when always-false) + result (try + (apply f cfg args) + (catch Throwable cause + (if (and (match? cause) (<= rnum max-retries)) + ::retry + (throw cause))))] + (if (= ::retry result) + (let [label (get cfg ::label "anonymous")] + (l/warn :hint "retrying operation" :label label :retry rnum) + (recur (inc rnum))) + result)))) + + (defn wrap-retry [_ f {:keys [::sv/name] :as mdata}] @@ -29,36 +46,10 @@ matches? (get mdata ::when always-false)] (l/dbg :hint "wrapping retry" :name name :max-retries max-retries) (fn [cfg params] - ((fn recursive-invoke [retry] - (try - (f cfg params) - (catch Throwable cause - (if (matches? cause) - (let [current-retry (inc retry)] - (l/wrn :hint "retrying operation" :retry current-retry :service name) - (if (<= current-retry max-retries) - (recursive-invoke current-retry) - (throw cause))) - (throw cause))))) 1))) + (-> cfg + (assoc ::max-retries max-retries) + (assoc ::when matches?) + (assoc ::label name) + (invoke! f params)))) f)) -(defn invoke - [{:keys [::db/conn ::max-retries] :or {max-retries 3} :as cfg} f & args] - (assert (db/connection? conn) "invalid database connection") - (loop [rnum 1] - (let [match? (get cfg ::when always-false) - result (let [spoint (db/savepoint conn)] - (try - (let [result (apply f cfg args)] - (db/release! conn spoint) - result) - (catch Throwable cause - (db/rollback! conn spoint) - (if (and (match? cause) (<= rnum max-retries)) - ::retry - (throw cause)))))] - (if (= ::retry result) - (let [label (get cfg ::label "anonymous")] - (l/warn :hint "retrying operation" :label label :retry rnum) - (recur (inc rnum))) - result)))) diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index 2070986fe3..5cceeb7222 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -74,10 +74,9 @@ [class cause] (loop [cause cause] (if (c/instance? class cause) - true - (if-let [cause (ex-cause cause)] - (recur cause) - false))))) + cause + (when-let [cause (ex-cause cause)] + (recur cause)))))) ;; NOTE: idea for a macro for error handling ;; (pu/try-let [cause (p/await (get-object-data backend object))] From 658c26014bf3e571763225842b03334f7fc1adbd Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 26 Jan 2024 17:10:04 +0100 Subject: [PATCH 36/37] :lipstick: Define a RPC schema as standalone var for create-file-thumbnail --- backend/src/app/rpc/commands/files_thumbnails.clj | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index 3736838403..f47300bdee 100644 --- a/backend/src/app/rpc/commands/files_thumbnails.clj +++ b/backend/src/app/rpc/commands/files_thumbnails.clj @@ -395,6 +395,13 @@ media)) +(def ^:private + schema:create-file-thumbnail + [:map {:title "create-file-thumbnail"} + [:file-id ::sm/uuid] + [:revn :int] + [:media ::media/upload]]) + (sv/defmethod ::create-file-thumbnail "Creates or updates the file thumbnail. Mainly used for paint the grid thumbnails." @@ -405,10 +412,7 @@ ::climit/key-fn ::rpc/profile-id ::rtry/enabled true ::rtry/when rtry/conflict-exception? - ::sm/params [:map {:title "create-file-thumbnail"} - [:file-id ::sm/uuid] - [:revn :int] - [:media ::media/upload]]} + ::sm/params schema:create-file-thumbnail} [cfg {:keys [::rpc/profile-id file-id] :as params}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] From a5c6d78ee5020509a5b40d4bee0983e302af52c9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 27 Jan 2024 22:33:52 +0100 Subject: [PATCH 37/37] :recycle: Fix some fundamental bugs on climit module The climit previously of this commit is heavily used inside a transactions, so in heavy contention operation such that file thumbnail creation can cause a db pool exhaust. This commit fixes this issue setting up a better resource limiting mechanism that works outside the transactions so, contention will no longer hold an open connection/transaction. It also adds general improvement to the traceability to the climit mechanism: it now properly logs the profile-id that is currently cause some contention on specific resources. It also add a general/root climit that is applied to all requests so if someone start making abussive requests, we can clearly detect it. --- backend/resources/climit.edn | 19 +- backend/src/app/main.clj | 4 +- backend/src/app/rpc.clj | 3 +- backend/src/app/rpc/climit.clj | 266 +++++++++++------- backend/src/app/rpc/commands/auth.clj | 58 ++-- .../src/app/rpc/commands/files_thumbnails.clj | 14 +- backend/src/app/rpc/commands/files_update.clj | 18 +- backend/src/app/rpc/commands/fonts.clj | 13 +- backend/src/app/rpc/commands/media.clj | 45 +-- backend/src/app/rpc/commands/profile.clj | 56 ++-- backend/test/backend_tests/helpers.clj | 1 + common/src/app/common/logging.cljc | 6 + 12 files changed, 291 insertions(+), 212 deletions(-) diff --git a/backend/resources/climit.edn b/backend/resources/climit.edn index 6bb330927f..34d2184153 100644 --- a/backend/resources/climit.edn +++ b/backend/resources/climit.edn @@ -3,15 +3,26 @@ ;; Optional: queue, ommited means Integer/MAX_VALUE ;; Optional: timeout, ommited means no timeout ;; Note: queue and timeout are excluding -{:update-file/by-profile +{:update-file/global {:permits 20} + :update-file/by-profile {:permits 1 :queue 5} - :update-file/global {:permits 20} + :process-font/global {:permits 4} + :process-font/by-profile {:permits 1} - :derive-password/global {:permits 8} - :process-font/global {:permits 4} :process-image/global {:permits 8} + :process-image/by-profile {:permits 1} + :auth/global {:permits 8} + + :root/global + {:permits 40} + + :root/by-profile + {:permits 10} + + :file-thumbnail-ops/global + {:permits 20} :file-thumbnail-ops/by-profile {:permits 2} diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 7028be8bfe..47e43f5cf7 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -322,9 +322,7 @@ ::rpc/climit (ig/ref ::rpc/climit) ::rpc/rlimit (ig/ref ::rpc/rlimit) ::setup/templates (ig/ref ::setup/templates) - ::props (ig/ref ::setup/props) - - :pool (ig/ref ::db/pool)} + ::props (ig/ref ::setup/props)} :app.rpc.doc/routes {:methods (ig/ref :app.rpc/methods)} diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index f9c36515a3..08ccd8cdb5 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -240,8 +240,7 @@ ::mtx/metrics ::main/props] :opt [::climit - ::rlimit] - :req-un [::db/pool])) + ::rlimit])) (defmethod ig/init-key ::methods [_ cfg] diff --git a/backend/src/app/rpc/climit.clj b/backend/src/app/rpc/climit.clj index 71c64b596a..cf2942c229 100644 --- a/backend/src/app/rpc/climit.clj +++ b/backend/src/app/rpc/climit.clj @@ -21,26 +21,31 @@ [app.worker :as-alias wrk] [clojure.edn :as edn] [clojure.spec.alpha :as s] + [cuerdas.core :as str] [datoteka.fs :as fs] [integrant.core :as ig] - [promesa.core :as p] [promesa.exec :as px] [promesa.exec.bulkhead :as pbh]) (:import - clojure.lang.ExceptionInfo)) + clojure.lang.ExceptionInfo + java.util.concurrent.atomic.AtomicLong)) (set! *warn-on-reflection* true) (defn- id->str - [id] - (-> (str id) - (subs 1))) + ([id] + (-> (str id) + (subs 1))) + ([id key] + (if key + (str (-> (str id) (subs 1)) "/" key) + (id->str id)))) (defn- create-cache [{:keys [::wrk/executor]}] (letfn [(on-remove [key _ cause] (let [[id skey] key] - (l/dbg :hint "destroy limiter" :id (id->str id) :key skey :reason (str cause))))] + (l/dbg :hint "disposed" :id (id->str id skey) :reason (str cause))))] (cache/create :executor executor :on-remove on-remove :keepalive "5m"))) @@ -81,132 +86,179 @@ (defn- create-limiter [config [id skey]] - (l/dbg :hint "create limiter" :id (id->str id) :key skey) + (l/dbg :hint "created" :id (id->str id skey)) (pbh/create :permits (or (:permits config) (:concurrency config)) :queue (or (:queue config) (:queue-size config)) :timeout (:timeout config) :type :semaphore)) -(defn- invoke! - [config cache metrics id key f] - (if-let [limiter (cache/get cache [id key] (partial create-limiter config))] - (let [tpoint (dt/tpoint) - labels (into-array String [(id->str id)]) - wrapped (fn [] - (let [elapsed (tpoint) - stats (pbh/get-stats limiter)] - (l/trc :hint "acquired" - :id (id->str id) - :key key - :permits (:permits stats) - :queue (:queue stats) - :max-permits (:max-permits stats) - :max-queue (:max-queue stats) - :elapsed (dt/format-duration elapsed)) +(defmacro ^:private measure-and-log! + [metrics mlabels stats id action limit-id limit-label profile-id elapsed] + `(let [mpermits# (:max-permits ~stats) + mqueue# (:max-queue ~stats) + permits# (:permits ~stats) + queue# (:queue ~stats) + queue# (- queue# mpermits#) + queue# (if (neg? queue#) 0 queue#) + level# (if (pos? queue#) :warn :trace)] - (mtx/run! metrics - :id :rpc-climit-timing - :val (inst-ms elapsed) - :labels labels) - (try - (f) - (finally - (let [elapsed (tpoint)] - (l/trc :hint "finished" - :id (id->str id) - :key key - :permits (:permits stats) - :queue (:queue stats) - :max-permits (:max-permits stats) - :max-queue (:max-queue stats) - :elapsed (dt/format-duration elapsed))))))) - measure! - (fn [stats] - (mtx/run! metrics - :id :rpc-climit-queue - :val (:queue stats) - :labels labels) - (mtx/run! metrics - :id :rpc-climit-permits - :val (:permits stats) - :labels labels))] + (mtx/run! ~metrics + :id :rpc-climit-queue + :val queue# + :labels ~mlabels) - (try - (let [stats (pbh/get-stats limiter)] - (measure! stats) - (l/trc :hint "enqueued" - :id (id->str id) - :key key - :permits (:permits stats) - :queue (:queue stats) - :max-permits (:max-permits stats) - :max-queue (:max-queue stats)) - (px/invoke! limiter wrapped)) - (catch ExceptionInfo cause - (let [{:keys [type code]} (ex-data cause)] - (if (= :bulkhead-error type) + (mtx/run! ~metrics + :id :rpc-climit-permits + :val permits# + :labels ~mlabels) + + (l/log level# + :hint ~action + :req ~id + :id ~limit-id + :label ~limit-label + :profile-id (str ~profile-id) + :permits permits# + :queue queue# + :max-permits mpermits# + :max-queue mqueue# + ~@(if (some? elapsed) + [:elapsed `(dt/format-duration ~elapsed)] + [])))) + +(def ^:private idseq (AtomicLong. 0)) + +(defn- invoke + [limiter metrics limit-id limit-key limit-label profile-id f params] + (let [tpoint (dt/tpoint) + limit-id (id->str limit-id limit-key) + mlabels (into-array String [limit-id]) + stats (pbh/get-stats limiter) + id (.incrementAndGet ^AtomicLong idseq)] + + (try + (measure-and-log! metrics mlabels stats id "enqueued" limit-id limit-label profile-id nil) + (px/invoke! limiter (fn [] + (let [elapsed (tpoint) + stats (pbh/get-stats limiter)] + (measure-and-log! metrics mlabels stats id "acquired" limit-id limit-label profile-id elapsed) + (mtx/run! metrics + :id :rpc-climit-timing + :val (inst-ms elapsed) + :labels mlabels) + (apply f params)))) + + (catch ExceptionInfo cause + (let [{:keys [type code]} (ex-data cause)] + (if (= :bulkhead-error type) + (let [elapsed (tpoint)] + (measure-and-log! metrics mlabels stats id "reject" limit-id limit-label profile-id elapsed) (ex/raise :type :concurrency-limit :code code - :hint "concurrency limit reached") - (throw cause)))) + :hint "concurrency limit reached" + :cause cause)) + (throw cause)))) - (finally - (measure! (pbh/get-stats limiter))))) - - (do - (l/wrn :hint "no limiter found" :id (id->str id)) - (f)))) + (finally + (let [elapsed (tpoint) + stats (pbh/get-stats limiter)] + (measure-and-log! metrics mlabels stats id "finished" limit-id limit-label profile-id elapsed)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MIDDLEWARE ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def noop-fn (constantly nil)) +(def ^:private noop-fn (constantly nil)) +(def ^:private global-limits + [[:root/global noop-fn] + [:root/by-profile ::rpc/profile-id]]) + +(defn- get-limits + [cfg] + (when-let [ref (get cfg ::id)] + (cond + (keyword? ref) + [[ref]] + + (and (vector? ref) + (keyword (first ref))) + [ref] + + (and (vector? ref) + (vector? (first ref))) + (rseq ref) + + :else + (throw (IllegalArgumentException. "unable to normalize limit"))))) (defn wrap - [{:keys [::rpc/climit ::mtx/metrics]} f {:keys [::id ::key-fn] :or {key-fn noop-fn} :as mdata}] - (if (and (some? climit) (some? id)) - (let [cache (::cache climit) - config (::config climit)] - (if-let [config (get config id)] - (do - (l/dbg :hint "instrumenting method" - :limit (id->str id) - :service-name (::sv/name mdata) - :timeout (:timeout config) - :permits (:permits config) - :queue (:queue config) - :keyed? (not= key-fn noop-fn)) + [{:keys [::rpc/climit ::mtx/metrics]} handler mdata] + (let [cache (::cache climit) + config (::config climit) + label (::sv/name mdata)] - (fn [cfg params] - (invoke! config cache metrics id (key-fn params) (partial f cfg params)))) + (reduce (fn [handler [limit-id key-fn]] + (if-let [config (get config limit-id)] + (let [key-fn (or key-fn noop-fn)] + (l/dbg :hint "instrumenting method" + :method label + :limit (id->str limit-id) + :timeout (:timeout config) + :permits (:permits config) + :queue (:queue config) + :keyed (not= key-fn noop-fn)) - (do - (l/wrn :hint "no config found for specified queue" :id (id->str id)) - f))) - f)) + (if (and (= key-fn ::rpc/profile-id) + (false? (::rpc/auth mdata true))) + + ;; We don't enforce by-profile limit on methods that does + ;; not require authentication + handler + + (fn [cfg params] + (let [limit-key (key-fn params) + cache-key [limit-id limit-key] + limiter (cache/get cache cache-key (partial create-limiter config)) + profile-id (if (= key-fn ::rpc/profile-id) + limit-key + (get params ::rpc/profile-id))] + (invoke limiter metrics limit-id limit-key label profile-id handler [cfg params]))))) + + (do + (l/wrn :hint "no config found for specified queue" :id (id->str limit-id)) + handler))) + + handler + (concat global-limits (get-limits mdata))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PUBLIC API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn configure - [{:keys [::rpc/climit]} id] - (us/assert! ::rpc/climit climit) - (assoc climit ::id id)) +(defn- build-exec-chain + [{:keys [::label ::profile-id ::rpc/climit ::mtx/metrics] :as cfg} f] + (let [config (get climit ::config) + cache (get climit ::cache)] -(defn run! + (reduce (fn [handler [limit-id limit-key :as ckey]] + (let [config (get config limit-id)] + (when-not config + (throw (IllegalArgumentException. + (str/ffmt "config not found for: %" limit-id)))) + + (fn [& params] + (let [limiter (cache/get cache ckey (partial create-limiter config))] + (invoke limiter metrics limit-id limit-key label profile-id handler params))))) + f + (get-limits cfg)))) + +(defn invoke! "Run a function in context of climit. Intended to be used in virtual threads." - ([{:keys [::id ::cache ::config ::mtx/metrics]} f] - (if-let [config (get config id)] - (invoke! config cache metrics id nil f) - (f))) - - ([{:keys [::id ::cache ::config ::mtx/metrics]} f executor] - (let [f #(p/await! (px/submit! executor f))] - (if-let [config (get config id)] - (invoke! config cache metrics id nil f) - (f))))) - + [{:keys [::executor] :as cfg} f & params] + (let [f (if (some? executor) + (fn [& params] (px/await! (px/submit! executor (fn [] (apply f params))))) + f) + f (build-exec-chain cfg f)] + (apply f params))) diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index c9b55b599e..2e82e56402 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -21,6 +21,7 @@ [app.loggers.audit :as audit] [app.main :as-alias main] [app.rpc :as-alias rpc] + [app.rpc.climit :as-alias climit] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] @@ -39,7 +40,7 @@ ;; ---- COMMAND: login with password (defn login-with-password - [{:keys [::db/pool] :as cfg} {:keys [email password] :as params}] + [cfg {:keys [email password] :as params}] (when-not (or (contains? cf/flags :login) (contains? cf/flags :login-with-password)) @@ -47,7 +48,7 @@ :code :login-disabled :hint "login is disabled in this instance")) - (letfn [(check-password [conn profile password] + (letfn [(check-password [cfg profile password] (if (= (:password profile) "!") (ex/raise :type :validation :code :account-without-password @@ -57,10 +58,10 @@ (l/trc :hint "updating profile password" :id (str (:id profile)) :email (:email profile)) - (profile/update-profile-password! conn (assoc profile :password password))) + (profile/update-profile-password! cfg (assoc profile :password password))) (:valid result)))) - (validate-profile [conn profile] + (validate-profile [cfg profile] (when-not profile (ex/raise :type :validation :code :wrong-credentials)) @@ -70,7 +71,7 @@ (when (:is-blocked profile) (ex/raise :type :restriction :code :profile-blocked)) - (when-not (check-password conn profile password) + (when-not (check-password cfg profile password) (ex/raise :type :validation :code :wrong-credentials)) (when-let [deleted-at (:deleted-at profile)] @@ -78,27 +79,29 @@ (ex/raise :type :validation :code :wrong-credentials))) - profile)] + profile) - (db/with-atomic [conn pool] - (let [profile (->> (profile/get-profile-by-email conn email) - (validate-profile conn) - (profile/strip-private-attrs)) + (login [{:keys [::db/conn] :as cfg}] + (let [profile (->> (profile/get-profile-by-email conn email) + (validate-profile cfg) + (profile/strip-private-attrs)) - invitation (when-let [token (:invitation-token params)] - (tokens/verify (::main/props cfg) {:token token :iss :team-invitation})) + invitation (when-let [token (:invitation-token params)] + (tokens/verify (::main/props cfg) {:token token :iss :team-invitation})) - ;; If invitation member-id does not matches the profile-id, we just proceed to ignore the - ;; invitation because invitations matches exactly; and user can't login with other email and - ;; accept invitation with other email - response (if (and (some? invitation) (= (:id profile) (:member-id invitation))) - {:invitation-token (:invitation-token params)} - (assoc profile :is-admin (let [admins (cf/get :admins)] - (contains? admins (:email profile)))))] - (-> response - (rph/with-transform (session/create-fn cfg (:id profile))) - (rph/with-meta {::audit/props (audit/profile->props profile) - ::audit/profile-id (:id profile)})))))) + ;; If invitation member-id does not matches the profile-id, we just proceed to ignore the + ;; invitation because invitations matches exactly; and user can't login with other email and + ;; accept invitation with other email + response (if (and (some? invitation) (= (:id profile) (:member-id invitation))) + {:invitation-token (:invitation-token params)} + (assoc profile :is-admin (let [admins (cf/get :admins)] + (contains? admins (:email profile)))))] + (-> response + (rph/with-transform (session/create-fn cfg (:id profile))) + (rph/with-meta {::audit/props (audit/profile->props profile) + ::audit/profile-id (:id profile)}))))] + + (db/tx-run! cfg login))) (def schema:login-with-password [:map {:title "login-with-password"} @@ -110,6 +113,7 @@ "Performs authentication using penpot password." {::rpc/auth false ::doc/added "1.15" + ::climit/id :auth/global ::sm/params schema:login-with-password} [cfg params] (login-with-password cfg params)) @@ -149,7 +153,8 @@ (sv/defmethod ::recover-profile {::rpc/auth false ::doc/added "1.15" - ::sm/params schema:recover-profile} + ::sm/params schema:recover-profile + ::climit/id :auth/global} [cfg params] (recover-profile cfg params)) @@ -360,7 +365,6 @@ {::audit/type "fact" ::audit/name "register-profile-retry" ::audit/profile-id id})) - (cond ;; If invitation token comes in params, this is because the ;; user comes from team-invitation process; in this case, @@ -402,7 +406,6 @@ {::audit/replace-props (audit/profile->props profile) ::audit/profile-id (:id profile)}))))) - (def schema:register-profile [:map {:title "register-profile"} [:token schema:token] @@ -411,7 +414,8 @@ (sv/defmethod ::register-profile {::rpc/auth false ::doc/added "1.15" - ::sm/params schema:register-profile} + ::sm/params schema:register-profile + ::climit/id :auth/global} [{:keys [::db/pool] :as cfg} params] (db/with-atomic [conn pool] (-> (assoc cfg ::db/conn conn) diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index f47300bdee..a44a8bdbd5 100644 --- a/backend/src/app/rpc/commands/files_thumbnails.clj +++ b/backend/src/app/rpc/commands/files_thumbnails.clj @@ -285,12 +285,10 @@ (sv/defmethod ::create-file-object-thumbnail {::doc/added "1.19" ::doc/module :files - ::climit/id :file-thumbnail-ops/by-profile - ::climit/key-fn ::rpc/profile-id - + ::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id] + [:file-thumbnail-ops/global]] ::rtry/enabled true ::rtry/when rtry/conflict-exception? - ::audit/skip true ::sm/params schema:create-file-object-thumbnail} @@ -332,8 +330,8 @@ {::doc/added "1.19" ::doc/module :files ::doc/deprecated "1.20" - ::climit/id :file-thumbnail-ops - ::climit/key-fn ::rpc/profile-id + ::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id] + [:file-thumbnail-ops/global]] ::audit/skip true} [cfg {:keys [::rpc/profile-id file-id object-id]}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] @@ -408,8 +406,8 @@ {::doc/added "1.19" ::doc/module :files ::audit/skip true - ::climit/id :file-thumbnail-ops - ::climit/key-fn ::rpc/profile-id + ::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id] + [:file-thumbnail-ops/global]] ::rtry/enabled true ::rtry/when rtry/conflict-exception? ::sm/params schema:create-file-thumbnail} diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj index 134a127946..fade957e03 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -35,7 +35,8 @@ [app.util.services :as sv] [app.util.time :as dt] [app.worker :as-alias wrk] - [clojure.set :as set])) + [clojure.set :as set] + [promesa.exec :as px])) ;; --- SCHEMA @@ -132,8 +133,8 @@ ;; database. (sv/defmethod ::update-file - {::climit/id :update-file/by-profile - ::climit/key-fn ::rpc/profile-id + {::climit/id [[:update-file/by-profile ::rpc/profile-id] + [:update-file/global]] ::webhooks/event? true ::webhooks/batch-timeout (dt/duration "2m") ::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id) @@ -232,13 +233,9 @@ (defn- update-file* [{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [profile-id file changes session-id ::created-at skip-validate] :as params}] - (let [;; Process the file data in the CLIMIT context; scheduling it - ;; to be executed on a separated executor for avoid to do the - ;; CPU intensive operation on vthread. - - update-fdata-fn (partial update-file-data cfg file changes skip-validate) - file (-> (climit/configure cfg :update-file/global) - (climit/run! update-fdata-fn executor))] + (let [;; Process the file data on separated thread for avoid to do + ;; the CPU intensive operation on vthread. + file (px/invoke! executor (partial update-file-data cfg file changes skip-validate))] (db/insert! conn :file-change {:id (uuid/next) @@ -306,7 +303,6 @@ (fmg/migrate-file)) file) - ;; WARNING: this ruins performance; maybe we need to find ;; some other way to do general validation libs (when (and (or (contains? cf/flags :file-validation) diff --git a/backend/src/app/rpc/commands/fonts.clj b/backend/src/app/rpc/commands/fonts.clj index c19b8a2854..0942da601d 100644 --- a/backend/src/app/rpc/commands/fonts.clj +++ b/backend/src/app/rpc/commands/fonts.clj @@ -16,7 +16,7 @@ [app.loggers.webhooks :as-alias webhooks] [app.media :as media] [app.rpc :as-alias rpc] - [app.rpc.climit :as climit] + [app.rpc.climit :as-alias climit] [app.rpc.commands.files :as files] [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] @@ -26,7 +26,8 @@ [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as-alias wrk])) + [app.worker :as-alias wrk] + [promesa.exec :as px])) (def valid-weight #{100 200 300 400 500 600 700 800 900 950}) (def valid-style #{"normal" "italic"}) @@ -87,6 +88,8 @@ (sv/defmethod ::create-font-variant {::doc/added "1.18" + ::climit/id [[:process-font/by-profile ::rpc/profile-id] + [:process-font/global]] ::webhooks/event? true ::sm/params schema:create-font-variant} [cfg {:keys [::rpc/profile-id team-id] :as params}] @@ -100,7 +103,7 @@ (create-font-variant cfg (assoc params :profile-id profile-id)))))) (defn create-font-variant - [{:keys [::sto/storage ::db/conn] :as cfg} {:keys [data] :as params}] + [{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}] (letfn [(generate-missing! [data] (let [data (media/run {:cmd :generate-fonts :input data})] (when (and (not (contains? data "font/otf")) @@ -152,9 +155,7 @@ :otf-file-id (:id otf) :ttf-file-id (:id ttf)}))] - (let [data (-> (climit/configure cfg :process-font/global) - (climit/run! (partial generate-missing! data) - (::wrk/executor cfg))) + (let [data (px/invoke! executor (partial generate-missing! data)) assets (persist-fonts-files! data) result (insert-font-variant! assets)] (vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys)))))) diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj index a3dc357db5..1bdcd3c502 100644 --- a/backend/src/app/rpc/commands/media.clj +++ b/backend/src/app/rpc/commands/media.clj @@ -27,7 +27,8 @@ [app.worker :as-alias wrk] [clojure.spec.alpha :as s] [cuerdas.core :as str] - [datoteka.io :as io])) + [datoteka.io :as io] + [promesa.exec :as px])) (def default-max-file-size (* 1024 1024 10)) ; 10 MiB @@ -56,20 +57,25 @@ :opt-un [::id])) (sv/defmethod ::upload-file-media-object - {::doc/added "1.17"} + {::doc/added "1.17" + ::climit/id [[:process-image/by-profile ::rpc/profile-id] + [:process-image/global]]} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}] (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] + (files/check-edition-permissions! pool profile-id file-id) (media/validate-media-type! content) (media/validate-media-size! content) - (let [object (db/run! cfg #(create-file-media-object % params)) - props {:name (:name params) - :file-id file-id - :is-local (:is-local params) - :size (:size content) - :mtype (:mtype content)}] - (with-meta object - {::audit/replace-props props})))) + + (db/run! cfg (fn [cfg] + (let [object (create-file-media-object cfg params) + props {:name (:name params) + :file-id file-id + :is-local (:is-local params) + :size (:size content) + :mtype (:mtype content)}] + (with-meta object + {::audit/replace-props props})))))) (defn- big-enough-for-thumbnail? "Checks if the provided image info is big enough for @@ -144,12 +150,10 @@ (assoc ::image (process-main-image info))))) (defn create-file-media-object - [{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg} + [{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [id file-id is-local name content]}] - (let [result (-> (climit/configure cfg :process-image/global) - (climit/run! (partial process-image content) executor)) - + (let [result (px/invoke! executor (partial process-image content)) image (sto/put-object! storage (::image result)) thumb (when-let [params (::thumb result)] (sto/put-object! storage params))] @@ -183,7 +187,7 @@ [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) - (db/run! cfg #(create-file-media-object-from-url % params)))) + (create-file-media-object-from-url cfg (assoc params :profile-id profile-id)))) (defn download-image [{:keys [::http/client]} uri] @@ -235,7 +239,16 @@ params (-> params (assoc :content content) (assoc :name (or name (:filename content))))] - (create-file-media-object cfg params))) + + ;; NOTE: we use the climit here in a dynamic invocation because we + ;; don't want saturate the process-image limit with IO (download + ;; of external image) + (-> cfg + (assoc ::climit/id [[:process-image/by-profile (:profile-id params)] + [:process-image/global]]) + (assoc ::climit/profile-id (:profile-id params)) + (assoc ::climit/label "create-file-media-object-from-url") + (climit/invoke! db/run! cfg create-file-media-object params)))) ;; --- Clone File Media object (Upload and create from url) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 5b814abe62..a2fa82ba49 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -28,7 +28,8 @@ [app.util.services :as sv] [app.util.time :as dt] [app.worker :as-alias wrk] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [promesa.exec :as px])) (declare check-profile-existence!) (declare decode-row) @@ -137,25 +138,24 @@ [:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]])) (sv/defmethod ::update-profile-password - {:doc/added "1.0" + {::doc/added "1.0" ::sm/params schema:update-profile-password - ::sm/result :nil} + ::climit/id :auth/global} + [cfg {:keys [::rpc/profile-id password] :as params}] - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}] - (db/with-atomic [conn pool] - (let [cfg (assoc cfg ::db/conn conn) - profile (validate-password! cfg (assoc params :profile-id profile-id)) - session-id (::session/id params)] + (db/tx-run! cfg (fn [cfg] + (let [profile (validate-password! cfg (assoc params :profile-id profile-id)) + session-id (::session/id params)] - (when (= (str/lower (:email profile)) - (str/lower (:password params))) - (ex/raise :type :validation - :code :email-as-password - :hint "you can't use your email as password")) + (when (= (str/lower (:email profile)) + (str/lower (:password params))) + (ex/raise :type :validation + :code :email-as-password + :hint "you can't use your email as password")) - (update-profile-password! conn (assoc profile :password password)) - (invalidate-profile-session! cfg profile-id session-id) - nil))) + (update-profile-password! cfg (assoc profile :password password)) + (invalidate-profile-session! cfg profile-id session-id) + nil)))) (defn- invalidate-profile-session! "Removes all sessions except the current one." @@ -173,10 +173,10 @@ profile)) (defn update-profile-password! - [conn {:keys [id password] :as profile}] + [{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}] (when-not (db/read-only? conn) (db/update! conn :profile - {:password (auth/derive-password password)} + {:password (derive-password cfg password)} {:id id}) nil)) @@ -203,6 +203,7 @@ (defn update-profile-photo [{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}] + (let [photo (upload-photo cfg params) profile (db/get-by-id pool :profile profile-id ::sql/for-update true)] @@ -241,8 +242,11 @@ (defn upload-photo [{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file]}] - (let [params (-> (climit/configure cfg :process-image/global) - (climit/run! (partial generate-thumbnail! file) executor))] + (let [params (-> cfg + (assoc ::climit/id :process-image/global) + (assoc ::climit/label "upload-photo") + (assoc ::climit/executor executor) + (climit/invoke! generate-thumbnail! file))] (sto/put-object! storage params))) @@ -438,17 +442,13 @@ (into {} (filter (fn [[k _]] (simple-ident? k))) props)) (defn derive-password - [cfg password] + [{:keys [::wrk/executor]} password] (when password - (-> (climit/configure cfg :derive-password/global) - (climit/run! (partial auth/derive-password password) - (::wrk/executor cfg))))) + (px/invoke! executor (partial auth/derive-password password)))) (defn verify-password - [cfg password password-data] - (-> (climit/configure cfg :derive-password/global) - (climit/run! (partial auth/verify-password password password-data) - (::wrk/executor cfg)))) + [{:keys [::wrk/executor]} password password-data] + (px/invoke! executor (partial auth/verify-password password password-data))) (defn decode-row [{:keys [props] :as row}] diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 8073c40a7a..ad08d5b625 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -71,6 +71,7 @@ :enable-email-verification :enable-smtp :enable-quotes + :enable-rpc-climit :enable-feature-fdata-pointer-map :enable-feature-fdata-objets-map :enable-feature-components-v2 diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index fe7a0e8f56..d7780ef70e 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -319,6 +319,12 @@ ::message (delay ~message)}) nil))) +(defmacro log + [level & params] + `(do + (log! ::logger ~(str *ns*) ::level ~level ~@params) + nil)) + (defmacro info [& params] `(do