From 638cf6daff07e26fd4785ceb577ea04bc1293ef5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Mar 2024 14:13:50 +0100 Subject: [PATCH 01/19] :lipstick: Add cosmetic enhancements to viewport comments layer That also improves performance --- frontend/src/app/main/ui/comments.cljs | 1 + .../main/ui/workspace/viewport/comments.cljs | 70 ++++++++++--------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index 1147cf99a..938d108a1 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -391,6 +391,7 @@ (mf/with-layout-effect [thread-pos comments-map] (when-let [node (mf/ref-val ref)] (dom/scroll-into-view-if-needed! node))) + (when (some? comment) [:div {:class (stl/css :thread-content) :style {:top (str pos-y "px") diff --git a/frontend/src/app/main/ui/workspace/viewport/comments.cljs b/frontend/src/app/main/ui/workspace/viewport/comments.cljs index ce44c0292..6a54a8e26 100644 --- a/frontend/src/app/main/ui/workspace/viewport/comments.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/comments.cljs @@ -7,61 +7,67 @@ (ns app.main.ui.workspace.viewport.comments (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.main.data.comments :as dcm] [app.main.data.workspace.comments :as dwcm] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.comments :as cmt] - [cuerdas.core :as str] [okulary.core :as l] [rumext.v2 :as mf])) +(defn- update-position + [positions {:keys [id] :as thread}] + (if (contains? positions id) + (-> thread + (assoc :position (dm/get-in positions [id :position])) + (assoc :frame-id (dm/get-in positions [id :frame-id]))) + thread)) + (mf/defc comments-layer + {::mf/props :obj} [{:keys [vbox vport zoom file-id page-id drawing] :as props}] - (let [pos-x (* (- (:x vbox)) zoom) - pos-y (* (- (:y vbox)) zoom) + (let [pos-x (* (- (:x vbox)) zoom) + pos-y (* (- (:y vbox)) zoom) - profile (mf/deref refs/profile) - users (mf/deref refs/current-file-comments-users) - local (mf/deref refs/comments-local) - threads-position-ref (l/derived (l/in [:workspace-data :pages-index page-id :options :comment-threads-position]) st/state) - threads-position-map (mf/deref threads-position-ref) - threads-map (mf/deref refs/threads-ref) + profile (mf/deref refs/profile) + users (mf/deref refs/current-file-comments-users) + local (mf/deref refs/comments-local) - update-thread-position (fn update-thread-position [thread] - (if (contains? threads-position-map (:id thread)) - (-> thread - (assoc :position (get-in threads-position-map [(:id thread) :position])) - (assoc :frame-id (get-in threads-position-map [(:id thread) :frame-id]))) - thread)) + positions-ref + (mf/with-memo [page-id] + (-> (l/in [:workspace-data :pages-index page-id :options :comment-threads-position]) + (l/derived st/state))) - threads (->> (vals threads-map) - (filter #(= (:page-id %) page-id)) - (mapv update-thread-position) - (dcm/apply-filters local profile)) + positions (mf/deref positions-ref) + threads-map (mf/deref refs/threads-ref) + + threads + (mf/with-memo [threads-map positions local profile] + (->> (vals threads-map) + (filter #(= (:page-id %) page-id)) + (mapv (partial update-position positions)) + (dcm/apply-filters local profile))) on-draft-cancel - (mf/use-callback - #(st/emit! :interrupt)) + (mf/use-fn #(st/emit! :interrupt)) on-draft-submit - (mf/use-callback + (mf/use-fn (fn [draft] (st/emit! (dcm/create-thread-on-workspace draft))))] - (mf/use-effect - (mf/deps file-id) - (fn [] - (st/emit! (dwcm/initialize-comments file-id)) - (fn [] - (st/emit! ::dwcm/finalize)))) + (mf/with-effect [file-id] + (st/emit! (dwcm/initialize-comments file-id)) + (fn [] (st/emit! ::dwcm/finalize))) + [:div {:class (stl/css :comments-section)} [:div {:class (stl/css :workspace-comments-container) - :style {:width (str (:width vport) "px") - :height (str (:height vport) "px")}} + :style {:width (dm/str (:width vport) "px") + :height (dm/str (:height vport) "px")}} [:div {:class (stl/css :threads) - :style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}} + :style {:transform (dm/fmt "translate(%px, %px)" pos-x pos-y)}} (for [item threads] [:& cmt/thread-bubble {:thread item :zoom zoom @@ -70,7 +76,7 @@ (when-let [id (:open local)] (when-let [thread (get threads-map id)] - [:& cmt/thread-comments {:thread (update-thread-position thread) + [:& cmt/thread-comments {:thread (update-position positions thread) :users users :zoom zoom}])) From 4106e8da56814bf101fb7d7afcea5c09dc7f681f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Mar 2024 14:16:57 +0100 Subject: [PATCH 02/19] :zap: Add performance enhancements to viewport comments layer --- .../app/main/ui/workspace/viewport/comments.cljs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/viewport/comments.cljs b/frontend/src/app/main/ui/workspace/viewport/comments.cljs index 6a54a8e26..8adbce9cd 100644 --- a/frontend/src/app/main/ui/workspace/viewport/comments.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/comments.cljs @@ -27,8 +27,13 @@ (mf/defc comments-layer {::mf/props :obj} [{:keys [vbox vport zoom file-id page-id drawing] :as props}] - (let [pos-x (* (- (:x vbox)) zoom) - pos-y (* (- (:y vbox)) zoom) + (let [vbox-x (dm/get-prop vbox :x) + vbox-y (dm/get-prop vbox :y) + vport-w (dm/get-prop vport :width) + vport-h (dm/get-prop vport :height) + + pos-x (* (- vbox-x) zoom) + pos-y (* (- vbox-y) zoom) profile (mf/deref refs/profile) users (mf/deref refs/current-file-comments-users) @@ -64,8 +69,8 @@ [:div {:class (stl/css :comments-section)} [:div {:class (stl/css :workspace-comments-container) - :style {:width (dm/str (:width vport) "px") - :height (dm/str (:height vport) "px")}} + :style {:width (dm/str vport-w "px") + :height (dm/str vport-h "px")}} [:div {:class (stl/css :threads) :style {:transform (dm/fmt "translate(%px, %px)" pos-x pos-y)}} (for [item threads] From 43cd4656b4e4e2c8d2bd926b78a5846f4f06e0b0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Mar 2024 15:57:26 +0100 Subject: [PATCH 03/19] :zap: Remove props wrapping on workspace comment react components --- frontend/src/app/main/ui/workspace/comments.cljs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/app/main/ui/workspace/comments.cljs b/frontend/src/app/main/ui/workspace/comments.cljs index d74fa287b..19fce235f 100644 --- a/frontend/src/app/main/ui/workspace/comments.cljs +++ b/frontend/src/app/main/ui/workspace/comments.cljs @@ -27,6 +27,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (mf/defc sidebar-options + {::mf/props :obj} [{:keys [from-viewer]}] (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) update-mode @@ -67,6 +68,7 @@ [:span {:class (stl/css :icon)} i/tick-refactor]]])) (mf/defc comments-sidebar + {::mf/props :obj} [{:keys [users threads page-id from-viewer]}] (let [threads-map (mf/deref refs/threads-ref) profile (mf/deref refs/profile) From ee91ab5dadcc61a506f4a398ee95fa3ea5eeb7f7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Mar 2024 16:11:50 +0100 Subject: [PATCH 04/19] :zap: Add nano optimizations to fo_text react component --- .../src/app/main/ui/shapes/text/fo_text.cljs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/main/ui/shapes/text/fo_text.cljs b/frontend/src/app/main/ui/shapes/text/fo_text.cljs index 8e2b1e528..7e2ebc504 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -8,6 +8,7 @@ (:require [app.common.colors :as cc] [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.text.styles :as sts] @@ -169,16 +170,16 @@ [colors color-mapping color-mapping-inverse])) (mf/defc text-shape - {::mf/wrap-props false + {::mf/props :obj ::mf/forward-ref true} - [props ref] - (let [shape (obj/get props "shape") - transform (gsh/transform-str shape) - - {:keys [id x y width height content]} shape - grow-type (obj/get props "grow-type") ;; This is only needed in workspace - ;; We add 8px to add a padding for the exporter - ;; width (+ width 8) + [{:keys [shape grow-type]} ref] + (let [transform (gsh/transform-str shape) + id (dm/get-prop shape :id) + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + width (dm/get-prop shape :width) + height (dm/get-prop shape :height) + content (get shape :content) [colors _color-mapping color-mapping-inverse] (retrieve-colors shape)] @@ -186,7 +187,7 @@ {:x x :y y :id id - :data-colors (->> colors (str/join ",")) + :data-colors (str/join "," colors) :data-mapping (-> color-mapping-inverse clj->js js/JSON.stringify) :transform transform :width (if (#{:auto-width} grow-type) 100000 width) From 85d06b10c2cf84c679000f93aa8c590655635af2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Mar 2024 16:18:57 +0100 Subject: [PATCH 05/19] :bug: Fix incorrect event handling on component annotation creation --- .../main/ui/workspace/sidebar/options/menus/component.cljs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 a016f99f3..84a888fba 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 @@ -110,9 +110,10 @@ (let [text (dom/get-value textarea)] (when-not (str/blank? text) (reset! editing* false) + (st/emit! (dw/update-component-annotation component-id text)) (when ^boolean creating? - (st/emit! (dw/set-annotations-id-for-create nil))) - (dw/update-component-annotation component-id text)))))) + (st/emit! (dw/set-annotations-id-for-create nil)))))))) + on-delete-annotation (mf/use-fn From 34126582864ae0016551ebf63c235333a965c124 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Mar 2024 08:46:41 +0100 Subject: [PATCH 06/19] :sparkles: Move some functions from file helpers to types.shape.layout --- common/src/app/common/files/helpers.cljc | 17 ----------------- common/src/app/common/types/shape/layout.cljc | 16 ++++++++++++++++ frontend/src/app/main/ui/shapes/frame.cljs | 2 +- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/common/src/app/common/files/helpers.cljc b/common/src/app/common/files/helpers.cljc index 296d4da83..5b31328e6 100644 --- a/common/src/app/common/files/helpers.cljc +++ b/common/src/app/common/files/helpers.cljc @@ -12,7 +12,6 @@ [app.common.schema :as sm] [app.common.types.components-list :as ctkl] [app.common.types.pages-list :as ctpl] - [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [clojure.set :as set] [cuerdas.core :as str])) @@ -741,22 +740,6 @@ (d/seek root-frame?) :id)) -(defn comparator-layout-z-index - [[idx-a child-a] [idx-b child-b]] - (cond - (> (ctl/layout-z-index child-a) (ctl/layout-z-index child-b)) 1 - (< (ctl/layout-z-index child-a) (ctl/layout-z-index child-b)) -1 - (< idx-a idx-b) 1 - (> idx-a idx-b) -1 - :else 0)) - -(defn sort-layout-children-z-index - [children] - (->> children - (d/enumerate) - (sort comparator-layout-z-index) - (mapv second))) - (defn common-parent-frame "Search for the common frame for the selected shapes. Otherwise returns the root frame" [objects selected] diff --git a/common/src/app/common/types/shape/layout.cljc b/common/src/app/common/types/shape/layout.cljc index 7c60ad412..b65f7b83b 100644 --- a/common/src/app/common/types/shape/layout.cljc +++ b/common/src/app/common/types/shape/layout.cljc @@ -536,6 +536,22 @@ ([shape] (or (:layout-item-z-index shape) 0))) +(defn- comparator-layout-z-index + [[idx-a child-a] [idx-b child-b]] + (cond + (> (layout-z-index child-a) (layout-z-index child-b)) 1 + (< (layout-z-index child-a) (layout-z-index child-b)) -1 + (< idx-a idx-b) 1 + (> idx-a idx-b) -1 + :else 0)) + +(defn sort-layout-children-z-index + [children] + (->> children + (d/enumerate) + (sort comparator-layout-z-index) + (mapv second))) + (defn change-h-sizing? [frame-id objects children-ids] (and (flex-layout? objects frame-id) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 705ce3233..64bd709f9 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -168,7 +168,7 @@ childs (unchecked-get props "childs") childs (cond-> childs (ctl/any-layout? shape) - (cfh/sort-layout-children-z-index))] + (ctl/sort-layout-children-z-index))] [:> frame-container props [:g.frame-children {:opacity (:opacity shape)} From 6fe85465a12ac263ab371afc2b7573359428d838 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Mar 2024 08:51:15 +0100 Subject: [PATCH 07/19] :zap: Add minor performance enhacement on shape layout functions --- common/src/app/common/types/shape/layout.cljc | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/common/src/app/common/types/shape/layout.cljc b/common/src/app/common/types/shape/layout.cljc index b65f7b83b..ea2f69dae 100644 --- a/common/src/app/common/types/shape/layout.cljc +++ b/common/src/app/common/types/shape/layout.cljc @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.shapes.grid-layout.areas :as sga] [app.common.math :as mth] [app.common.schema :as sm] @@ -47,7 +48,8 @@ #{:flex :grid}) (def flex-direction-types - #{:row :reverse-row :row-reverse :column :reverse-column :column-reverse}) ;;TODO remove reverse-column and reverse-row after script + ;;TODO remove reverse-column and reverse-row after script + #{:row :reverse-row :row-reverse :column :reverse-column :column-reverse}) (def grid-direction-types #{:row :column}) @@ -128,7 +130,7 @@ (def grid-cell-justify-self-types #{:auto :start :center :end :stretch}) -(sm/define! ::grid-cell +(sm/def! ::grid-cell [:map {:title "GridCell"} [:id ::sm/uuid] [:area-name {:optional true} :string] @@ -142,7 +144,7 @@ [:shapes [:vector {:gen/max 1} ::sm/uuid]]]) -(sm/define! ::grid-track +(sm/def! ::grid-track [:map {:title "GridTrack"} [:type [::sm/one-of grid-track-types]] [:value {:optional true} [:maybe ::sm/safe-number]]]) @@ -197,14 +199,14 @@ ([objects id] (flex-layout? (get objects id))) ([shape] - (and (= :frame (:type shape)) + (and (cfh/frame-shape? shape) (= :flex (:layout shape))))) (defn grid-layout? ([objects id] (grid-layout? (get objects id))) ([shape] - (and (= :frame (:type shape)) + (and (cfh/frame-shape? shape) (= :grid (:layout shape))))) (defn any-layout? @@ -212,7 +214,10 @@ (any-layout? (get objects id))) ([shape] - (or (flex-layout? shape) (grid-layout? shape)))) + (and (cfh/frame-shape? shape) + (let [layout (:layout shape)] + (or (= :flex layout) + (= :grid layout)))))) (defn flex-layout-immediate-child? [objects shape] (let [parent-id (:parent-id shape) From d2059475f00bbb8c6c597613606f31a8c8ea852d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Mar 2024 10:58:10 +0100 Subject: [PATCH 08/19] :zap: Add minor performance enhancement for `inside-layout?` helper --- common/src/app/common/types/shape/layout.cljc | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/common/src/app/common/types/shape/layout.cljc b/common/src/app/common/types/shape/layout.cljc index ea2f69dae..c39545041 100644 --- a/common/src/app/common/types/shape/layout.cljc +++ b/common/src/app/common/types/shape/layout.cljc @@ -267,20 +267,21 @@ (defn inside-layout? "Check if the shape is inside a layout" [objects shape] - - (loop [current-id (:id shape)] - (let [current (get objects current-id)] + (loop [current-id (dm/get-prop shape :id)] + (let [current (get objects current-id) + parent-id (dm/get-prop current :parent-id)] (cond - (or (nil? current) (= current-id (:parent-id current))) + (or (nil? current) (= current-id parent-id)) false - (= :frame (:type current)) + (cfh/frame-shape? current-id) (:layout current) :else - (recur (:parent-id current)))))) + (recur parent-id))))) -(defn wrap? [{:keys [layout-wrap-type]}] +(defn wrap? + [{:keys [layout-wrap-type]}] (= layout-wrap-type :wrap)) (defn fill-width? From cac785f3e14bb98244ffea0dc483d9565d83b8dc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Mar 2024 11:05:32 +0100 Subject: [PATCH 09/19] :lipstick: Add cosmetic improvements to dashboard import modal code --- .../src/app/main/ui/dashboard/import.cljs | 81 ++++++++++--------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 8bca69f80..2d218e06c 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -36,7 +36,7 @@ (defn use-import-file [project-id on-finish-import] - (mf/use-callback + (mf/use-fn (mf/deps project-id on-finish-import) (fn [files] (when files @@ -52,7 +52,9 @@ :on-finish-import on-finish-import}))))))) (mf/defc import-form - {::mf/forward-ref true} + {::mf/forward-ref true + ::mf/props :obj} + [{:keys [project-id on-finish-import]} external-ref] (let [on-file-selected (use-import-file project-id on-finish-import)] @@ -62,23 +64,25 @@ :ref external-ref :on-selected on-file-selected}]])) -(defn update-file [files file-id new-name] - (->> files - (mapv - (fn [file] - (let [new-name (str/trim new-name)] - (cond-> file - (and (= (:file-id file) file-id) - (not= "" new-name)) - (assoc :name new-name))))))) +(defn update-file + [files file-id new-name] + (mapv + (fn [file] + (let [new-name (str/trim new-name)] + (cond-> file + (and (= (:file-id file) file-id) + (not= "" new-name)) + (assoc :name new-name)))) + files)) -(defn remove-file [files file-id] - (->> files - (mapv - (fn [file] - (cond-> file - (= (:file-id file) file-id) - (assoc :deleted? true)))))) +(defn remove-file + [files file-id] + (mapv + (fn [file] + (cond-> file + (= (:file-id file) file-id) + (assoc :deleted? true))) + files)) (defn set-analyze-error [files uri error] @@ -89,7 +93,8 @@ (-> (assoc :status :analyze-error) (assoc :error error))))))) -(defn set-analyze-result [files uri type data] +(defn set-analyze-result + [files uri type data] (let [existing-files? (into #{} (->> files (map :file-id) (filter some?))) replace-file (fn [file] @@ -106,12 +111,14 @@ [file]))] (into [] (mapcat replace-file) files))) -(defn mark-files-importing [files] +(defn mark-files-importing + [files] (->> files (filter #(= :ready (:status %))) (mapv #(assoc % :status :importing)))) -(defn update-status [files file-id status progress errors] +(defn update-status + [files file-id status progress errors] (->> files (mapv (fn [file] (cond-> file @@ -124,7 +131,7 @@ (= file-id (:file-id file)) (assoc :errors errors)))))) -(defn parse-progress-message +(defn- parse-progress-message [message] (case (:type message) :upload-data @@ -151,7 +158,8 @@ (str message))) (mf/defc import-entry - [{:keys [state file editing? can-be-deleted?]}] + {::mf/props :obj} + [{:keys [state file editing? can-be-deleted]}] (let [loading? (or (= :analyzing (:status file)) (= :importing (:status file))) analyze-error? (= :analyze-error (:status file)) @@ -163,7 +171,7 @@ progress (:progress file) handle-edit-key-press - (mf/use-callback + (mf/use-fn (fn [e] (when (or (kbd/enter? e) (kbd/esc? e)) (dom/prevent-default e) @@ -171,7 +179,7 @@ (dom/blur! (dom/get-target e))))) handle-edit-blur - (mf/use-callback + (mf/use-fn (mf/deps file) (fn [e] (let [value (dom/get-target-val e)] @@ -179,13 +187,13 @@ (update :files update-file (:file-id file) value)))))) handle-edit-entry - (mf/use-callback + (mf/use-fn (mf/deps file) (fn [] (swap! state assoc :editing (:file-id file)))) handle-remove-entry - (mf/use-callback + (mf/use-fn (mf/deps file) (fn [] (swap! state update :files remove-file (:file-id file))))] @@ -224,7 +232,7 @@ [:div {:class (stl/css :edit-entry-buttons)} (when (= "application/zip" (:type file)) [:button {:on-click handle-edit-entry} i/curve-refactor]) - (when can-be-deleted? + (when can-be-deleted [:button {:on-click handle-remove-entry} i/delete-refactor])]] (cond analyze-error? @@ -252,7 +260,8 @@ (mf/defc import-dialog {::mf/register modal/components - ::mf/register-as :import} + ::mf/register-as :import + ::mf/props :obj} [{:keys [project-id files template on-finish-import]}] (let [state (mf/use-state {:status :analyzing @@ -262,7 +271,7 @@ (mapv #(assoc % :status :analyzing)))}) analyze-import - (mf/use-callback + (mf/use-fn (fn [files] (->> (uw/ask-many! {:cmd :analyze-import @@ -277,7 +286,7 @@ (swap! state update :files set-analyze-result uri type data))))))) import-files - (mf/use-callback + (mf/use-fn (fn [project-id files] (st/emit! (ptk/event ::ev/event {::ev/name "import-files" :num-files (count files)})) @@ -291,7 +300,7 @@ (swap! state update :files update-status file-id status message errors)))))) handle-cancel - (mf/use-callback + (mf/use-fn (mf/deps (:editing @state)) (fn [event] (when (nil? (:editing @state)) @@ -334,7 +343,7 @@ handle-continue - (mf/use-callback + (mf/use-fn (mf/deps project-id (:files @state)) (fn [event] (dom/prevent-default event) @@ -343,7 +352,7 @@ (continue-files)))) handle-accept - (mf/use-callback + (mf/use-fn (fn [event] (dom/prevent-default event) (st/emit! (modal/hide)) @@ -400,13 +409,13 @@ :key (dm/str (:uri file)) :file file :editing? editing? - :can-be-deleted? (> (count files) 1)}])) + :can-be-deleted (> (count files) 1)}])) (when (some? template) [:& import-entry {:state state :file (assoc template :status (if (= 1 (:importing-templates @state)) :importing :ready)) :editing? false - :can-be-deleted? false}])] + :can-be-deleted false}])] [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} From afd373ffeefaf6b8992b449d8ab54d4e6b3a46ec Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Mar 2024 17:07:41 +0100 Subject: [PATCH 10/19] :sparkles: Simplify implementation of d/name --- common/src/app/common/data.cljc | 21 +++++++++---------- .../sidebar/options/menus/constraints.cljs | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 0dcf23e7e..736803631 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -716,20 +716,19 @@ (defn name "Improved version of name that won't fail if the input is not a keyword" - ([maybe-keyword] (name maybe-keyword nil)) - ([maybe-keyword default-value] - (cond - (keyword? maybe-keyword) - (c/name maybe-keyword) + [maybe-keyword] + (cond + (nil? maybe-keyword) + nil - (string? maybe-keyword) - maybe-keyword + (keyword? maybe-keyword) + (c/name maybe-keyword) - (nil? maybe-keyword) default-value + (string? maybe-keyword) + maybe-keyword - :else - (or default-value - (str maybe-keyword))))) + :else + (str maybe-keyword))) (defn prefix-keyword "Given a keyword and a prefix will return a new keyword with the prefix attached diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs index 44d5182ec..538b4f960 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs @@ -209,12 +209,12 @@ [:div {:class (stl/css :contraints-selects)} [:div {:class (stl/css :horizontal-select)} [:& select - {:default-value (d/name constraints-h "scale") + {:default-value (d/nilv (d/name constraints-h) "scale") :options options-h :on-change on-constraint-h-select-changed}]] [:div {:class (stl/css :vertical-select)} [:& select - {:default-value (d/name constraints-v "scale") + {:default-value (d/nilv (d/name constraints-v) "scale") :options options-v :on-change on-constraint-v-select-changed}]] (when first-level? From c3f37fb8a363adc244f6af0497e7fc1a1155b4c1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Mar 2024 13:08:55 +0100 Subject: [PATCH 11/19] :recycle: Refactor import dialog on dashboard --- .../src/app/main/ui/dashboard/import.cljs | 546 ++++++++++-------- .../src/app/main/ui/dashboard/import.scss | 1 + 2 files changed, 310 insertions(+), 237 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 2d218e06c..2d079a778 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -38,17 +38,17 @@ [project-id on-finish-import] (mf/use-fn (mf/deps project-id on-finish-import) - (fn [files] - (when files - (let [files (->> files - (mapv - (fn [file] - {:name (.-name file) - :uri (wapi/create-uri file)})))] + (fn [entries] + (let [entries (->> entries + (mapv (fn [file] + {:name (.-name file) + :uri (wapi/create-uri file)})) + (not-empty))] + (when entries (st/emit! (modal/show {:type :import :project-id project-id - :files files + :entries entries :on-finish-import on-finish-import}))))))) (mf/defc import-form @@ -56,7 +56,6 @@ ::mf/props :obj} [{:keys [project-id on-finish-import]} external-ref] - (let [on-file-selected (use-import-file project-id on-finish-import)] [:form.import-file {:aria-hidden "true"} [:& file-uploader {:accept ".penpot,.zip" @@ -64,72 +63,70 @@ :ref external-ref :on-selected on-file-selected}]])) -(defn update-file - [files file-id new-name] - (mapv - (fn [file] - (let [new-name (str/trim new-name)] - (cond-> file - (and (= (:file-id file) file-id) - (not= "" new-name)) - (assoc :name new-name)))) - files)) +(defn- update-entry-name + [entries file-id new-name] + (mapv (fn [entry] + (let [new-name (str/trim new-name)] + (cond-> entry + (and (= (:file-id entry) file-id) + (not= "" new-name)) + (assoc :name new-name)))) + entries)) -(defn remove-file - [files file-id] - (mapv - (fn [file] - (cond-> file - (= (:file-id file) file-id) - (assoc :deleted? true))) - files)) +(defn- remove-entry + [entries file-id] + (mapv (fn [entry] + (cond-> entry + (= (:file-id entry) file-id) + (assoc :deleted true))) + entries)) -(defn set-analyze-error - [files uri error] - (->> files - (mapv (fn [file] - (cond-> file - (= uri (:uri file)) +(defn- update-with-analyze-error + [entries uri error] + (->> entries + (mapv (fn [entry] + (cond-> entry + (= uri (:uri entry)) (-> (assoc :status :analyze-error) (assoc :error error))))))) -(defn set-analyze-result - [files uri type data] - (let [existing-files? (into #{} (->> files (map :file-id) (filter some?))) - replace-file - (fn [file] - (if (and (= uri (:uri file)) - (= (:status file) :analyzing)) - (->> (:files data) - (remove (comp existing-files? first)) - (mapv (fn [[file-id file-data]] - (-> file-data - (assoc :file-id file-id - :status :ready - :uri uri - :type type))))) - [file]))] - (into [] (mapcat replace-file) files))) +(defn- update-with-analyze-result + [entries uri type result] + (let [existing-entries? (into #{} (keep :file-id) entries) + replace-entry + (fn [entry] + (if (and (= uri (:uri entry)) + (= (:status entry) :analyzing)) + (->> (:files result) + (remove (comp existing-entries? first)) + (map (fn [[file-id file-data]] + (-> file-data + (assoc :file-id file-id) + (assoc :status :ready) + (assoc :uri uri) + (assoc :type type))))) + [entry]))] + (into [] (mapcat replace-entry) entries))) -(defn mark-files-importing - [files] - (->> files +(defn- mark-entries-importing + [entries] + (->> entries (filter #(= :ready (:status %))) (mapv #(assoc % :status :importing)))) -(defn update-status - [files file-id status progress errors] - (->> files - (mapv (fn [file] - (cond-> file - (and (= file-id (:file-id file)) (not= status :import-progress)) - (assoc :status status) +(defn- update-entry-status + [entries file-id status progress errors] + (mapv (fn [entry] + (cond-> entry + (and (= file-id (:file-id entry)) (not= status :import-progress)) + (assoc :status status) - (and (= file-id (:file-id file)) (= status :import-progress)) - (assoc :progress progress) + (and (= file-id (:file-id entry)) (= status :import-progress)) + (assoc :progress progress) - (= file-id (:file-id file)) - (assoc :errors errors)))))) + (= file-id (:file-id entry)) + (assoc :errors errors))) + entries)) (defn- parse-progress-message [message] @@ -157,53 +154,116 @@ (str message))) +(defn- has-status-importing? + [item] + (= (:status item) :importing)) + +(defn- has-status-analyzing? + [item] + (= (:status item) :analyzing)) + +(defn- has-status-analyze-error? + [item] + (= (:status item) :analyzing)) + +(defn- has-status-success? + [item] + (and (= (:status item) :import-finish) + (empty? (:errors item)))) + +(defn- has-status-error? + [item] + (and (= (:status item) :import-finish) + (d/not-empty? (:errors item)))) + +(defn- has-status-ready? + [item] + (and (= :ready (:status item)) + (not (:deleted item)))) + +(defn- analyze-entries + [state entries] + (->> (uw/ask-many! + {:cmd :analyze-import + :files entries + :features @features/features-ref}) + (rx/mapcat #(rx/delay emit-delay (rx/of %))) + (rx/filter some?) + (rx/subs! + (fn [{:keys [uri data error type] :as msg}] + (if (some? error) + (swap! state update-with-analyze-error uri error) + (swap! state update-with-analyze-result uri type data)))))) + +(defn- import-files! + [state project-id entries] + (st/emit! (ptk/data-event ::ev/event {::ev/name "import-files" + :num-files (count entries)})) + (->> (uw/ask-many! + {:cmd :import-files + :project-id project-id + :files entries + :features @features/features-ref}) + (rx/subs! + (fn [{:keys [file-id status message errors] :as msg}] + (swap! state update-entry-status file-id status message errors))))) + (mf/defc import-entry - {::mf/props :obj} - [{:keys [state file editing? can-be-deleted]}] - (let [loading? (or (= :analyzing (:status file)) - (= :importing (:status file))) - analyze-error? (= :analyze-error (:status file)) - import-finish? (= :import-finish (:status file)) - import-error? (= :import-error (:status file)) - import-warn? (d/not-empty? (:errors file)) - ready? (= :ready (:status file)) - is-shared? (:shared file) - progress (:progress file) + {::mf/props :obj + ::mf/memo true + ::mf/private true} + [{:keys [entries entry edition can-be-deleted on-edit on-change on-delete]}] + (let [status (:status entry) + loading? (or (= :analyzing status) + (= :importing status)) + analyze-error? (= :analyze-error status) + import-finish? (= :import-finish status) + import-error? (= :import-error status) + import-warn? (d/not-empty? (:errors entry)) + ready? (= :ready status) + is-shared? (:shared entry) + progress (:progress entry) - handle-edit-key-press + file-id (:file-id entry) + editing? (and (some? file-id) (= edition file-id)) + + on-edit-key-press (mf/use-fn - (fn [e] - (when (or (kbd/enter? e) (kbd/esc? e)) - (dom/prevent-default e) - (dom/stop-propagation e) - (dom/blur! (dom/get-target e))))) + (fn [event] + (when (or (kbd/enter? event) + (kbd/esc? event)) + (dom/prevent-default event) + (dom/stop-propagation event) + (dom/blur! (dom/get-target event))))) - handle-edit-blur + on-edit-blur (mf/use-fn - (mf/deps file) - (fn [e] - (let [value (dom/get-target-val e)] - (swap! state #(-> (assoc % :editing nil) - (update :files update-file (:file-id file) value)))))) + (mf/deps file-id on-change) + (fn [event] + (let [value (dom/get-target-val event)] + (on-change file-id value event)))) - handle-edit-entry + on-edit' (mf/use-fn - (mf/deps file) - (fn [] - (swap! state assoc :editing (:file-id file)))) + (mf/deps file-id on-change) + (fn [event] + (when (fn? on-edit) + (on-edit file-id event)))) - handle-remove-entry + on-delete' (mf/use-fn - (mf/deps file) - (fn [] - (swap! state update :files remove-file (:file-id file))))] + (mf/deps file-id on-delete) + (fn [event] + (when (fn? on-delete) + (on-delete file-id event))))] - [:div {:class (stl/css-case :file-entry true - :loading loading? - :success (and import-finish? (not import-warn?) (not import-error?)) - :warning (and import-finish? import-warn? (not import-error?)) - :error (or import-error? analyze-error?) - :editable (and ready? (not editing?)))} + [:div {:class (stl/css-case + :file-entry true + :loading loading? + :success (and import-finish? (not import-warn?) (not import-error?)) + :warning (and import-finish? import-warn? (not import-error?)) + :error (or import-error? analyze-error?) + :editable (and ready? (not editing?)))} [:div {:class (stl/css :file-name)} [:div {:class (stl/css-case :file-icon true @@ -219,26 +279,28 @@ [:div {:class (stl/css :file-name-edit)} [:input {:type "text" :auto-focus true - :default-value (:name file) - :on-key-press handle-edit-key-press - :on-blur handle-edit-blur}]] + :default-value (:name entry) + :on-key-press on-edit-key-press + :on-blur on-edit-blur}]] [:div {:class (stl/css :file-name-label)} - (:name file) - (when is-shared? + (:name entry) + (when ^boolean is-shared? [:span {:class (stl/css :icon)} i/library-refactor])]) [:div {:class (stl/css :edit-entry-buttons)} - (when (= "application/zip" (:type file)) - [:button {:on-click handle-edit-entry} i/curve-refactor]) + (when (and (= "application/zip" (:type entry)) + (= status :ready)) + [:button {:on-click on-edit'} i/curve-refactor]) (when can-be-deleted - [:button {:on-click handle-remove-entry} i/delete-refactor])]] + [:button {:on-click on-delete'} i/delete-refactor])]] + (cond analyze-error? [:div {:class (stl/css :error-message)} - (if (some? (:error file)) - (tr (:error file)) + (if (some? (:error entry)) + (tr (:error entry)) (tr "dashboard.import.analyze-error"))] import-error? @@ -249,139 +311,143 @@ [:div {:class (stl/css :progress-message)} (parse-progress-message progress)]) [:div {:class (stl/css :linked-libraries)} - (for [library-id (:libraries file)] - (let [library-data (->> @state :files (d/seek #(= library-id (:file-id %)))) - error? (or (:deleted? library-data) (:import-error library-data))] + (for [library-id (:libraries entry)] + (let [library-data (d/seek #(= library-id (:file-id %)) entries) + error? (or (:deleted library-data) + (:import-error library-data))] (when (some? library-data) - [:div {:class (stl/css :linked-library)} + [:div {:class (stl/css :linked-library) + :key (dm/str library-id)} (:name library-data) - [:span {:class (stl/css-case :linked-library-tag true - :error error?)} i/detach-refactor]])))]])) + [:span {:class (stl/css-case + :linked-library-tag true + :error error?)} + i/detach-refactor]])))]])) (mf/defc import-dialog {::mf/register modal/components ::mf/register-as :import ::mf/props :obj} - [{:keys [project-id files template on-finish-import]}] - (let [state (mf/use-state - {:status :analyzing - :editing nil - :importing-templates 0 - :files (->> files - (mapv #(assoc % :status :analyzing)))}) - analyze-import - (mf/use-fn - (fn [files] - (->> (uw/ask-many! - {:cmd :analyze-import - :files files - :features @features/features-ref}) - (rx/mapcat #(rx/delay emit-delay (rx/of %))) - (rx/filter some?) - (rx/subs! - (fn [{:keys [uri data error type] :as msg}] - (if (some? error) - (swap! state update :files set-analyze-error uri error) - (swap! state update :files set-analyze-result uri type data))))))) + [{:keys [project-id entries template on-finish-import]}] - import-files - (mf/use-fn - (fn [project-id files] - (st/emit! (ptk/event ::ev/event {::ev/name "import-files" - :num-files (count files)})) - (->> (uw/ask-many! - {:cmd :import-files - :project-id project-id - :files files - :features @features/features-ref}) - (rx/subs! - (fn [{:keys [file-id status message errors] :as msg}] - (swap! state update :files update-status file-id status message errors)))))) + (mf/with-effect [] + ;; dispose uris when the component is umount + (fn [] (run! wapi/revoke-uri (map :uri entries)))) - handle-cancel + (let [entries* (mf/use-state + (fn [] (mapv #(assoc % :status :analyzing) entries))) + entries (deref entries*) + + status* (mf/use-state :analyzing) + status (deref status*) + + edition* (mf/use-state nil) + edition (deref edition*) + + on-template-cloned-success (mf/use-fn - (mf/deps (:editing @state)) + (fn [] + (swap! status* (constantly :importing)) + ;; (swap! state assoc :status :importing :importing-templates 0) + (st/emit! (dd/fetch-recent-files)))) + + on-template-cloned-error + (mf/use-fn + (fn [cause] + (swap! status* (constantly :error)) + ;; (swap! state assoc :status :error :importing-templates 0) + (errors/print-error! cause) + (rx/of (modal/hide) + (msg/error (tr "dashboard.libraries-and-templates.import-error"))))) + + continue-entries + (mf/use-fn + (mf/deps entries) + (fn [] + (let [entries (filterv has-status-ready? entries)] + (swap! status* (constantly :importing)) + (swap! entries* mark-entries-importing) + (import-files! entries* project-id entries)))) + + continue-template + (mf/use-fn + (mf/deps on-template-cloned-success + on-template-cloned-error + template) + (fn [] + (let [mdata {:on-success on-template-cloned-success + :on-error on-template-cloned-error} + params {:project-id project-id :template-id (:id template)}] + (swap! status* (constantly :importing)) + (st/emit! (dd/clone-template (with-meta params mdata)))))) + + on-edit + (mf/use-fn + (fn [file-id _event] + (swap! edition* (constantly file-id)))) + + on-entry-change + (mf/use-fn + (fn [file-id value] + (swap! edition* (constantly nil)) + (swap! entries* update-entry-name file-id value))) + + on-entry-delete + (mf/use-fn + (fn [file-id] + (swap! entries* remove-entry file-id))) + + on-cancel + (mf/use-fn + (mf/deps edition) (fn [event] - (when (nil? (:editing @state)) + (when (nil? edition) (dom/prevent-default event) (st/emit! (modal/hide))))) - on-template-cloned-success - (fn [] - (swap! state assoc :status :importing :importing-templates 0) - (st/emit! (dd/fetch-recent-files))) - - on-template-cloned-error - (fn [cause] - (swap! state assoc :status :error :importing-templates 0) - (errors/print-error! cause) - (rx/of (modal/hide) - (msg/error (tr "dashboard.libraries-and-templates.import-error")))) - - continue-files - (fn [] - (let [files (->> @state :files (filterv #(and (= :ready (:status %)) (not (:deleted? %)))))] - (import-files project-id files)) - - (swap! state - (fn [state] - (-> state - (assoc :status :importing) - (update :files mark-files-importing))))) - - continue-template - (fn [] - (let [mdata {:on-success on-template-cloned-success - :on-error on-template-cloned-error} - params {:project-id project-id :template-id (:id template)}] - (swap! state - (fn [state] - (-> state - (assoc :status :importing :importing-templates 1)))) - (st/emit! (dd/clone-template (with-meta params mdata))))) - - - handle-continue + on-continue (mf/use-fn - (mf/deps project-id (:files @state)) + (mf/deps template + continue-template + continue-entries) (fn [event] (dom/prevent-default event) (if (some? template) (continue-template) - (continue-files)))) + (continue-entries)))) - handle-accept + on-accept (mf/use-fn + (mf/deps on-finish-import) (fn [event] (dom/prevent-default event) (st/emit! (modal/hide)) - (when on-finish-import (on-finish-import)))) + (when (fn? on-finish-import) + (on-finish-import)))) - files (->> (:files @state) (filterv (comp not :deleted?))) + entries (filterv (comp not :deleted) entries) + num-importing (+ (count (filterv has-status-importing? entries)) + (if (some? template) 1 0)) - num-importing (+ - (->> files (filter #(= (:status %) :importing)) count) - (:importing-templates @state)) + success-num (if (some? template) + 1 + (count (filterv has-status-success? entries))) - warning-files (->> files (filter #(and (= (:status %) :import-finish) (d/not-empty? (:errors %)))) count) - success-files (->> files (filter #(and (= (:status %) :import-finish) (empty? (:errors %)))) count) - pending-analysis? (> (->> files (filter #(= (:status %) :analyzing)) count) 0) - pending-import? (> num-importing 0) + errors? (or (some has-status-error? entries) + (zero? (count entries))) - valid-files? (or (some? template) - (> (+ (->> files (filterv (fn [x] (not= (:status x) :analyze-error))) count)) 0))] - (mf/use-effect - (fn [] - (let [sub (analyze-import files)] - #(rx/dispose! sub)))) + pending-analysis? (some has-status-analyzing? entries) + pending-import? (pos? num-importing) + valid-all-entries? (or (some? template) + (not (some has-status-analyze-error? entries)))] - (mf/use-effect - (fn [] - ;; dispose uris when the component is umount - #(doseq [file files] - (wapi/revoke-uri (:uri file))))) + + ;; Run analyze operation on component mount + (mf/with-effect [] + (let [sub (analyze-entries entries* entries)] + (partial rx/dispose! sub))) [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-container)} @@ -389,52 +455,58 @@ [:h2 {:class (stl/css :modal-title)} (tr "dashboard.import")] [:button {:class (stl/css :modal-close-btn) - :on-click handle-cancel} i/close-refactor]] + :on-click on-cancel} i/close-refactor]] [:div {:class (stl/css :modal-content)} + (when (and (= :analyzing status) errors?) + [:& context-notification + {:type :warning + :content (tr "dashboard.import.import-warning")}]) - (when (and (= :importing (:status @state)) (not pending-import?)) - (if (> warning-files 0) + (when (and (= :importing status) (not ^boolean pending-import?)) + (cond + errors? [:& context-notification {:type :warning - :content (tr "dashboard.import.import-warning" warning-files success-files)}] + :content (tr "dashboard.import.import-warning")}] + + :else [:& context-notification {:type :success - :content (tr "dashboard.import.import-message" (i18n/c (if (some? template) 1 success-files)))}])) + :content (tr "dashboard.import.import-message" (i18n/c success-num))}])) - (for [file files] - (let [editing? (and (some? (:file-id file)) - (= (:file-id file) (:editing @state)))] - [:& import-entry {:state state - :key (dm/str (:uri file)) - :file file - :editing? editing? - :can-be-deleted (> (count files) 1)}])) + (for [entry entries] + [:& import-entry {:edition edition + :key (dm/str (:uri entry)) + :entry entry + :entries entries + :on-edit on-edit + :on-change on-entry-change + :on-delete on-entry-delete + :can-be-deleted (> (count entries) 1)}]) (when (some? template) - [:& import-entry {:state state - :file (assoc template :status (if (= 1 (:importing-templates @state)) :importing :ready)) - :editing? false + [:& import-entry {:entry (assoc template :status :ready) :can-be-deleted false}])] [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} - (when (= :analyzing (:status @state)) + (when (= :analyzing status) [:input {:class (stl/css :cancel-button) :type "button" :value (tr "labels.cancel") - :on-click handle-cancel}]) + :on-click on-cancel}]) - (when (= :analyzing (:status @state)) + (when (and (= :analyzing status) (not errors?)) [:input {:class (stl/css :accept-btn) :type "button" :value (tr "labels.continue") - :disabled (or pending-analysis? (not valid-files?)) - :on-click handle-continue}]) + :disabled (or pending-analysis? (not valid-all-entries?)) + :on-click on-continue}]) - (when (= :importing (:status @state)) + (when (and (= :importing status) (not errors?)) [:input {:class (stl/css :accept-btn) :type "button" :value (tr "labels.accept") - :disabled (or pending-import? (not valid-files?)) - :on-click handle-accept}])]]]])) + :disabled (or pending-import? (not valid-all-entries?)) + :on-click on-accept}])]]]])) diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index 0fc0c620f..a89dda6da 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -33,6 +33,7 @@ grid-template-columns: 1fr; gap: $s-16; margin-bottom: $s-24; + min-height: 40px; } .action-buttons { From 07b8a2a6e6c9e414e8923094297089937d33a747 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Mar 2024 19:06:29 +0100 Subject: [PATCH 12/19] :sparkles: Restrict http methods on RPC handlers --- backend/src/app/http/errors.clj | 8 ++++++-- backend/src/app/rpc.clj | 36 ++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 580cd6703..18350d21d 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -60,8 +60,12 @@ (defmethod handle-error :restriction [err _ _] - {::rres/status 400 - ::rres/body (ex-data err)}) + (let [{:keys [code] :as data} (ex-data err)] + (if (= code :method-not-allowed) + {::rres/status 405 + ::rres/body data} + {::rres/status 400 + ::rres/body data}))) (defmethod handle-error :rate-limit [err _ _] diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 2f999a08e..8ae7a38d6 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -31,6 +31,7 @@ [app.util.services :as sv] [app.util.time :as dt] [clojure.spec.alpha :as s] + [cuerdas.core :as str] [integrant.core :as ig] [promesa.core :as p] [ring.request :as rreq] @@ -71,24 +72,31 @@ (defn- rpc-handler "Ring handler that dispatches cmd requests and convert between internal async flow into ring async flow." - [methods {:keys [params path-params] :as request}] - (let [type (keyword (:type path-params)) - etag (rreq/get-header request "if-none-match") - profile-id (or (::session/profile-id request) - (::actoken/profile-id request)) + [methods {:keys [params path-params method] :as request}] + (let [handler-name (:type path-params) + etag (rreq/get-header request "if-none-match") + profile-id (or (::session/profile-id request) + (::actoken/profile-id request)) - data (-> params - (assoc ::request-at (dt/now)) - (assoc ::session/id (::session/id request)) - (assoc ::cond/key etag) - (cond-> (uuid? profile-id) - (assoc ::profile-id profile-id))) + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::session/id (::session/id request)) + (assoc ::cond/key etag) + (cond-> (uuid? profile-id) + (assoc ::profile-id profile-id))) - data (vary-meta data assoc ::http/request request) - method (get methods type default-handler)] + data (vary-meta data assoc ::http/request request) + handler-fn (get methods (keyword handler-name) default-handler)] + + (when (and (or (= method :get) + (= method :head)) + (not (str/starts-with? handler-name "get-"))) + (ex/raise :type :restriction + :code :method-not-allowed + :hint "method not allowed for this request")) (binding [cond/*enabled* true] - (let [response (method data)] + (let [response (handler-fn data)] (handle-response request response))))) (defn- wrap-metrics From 1bc4001e70c9bc4ea72019bd261de17e48f1dc14 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Mar 2024 19:44:09 +0100 Subject: [PATCH 13/19] :sparkles: Add the ability to set :string for cookie same-site By configuration. The default is :lax (unchanged) --- backend/src/app/http/session.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index 696cc6a3a..7ff6dfa01 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -248,6 +248,7 @@ renewal (dt/plus created-at default-renewal-max-age) expires (dt/plus created-at max-age) secure? (contains? cf/flags :secure-session-cookies) + strict? (contains? cf/flags :strict-session-cookies) cors? (contains? cf/flags :cors) name (cf/get :auth-token-cookie-name default-auth-token-cookie-name) comment (str "Renewal at: " (dt/format-instant renewal :rfc1123)) @@ -256,7 +257,7 @@ :expires expires :value token :comment comment - :same-site (if cors? :none :lax) + :same-site (if cors? :none (if strict? :strict :lax)) :secure secure?}] (update response :cookies assoc name cookie))) From 8cb550120ac878048880a584c24018e841ec6cfc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Mar 2024 09:16:45 +0100 Subject: [PATCH 14/19] :bug: Fix error handling on recovery request page --- .../app/main/ui/auth/recovery_request.cljs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index e90eec477..d1d72ed2d 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -49,19 +49,20 @@ on-error (mf/use-callback - (fn [data {:keys [code] :as error}] + (fn [data cause] (reset! submitted false) - (case code - :profile-not-verified - (rx/of (msg/error (tr "auth.notifications.profile-not-verified"))) + (let [code (-> cause ex-data :code)] + (case code + :profile-not-verified + (rx/of (msg/error (tr "auth.notifications.profile-not-verified"))) - :profile-is-muted - (rx/of (msg/error (tr "errors.profile-is-muted"))) + :profile-is-muted + (rx/of (msg/error (tr "errors.profile-is-muted"))) - :email-has-permanent-bounces - (rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data)))) + :email-has-permanent-bounces + (rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data)))) - (rx/throw error)))) + (rx/throw cause))))) on-submit (mf/use-callback From 5b722a86086c8374d7390c596c44852aec8b38b2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Mar 2024 09:17:04 +0100 Subject: [PATCH 15/19] :bug: Fix error handling on register page --- frontend/src/app/main/ui/auth/register.cljs | 34 ++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index cda4f878d..0c1a0e70c 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -58,33 +58,33 @@ :opt-un [::invitation-token])) (defn- handle-prepare-register-error - [form {:keys [type code] :as cause}] - (condp = [type code] - [:restriction :registration-disabled] - (st/emit! (msg/error (tr "errors.registration-disabled"))) + [form cause] + (let [{:keys [type code]} (ex-data cause)] + (condp = [type code] + [:restriction :registration-disabled] + (st/emit! (msg/error (tr "errors.registration-disabled"))) - [:restriction :profile-blocked] - (st/emit! (msg/error (tr "errors.profile-blocked"))) + [:restriction :profile-blocked] + (st/emit! (msg/error (tr "errors.profile-blocked"))) - [:validation :email-has-permanent-bounces] - (let [email (get @form [:data :email])] - (st/emit! (msg/error (tr "errors.email-has-permanent-bounces" email)))) + [:validation :email-has-permanent-bounces] + (let [email (get @form [:data :email])] + (st/emit! (msg/error (tr "errors.email-has-permanent-bounces" email)))) - [:validation :email-already-exists] - (swap! form assoc-in [:errors :email] - {:message "errors.email-already-exists"}) + [:validation :email-already-exists] + (swap! form assoc-in [:errors :email] + {:message "errors.email-already-exists"}) - [:validation :email-as-password] - (swap! form assoc-in [:errors :password] - {:message "errors.email-as-password"}) + [:validation :email-as-password] + (swap! form assoc-in [:errors :password] + {:message "errors.email-as-password"}) - (st/emit! (msg/error (tr "errors.generic"))))) + (st/emit! (msg/error (tr "errors.generic")))))) (defn- handle-prepare-register-success [params] (st/emit! (rt/nav :auth-register-validate {} params))) - (mf/defc register-form [{:keys [params on-success-callback] :as props}] (let [initial (mf/use-memo (mf/deps params) (constantly params)) From 88f49cfbc932272c5bf7e9cd789a5d1ba1f462d3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Mar 2024 09:17:39 +0100 Subject: [PATCH 16/19] :bug: Fix email field intrusive autocomplete on firefox Firefox has a strange behavior because it ignores the autocomplete attribute and just does not allow submit a form when an email type field has invalid email (valid but surrounded with whitespace). This fix is a workaround, setting up the input field as simple text instead of semantic type 'email'. --- frontend/src/app/main/ui/auth/register.cljs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 0c1a0e70c..6789b99dc 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -100,7 +100,7 @@ (on-success-callback p))) on-submit - (mf/use-callback + (mf/use-fn (fn [form _event] (reset! submitted? true) (let [cdata (:clean-data @form)] @@ -114,7 +114,7 @@ [:& fm/form {:on-submit on-submit :form form} [:div {:class (stl/css :fields-row)} - [:& fm/input {:type "email" + [:& fm/input {:type "text" :name :email :label (tr "auth.email") :data-test "email-input" @@ -225,7 +225,7 @@ (on-success-callback (:email p)))) on-submit - (mf/use-callback + (mf/use-fn (fn [form _event] (reset! submitted? true) (let [params (:clean-data @form)] From 7eecd50c50d12ce5b37ecd6e5f178e052af3eea8 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Mar 2024 09:24:37 +0100 Subject: [PATCH 17/19] :books: Add http methods documentation to the API doc page --- backend/resources/app/templates/api-doc.tmpl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/resources/app/templates/api-doc.tmpl b/backend/resources/app/templates/api-doc.tmpl index aa0b2a7d9..8c67fdeee 100644 --- a/backend/resources/app/templates/api-doc.tmpl +++ b/backend/resources/app/templates/api-doc.tmpl @@ -37,6 +37,13 @@

GENERAL NOTES

+

HTTP Transport & Methods

+

The HTTP is the transport method for accesing this API; all + functions can be called using POST HTTP method; the functions + that starts with get- in the name, can use GET HTTP + method which in many cases benefits from the HTTP cache.

+ +

Authentication

The penpot backend right now offers two way for authenticate the request: cookies (the same mechanism that we use ourselves on accessing the API from the From 131fc95ab0d2b0025eae92d16abd011cf81e805a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Mar 2024 10:01:57 +0100 Subject: [PATCH 18/19] :bug: Fix release notes not showing on release build --- frontend/src/app/main/ui/releases.cljs | 2 +- frontend/src/app/main/ui/releases/v2_0.cljs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs index 562c1eab2..50f87648f 100644 --- a/frontend/src/app/main/ui/releases.cljs +++ b/frontend/src/app/main/ui/releases.cljs @@ -91,4 +91,4 @@ (defmethod rc/render-release-notes "0.0" [params] - (rc/render-release-notes (assoc params :version "2.0"))) + (rc/render-release-notes (assoc params :version "1.21"))) diff --git a/frontend/src/app/main/ui/releases/v2_0.cljs b/frontend/src/app/main/ui/releases/v2_0.cljs index 67ae75e17..53ce83ee1 100644 --- a/frontend/src/app/main/ui/releases/v2_0.cljs +++ b/frontend/src/app/main/ui/releases/v2_0.cljs @@ -11,6 +11,10 @@ [app.main.ui.releases.common :as c] [rumext.v2 :as mf])) +(defmethod c/render-release-notes "1.21" + [data] + (c/render-release-notes (assoc data :verstion "2.0"))) + ;; TODO: Review all copies and alt text (defmethod c/render-release-notes "2.0" [{:keys [slide klass next finish navigate version]}] From 1134f16ffac65bb00b9d7ccaf845c9410867d433 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Mar 2024 12:46:30 +0100 Subject: [PATCH 19/19] :lipstick: Add cosmetic refactor to dashboard fonts react components --- frontend/src/app/main/ui/dashboard/fonts.cljs | 352 +++++++++++------- 1 file changed, 208 insertions(+), 144 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index bdd8e5ede..a444236ce 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -25,108 +25,143 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) -(defn- use-set-page-title +(defn- use-page-title [team section] - (mf/use-effect - (mf/deps team) - (fn [] - (when team - (let [tname (if (:is-default team) - (tr "dashboard.your-penpot") - (:name team))] - (case section - :fonts (dom/set-html-title (tr "title.dashboard.fonts" tname)) - :providers (dom/set-html-title (tr "title.dashboard.font-providers" tname)))))))) + (mf/with-effect [team] + (when team + (let [tname (if (:is-default team) + (tr "dashboard.your-penpot") + (:name team))] + (case section + :fonts (dom/set-html-title (tr "title.dashboard.fonts" tname)) + :providers (dom/set-html-title (tr "title.dashboard.font-providers" tname))))))) + +(defn- bad-font-family-tmp? + [font] + (and (contains? font :font-family-tmp) + (str/blank? (:font-family-tmp font)))) (mf/defc header - {::mf/wrap [mf/memo]} - [{:keys [section team] :as props}] - (use-set-page-title team section) + {::mf/props :obj + ::mf/memo true + ::mf/private true} + [{:keys [section team]}] + (use-page-title team section) [:header {:class (stl/css :dashboard-header)} [:div#dashboard-fonts-title {:class (stl/css :dashboard-title)} [:h1 (tr "labels.fonts")]]]) (mf/defc font-variant-display-name + {::mf/props :obj + ::mf/private true} [{:keys [variant]}] [:* [:span (cm/font-weight->name (:font-weight variant))] (when (not= "normal" (:font-style variant)) [:span " " (str/capital (:font-style variant))])]) -(mf/defc fonts-upload +(mf/defc uploaded-fonts + {::mf/props :obj + ::mf/private true} [{:keys [team installed-fonts] :as props}] - (let [fonts* (mf/use-state {}) - fonts (deref fonts*) - input-ref (mf/use-ref) - uploading (mf/use-state #{}) + (let [fonts* (mf/use-state {}) + fonts (deref fonts*) + font-vals (mf/with-memo [fonts] + (->> fonts + (into [] (map val)) + (not-empty))) - bad-font-family-tmp? - (mf/use-fn - (fn [font] - (and (contains? font :font-family-tmp) - (str/blank? (:font-family-tmp font))))) + team-id (:id team) - disable-upload-all? (some bad-font-family-tmp? (vals fonts)) + input-ref (mf/use-ref) - handle-click + uploading* (mf/use-state #{}) + uploading (deref uploading*) + + disable-upload-all? + (some bad-font-family-tmp? fonts) + + problematic-fonts? + (some :height-warning? (vals fonts)) + + on-click (mf/use-fn #(dom/click (mf/ref-val input-ref))) - handle-selected + on-selected (mf/use-fn - (mf/deps team installed-fonts) + (mf/deps team-id installed-fonts) (fn [blobs] - (->> (df/process-upload blobs (:id team)) + (->> (df/process-upload blobs team-id) (rx/subs! (fn [result] (swap! fonts* df/merge-and-group-fonts installed-fonts result)) (fn [error] (js/console.error "error" error)))))) - on-upload + on-upload* (mf/use-fn - (mf/deps team) - (fn [item] - (swap! uploading conj (:id item)) + (fn [{:keys [id] :as item}] + (swap! uploading* conj id) (->> (rp/cmd! :create-font-variant item) (rx/delay-at-least 2000) (rx/subs! (fn [font] - (swap! fonts* dissoc (:id item)) - (swap! uploading disj (:id item)) + (swap! fonts* dissoc id) + (swap! uploading* disj id) (st/emit! (df/add-font font))) (fn [error] (js/console.log "error" error)))))) - on-upload-all - (fn [items] - (run! on-upload items)) + on-upload + (mf/use-fn + (mf/deps fonts on-upload*) + (fn [event] + (let [id (-> (dom/get-current-target event) + (dom/get-data "id") + (parse-uuid)) + item (get fonts id)] + (on-upload* item)))) on-blur-name - (fn [id event] - (let [name (dom/get-target-val event)] - (when-not (str/blank? name) - (swap! fonts* df/rename-and-regroup id name installed-fonts)))) + (mf/use-fn + (mf/deps installed-fonts) + (fn [event] + (let [target (dom/get-current-target event) + id (-> target + (dom/get-data "id") + (parse-uuid)) + name (dom/get-value target)] + (when-not (str/blank? name) + (swap! fonts* df/rename-and-regroup id name installed-fonts))))) on-change-name - (fn [id event] - (let [name (dom/get-target-val event)] - (swap! fonts* update-in [id] #(assoc % :font-family-tmp name)))) + (mf/use-fn + (fn [event] + (let [target (dom/get-current-target event) + id (-> target + (dom/get-data "id") + (parse-uuid)) + name (dom/get-value target)] + (swap! fonts* update id assoc :font-family-tmp name)))) on-delete (mf/use-fn (mf/deps team) - (fn [{:keys [id] :as item}] - (swap! fonts* dissoc id))) + (fn [event] + (let [id (-> (dom/get-current-target event) + (dom/get-data "id") + (parse-uuid))] + (swap! fonts* dissoc id)))) - on-dismiss-all - (fn [items] - (run! on-delete items)) + on-upload-all + (mf/use-fn + (mf/deps font-vals) + (fn [_] + (run! on-upload* font-vals))) - problematic-fonts? (some :height-warning? (vals fonts)) - - handle-upload-all - (mf/use-fn (mf/deps fonts) #(on-upload-all (vals fonts))) - - handle-dismiss-all - (mf/use-fn (mf/deps fonts) #(on-dismiss-all (vals fonts)))] + on-dismis-all + (mf/use-fn + (mf/deps fonts) + (fn [_] + (run! on-delete (vals fonts))))] [:div {:class (stl/css :dashboard-fonts-upload)} [:div {:class (stl/css :dashboard-fonts-hero)} @@ -135,14 +170,14 @@ [:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}] [:button {:class (stl/css :btn-primary) - :on-click handle-click + :on-click on-click :tab-index "0"} [:span (tr "labels.add-custom-font")] [:& file-uploader {:input-id "font-upload" :accept cm/str-font-types :multi true :ref input-ref - :on-selected handle-selected}]] + :on-selected on-selected}]] [:& context-notification {:content (tr "dashboard.fonts.hero-text2") :type :default @@ -154,31 +189,32 @@ :is-html true}])]] [:* - (when (some? (vals fonts)) + (when (seq fonts) [:div {:class (stl/css :font-item :table-row)} - [:span (tr "dashboard.fonts.fonts-added" (i18n/c (count (vals fonts))))] + [:span (tr "dashboard.fonts.fonts-added" (i18n/c (count fonts)))] [:div {:class (stl/css :table-field :options)} - [:button {:class (stl/css-case :btn-primary true - :disabled disable-upload-all?) - :on-click handle-upload-all + [:button {:class (stl/css-case + :btn-primary true + :disabled disable-upload-all?) + :on-click on-upload-all :data-test "upload-all" :disabled disable-upload-all?} [:span (tr "dashboard.fonts.upload-all")]] [:button {:class (stl/css :btn-secondary) - :on-click handle-dismiss-all + :on-click on-dismis-all :data-test "dismiss-all"} [:span (tr "dashboard.fonts.dismiss-all")]]]]) - (for [item (sort-by :font-family (vals fonts))] - (let [uploading? (contains? @uploading (:id item)) - disable-upload? (or uploading? - (bad-font-family-tmp? item))] + (for [{:keys [id] :as item} (sort-by :font-family font-vals)] + (let [uploading? (contains? uploading id) + disable-upload? (or uploading? (bad-font-family-tmp? item))] [:div {:class (stl/css :font-item :table-row) - :key (:id item)} + :key (dm/str id)} [:div {:class (stl/css :table-field :family)} [:input {:type "text" - :on-blur #(on-blur-name (:id item) %) - :on-change #(on-change-name (:id item) %) + :data-id (dm/str id) + :on-blur on-blur-name + :on-change on-change-name :default-value (:font-family item)}]] [:div {:class (stl/css :table-field :variants)} [:span {:class (stl/css :label)} @@ -190,115 +226,151 @@ [:div {:class (stl/css :table-field :options)} (when (:height-warning? item) - [:span {:class (stl/css :icon :failure)} i/msg-neutral-refactor]) + [:span {:class (stl/css :icon :failure)} + i/msg-neutral-refactor]) - [:button {:on-click #(on-upload item) - :class (stl/css-case :btn-primary true - :upload-button true - :disabled disable-upload?) + [:button {:on-click on-upload + :data-id (dm/str id) + :class (stl/css-case + :btn-primary true + :upload-button true + :disabled disable-upload?) :disabled disable-upload?} - (if uploading? + (if ^boolean uploading? (tr "labels.uploading") (tr "labels.upload"))] [:span {:class (stl/css :icon :close) - :on-click #(on-delete item)} i/close-refactor]]]))]])) + :data-id (dm/str id) + :on-click on-delete} + i/close-refactor]]]))]])) + +(mf/defc installed-font-context-menu + {::mf/props :obj + ::mf/private true} + [{:keys [is-open on-close on-edit on-delete]}] + (let [options (mf/with-memo [on-edit on-delete] + [{:option-name (tr "labels.edit") + :id "font-edit" + :option-handler on-edit} + {:option-name (tr "labels.delete") + :id "font-delete" + :option-handler on-delete}])] + [:& context-menu-a11y + {:on-close on-close + :show is-open + :fixed? false + :min-width? true + :top -15 + :left -115 + :options options + :workspace? false}])) (mf/defc installed-font - [{:keys [font-id variants] :as props}] + {::mf/props :obj + ::mf/private true + ::mf/memo true} + [{:keys [font-id variants]}] (let [font (first variants) - variants (sort-by (fn [item] - [(:font-weight item) - (if (= "normal" (:font-style item)) 1 2)]) - variants) + menu-open* (mf/use-state false) + menu-open? (deref menu-open*) + edition* (mf/use-state false) + edition? (deref edition*) - open-menu? (mf/use-state false) - edit? (mf/use-state false) state* (mf/use-state (:font-family font)) font-family (deref state*) + variants + (mf/with-memo [variants] + (sort-by (fn [item] + [(:font-weight item) + (if (= "normal" (:font-style item)) 1 2)]) + variants)) on-change - (mf/use-callback + (mf/use-fn (fn [event] (reset! state* (dom/get-target-val event)))) + on-edit + (mf/use-fn #(reset! edition* true)) + + on-menu-open + (mf/use-fn #(reset! menu-open* true)) + + on-menu-close + (mf/use-fn #(reset! menu-open* false)) + on-save - (mf/use-callback + (mf/use-fn (mf/deps font-family) (fn [_] + (reset! edition* false) (when-not (str/blank? font-family) - (st/emit! (df/update-font {:id font-id :name font-family}))) - (reset! edit? false))) + (st/emit! (df/update-font {:id font-id :name font-family}))))) on-key-down - (mf/use-callback + (mf/use-fn (mf/deps on-save) (fn [event] (when (kbd/enter? event) (on-save event)))) on-cancel - (mf/use-callback + (mf/use-fn (fn [_] - (reset! edit? false) + (reset! edition* false) (reset! state* (:font-family font)))) - delete-font-fn - (mf/use-callback + on-delete-font + (mf/use-fn (mf/deps font-id) (fn [] - (st/emit! (df/delete-font font-id)))) - - delete-variant-fn - (mf/use-callback - (fn [id] - (st/emit! (df/delete-font-variant id)))) - - on-delete - (mf/use-callback - (mf/deps delete-font-fn) - (fn [] - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-font.title") - :message (tr "modals.delete-font.message") - :accept-label (tr "labels.delete") - :on-accept (fn [_props] (delete-font-fn))})))) + (let [options {:type :confirm + :title (tr "modals.delete-font.title") + :message (tr "modals.delete-font.message") + :accept-label (tr "labels.delete") + :on-accept (fn [_props] + (st/emit! (df/delete-font font-id)))}] + (st/emit! (modal/show options))))) on-delete-variant - (mf/use-callback - (mf/deps delete-variant-fn) - (fn [id] - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-font-variant.title") - :message (tr "modals.delete-font-variant.message") - :accept-label (tr "labels.delete") - :on-accept (fn [_props] - (delete-variant-fn id))}))))] + (mf/use-fn + (fn [event] + (let [id (-> (dom/get-current-target event) + (dom/get-data "id") + (parse-uuid)) + options {:type :confirm + :title (tr "modals.delete-font-variant.title") + :message (tr "modals.delete-font-variant.message") + :accept-label (tr "labels.delete") + :on-accept (fn [_props] + (st/emit! (df/delete-font-variant id)))}] + (st/emit! (modal/show options)))))] [:div {:class (stl/css :font-item :table-row)} [:div {:class (stl/css :table-field :family)} - (if @edit? + (if ^boolean edition? [:input {:type "text" + :auto-focus true :default-value font-family :on-key-down on-key-down :on-change on-change}] [:span (:font-family font)])] [:div {:class (stl/css :table-field :variants)} - (for [item variants] + (for [{:keys [id] :as item} variants] [:div {:class (stl/css :variant) - :key (dm/str (:id item) "-variant")} + :key (dm/str id)} [:span {:class (stl/css :label)} [:& font-variant-display-name {:variant item}]] [:span {:class (stl/css :icon :close) - :on-click #(on-delete-variant (:id item))} + :data-id (dm/str id) + :on-click on-delete-variant} i/add-refactor]])] - (if @edit? + (if ^boolean edition? [:div {:class (stl/css :table-field :options)} [:button {:disabled (str/blank? font-family) @@ -307,27 +379,19 @@ :btn-disabled (str/blank? font-family))} (tr "labels.save")] [:button {:class (stl/css :icon :close) - :on-click on-cancel} i/close-refactor]] + :on-click on-cancel} + i/close-refactor]] [:div {:class (stl/css :table-field :options)} [:span {:class (stl/css :icon) - :on-click #(reset! open-menu? true)} + :on-click on-menu-open} i/menu-refactor] - [:& context-menu-a11y {:on-close #(reset! open-menu? false) - :show @open-menu? - :fixed? false - :min-width? true - :top -15 - :left -115 - :options [{:option-name (tr "labels.edit") - :id "font-edit" - :option-handler #(reset! edit? true)} - {:option-name (tr "labels.delete") - :id "font-delete" - :option-handler on-delete}] - :workspace? false}]])])) - + [:& installed-font-context-menu + {:on-close on-menu-close + :is-open menu-open? + :on-delete on-delete-font + :on-edit on-edit}]])])) (mf/defc installed-fonts [{:keys [fonts] :as props}] @@ -377,7 +441,7 @@ [:* [:& header {:team team :section :fonts}] [:section {:class (stl/css :dashboard-container :dashboard-fonts)} - [:& fonts-upload {:team team :installed-fonts fonts}] + [:& uploaded-fonts {:team team :installed-fonts fonts}] [:& installed-fonts {:team team :fonts fonts}]]])) (mf/defc font-providers-page