diff --git a/CHANGELOG.md b/CHANGELOG.md index f9aa5bf6..7e2aa44e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ All notable changes to this project will be documented in this file. This change - Upgrade reagent to 1.2.0 - Namespace aliasing can be toggled in the settings. - Namespace aliasing is more performant, especially when turned off. +- Right-click popup menu "Copy Object" replaced with "Copy Subtree." + - Copies parent subtree of the clicked item. + +#### Fixed + +- Right-click popup menu is positioned more accurately. See #348. ## 1.8.1 (2023-07-26) diff --git a/src/day8/re_frame_10x/components/cljs_devtools.cljs b/src/day8/re_frame_10x/components/cljs_devtools.cljs index abf30b13..ce1ad2a1 100644 --- a/src/day8/re_frame_10x/components/cljs_devtools.cljs +++ b/src/day8/re_frame_10x/components/cljs_devtools.cljs @@ -21,15 +21,9 @@ [day8.re-frame-10x.styles :as styles] [day8.re-frame-10x.panels.app-db.events :as app-db.events] [day8.re-frame-10x.panels.app-db.subs :as app-db.subs] - [day8.re-frame-10x.fx.clipboard :as clipboard] - [day8.re-frame-10x.inlined-deps.reagent.v1v2v0.reagent.core :as r] - [day8.re-frame-10x.inlined-deps.reagent.v1v2v0.reagent.dom :as dom] - [day8.re-frame-10x.tools.coll :as tools.coll] [day8.re-frame-10x.tools.datafy :as tools.datafy] [day8.re-frame-10x.tools.reader.edn :as reader.edn] - [day8.re-frame-10x.panels.settings.subs :as settings.subs]) - (:import - [goog.dom TagName])) + [day8.re-frame-10x.panels.settings.subs :as settings.subs])) (def default-config @devtools.prefs/default-config) @@ -314,146 +308,46 @@ (prn-str-render data) (jsonml->hiccup (header data nil) (conj path 0)))])) -(def event-log (atom '())) ;;stores a history of the events, treated as a stack - -;; `html-element` is the html element that has received the right click -;; `app-db` is the full app db -;; `path` is the current path at the point where the popup is clicked in `data` -;; `html-target`, optional, is the element which the menus will be rendered in -(defn build-popup - [app-db path indexed-path html-element offset-x offset-y & [html-target]] - (let [popup-menu (goog.ui.PopupMenu.) - js-menu-style (-> #js {:text-align "center" - :padding "10px" - :border "1px solid #b9bdc6"} - (goog.style.toStyleAttribute)) - create-menu-item (fn [menu-text] - (-> (goog.dom.createDom - TagName.DIV - #js {} - (goog.dom.createDom TagName.SPAN #js {} menu-text)) - (doto (.setAttribute "style" js-menu-style)) - goog.ui.MenuItem.)) - copy-path-item (create-menu-item "Copy path") - copy-obj-item (create-menu-item "Copy object") - copy-repl-item (create-menu-item "Copy REPL command") - element-rect (.getBoundingClientRect html-element) - target-rect (when html-target (.getBoundingClientRect html-target)) - target-x-offset (when target-rect (+ (.-left target-rect) (.-scrollX js/window))) - element-x-pos (+ (.-left element-rect) (.-scrollX js/window)) - ;; element-x-pos is relative to window, so we remove offset of element we're rendering in below - menu-x-pos (+ offset-x - (if target-x-offset - (- element-x-pos target-x-offset) - element-x-pos)) - menu-y-pos (+ offset-y (.-top element-rect) (.-scrollY js/window))] - (doto copy-path-item - (.addClassName "copy-path") - (.addClassName "10x-menu-item")) - (doto copy-obj-item - (.addClassName "copy-object") - (.addClassName "10x-menu-item")) - (doto copy-repl-item - (.addClassName "copy-repl") - (.addClassName "10x-menu-item")) - (doto popup-menu - (.addItem copy-path-item) - (.addItem copy-obj-item) - (.addItem copy-repl-item) - (.showAt menu-x-pos menu-y-pos) - (.render (or html-target html-element))) ;;if menu target is not supplied we render on clicked element - (goog.object.forEach - goog.ui.Component.EventType - (fn [type] - (goog.events.listen - popup-menu - type - (fn [e] - (cond - (= (.-type e) "hide") - (when (= (peek @event-log) "highlight") - ;; if the last event registered is 'highlight' then we should not close the dialog - ;; `highlight` event is dispatched right before `action`. Action would not be dispatched - ;; if the preceding `highlight` closes the dialog - (.preventDefault e)) - - ;; `action` is thrown after hide - ;; `action` is thrown before unhighlight -> hide -> leave - (= (.-type e) "action") - (let [class-names (-> e .-target .getExtraClassNames js->clj) - object (tools.coll/get-in-with-lists-and-sets app-db path)] - (swap! event-log conj "action") - (cond - (some (fn [class-name] (= class-name "copy-object")) class-names) - (if (or object (= object false)) - (clipboard/copy! object) ;; note we can't copy nil objects - (js/console.error "Could not copy!")) - - (some (fn [class-name] (= class-name "copy-path")) class-names) - (clipboard/copy! path) - - (some (fn [class-name] (= class-name "copy-repl")) class-names) - (clipboard/copy! (str "(simple-render-with-path-annotations " app-db " " ["app-db-path" indexed-path] {} ")")))) - - :else - (swap! event-log conj (.-type e))))))))) - (defn simple-render-with-path-annotations - [data indexed-path {:keys [object path-id sort?] :as opts} & [class]] - (let [render-paths? (rf/subscribe [::app-db.subs/data-path-annotations?]) + [{:keys [data path path-id sort?] :as opts}] + (let [render-paths? @(rf/subscribe [::app-db.subs/data-path-annotations?]) open-new-inspectors? @(rf/subscribe [::settings.subs/open-new-inspectors?]) ns->alias @(rf/subscribe [::settings.subs/ns->alias]) alias? (and (seq ns->alias) @(rf/subscribe [::settings.subs/alias-namespaces?])) data (cond-> data alias? (tools.datafy/alias-namespaces ns->alias) - sort? tools.datafy/deep-sorted-map) - input-field-path (second indexed-path) ;;path typed in input-box - shadow-root (-> (.getElementById js/document "--re-frame-10x--") ;;main shadow-root html component - .-shadowRoot - .-children) - root-div (-> (filter (fn [element] ;; root re-frame-10x parent div - (= (.-tagName element) "DIV")) shadow-root) - first) - menu-html-target (some-> root-div .-firstChild) - menu-html-target (when (and menu-html-target - (= (.-childElementCount menu-html-target) 2)) ;; we will render menus on this element - (.-lastChild menu-html-target)) - ;; triggered during `contextmenu` event when a path annotation is right-clicked - menu-listener (fn [event] - ;; at this stage `data` might have changed - ;; we have to rely on `current-data` alias `obj` - (when-let [target (some-> event .-target .-parentElement)] - (let [path (.getAttribute target "data-path") - path-obj (reader.edn/read-string-maybe path) - offset-x (.-offsetX event) - offset-y (.-offsetY event)] - (.preventDefault event) - (when menu-html-target (build-popup object path-obj indexed-path target offset-x offset-y menu-html-target))))) - ;; triggered during `click` event when a path annotation is clicked - click-listener (fn [event] - (when-let [path (some-> event .-target .-parentElement (.getAttribute "data-path"))] - (when (= (.-button event) 0) ;;left click btn - (rf/dispatch [::app-db.events/update-path {:id path-id :path path}])))) - ;; triggered during `mousedown` event when an element is clicked. - middle-click-listener (fn [event] - (when-let [target (some-> event .-target .-parentElement)] - (let [path (.getAttribute target "data-path") - btn (.-button event)] - (.preventDefault event) - (when (= btn 1) ;;middle click btn - (rf/dispatch [::app-db.events/create-path-and-skip-to path open-new-inspectors?])))))] + sort? tools.datafy/deep-sorted-map)] [rc/box :size "1" - :class (str (jsonml-style) " " class) + :class (jsonml-style) :child (if (prn-str-render? data) (prn-str-render data) (jsonml->hiccup-with-path-annotations - (header data nil {:render-paths? @render-paths?}) - (conj indexed-path 0) - (or input-field-path []) - (assoc opts - :click-listener click-listener - :middle-click-listener middle-click-listener - :menu-listener menu-listener)))])) + (header data nil {:render-paths? render-paths?}) + ["app-db-path" path] + (or path []) + (merge + opts + {:click-listener #(when-let [path (some-> % .-target .-parentElement (.getAttribute "data-path"))] + (when (= (.-button %) 0) + (rf/dispatch [::app-db.events/update-path {:id path-id :path path}]))) + :middle-click-listener #(when-let [target (some-> % .-target .-parentElement)] + (let [path (.getAttribute target "data-path") + btn (.-button %)] + (.preventDefault %) + (when (= btn 1) + (rf/dispatch + [::app-db.events/create-path-and-skip-to path open-new-inspectors?])))) + :menu-listener #(do (.preventDefault %) + (rf/dispatch + [::app-db.events/open-popup-menu + {:data data + :mouse-position [(.-clientX %) (.-clientY %)] + :path path + :data-path (some-> % + .-target + .-parentElement + (.getAttribute "data-path") + reader.edn/read-string-maybe)}]))})))])) diff --git a/src/day8/re_frame_10x/fx/window.cljs b/src/day8/re_frame_10x/fx/window.cljs index 4ab063f2..b807a704 100644 --- a/src/day8/re_frame_10x/fx/window.cljs +++ b/src/day8/re_frame_10x/fx/window.cljs @@ -3,8 +3,11 @@ [goog.object :as gobj] [goog.string :as gstring] [clojure.string :as string] + [day8.re-frame-10x.inlined-deps.reagent.v1v2v0.reagent.core :as r] [day8.re-frame-10x.inlined-deps.re-frame.v1v3v0.re-frame.core :as rf])) +(def popout-window (r/atom nil)) + (defn m->str [m] (->> m @@ -50,9 +53,14 @@ (.write d window-html) (gobj/set w "onload" (partial on-load w d)) (.close d) - (rf/dispatch on-success)) + (rf/dispatch on-success) + (reset! popout-window w)) (rf/dispatch on-failure)))) (rf/reg-fx ::open-debugger-window open-debugger-window) + +(rf/reg-fx + ::close-debugger-window + (fn [] (reset! popout-window nil))) diff --git a/src/day8/re_frame_10x/material.cljs b/src/day8/re_frame_10x/material.cljs index f536d3fa..49324533 100644 --- a/src/day8/re_frame_10x/material.cljs +++ b/src/day8/re_frame_10x/material.cljs @@ -3,9 +3,48 @@ [day8.re-frame-10x.styles :as styles]) (:refer-clojure :exclude [print])) -;; Icons from https://material.io/resources/icons/ 'Sharp' theme. +;; Most icons from https://material.io/resources/icons/ 'Sharp' theme. ;; Names have been kept the same for ease of reference. +(defn clojure [{:keys [size] ;; from https://brandeps.com + :or {size styles/gs-19s}}] + [:svg {:version "1.1" + :id "Layer_1" + :width size + :height size + :viewBox "0 0 512 512" + :style {:enable-background "new 0 0 512 512"}} + [:style {:type "text/css"} ".st0{fill:#444444;}"] + [:g + [:path {:class "st0" + :d "M256,0C114.8,0,0,114.8,0,256s114.8,256,256,256c141.2,0,256-114.8,256-256S397.1,0,256,0z M256,24.6 c127.8,0,231.3,103.6,231.3,231.4c0,10-0.7,19.8-1.9,29.5c-6.3,25-17.6,41.7-31.2,53.1c-20.8,17.5-48.4,23.1-74.1,23.1 c-7.2,0-14.3-0.4-20.9-1.2c27-26.6,43.7-63.6,43.7-104.6c0-81.2-65.8-147-147-147c-21,0-41.1,4.5-59.2,12.4 c-2.3-1.6-4.6-3.1-7.1-4.5c-10.1-5.7-31.3-14-58.2-14.1c-19.5-0.2-41.8,4.8-63.6,18.5C110,62.8,178.5,24.7,256,24.6L256,24.6z M215.6,339.8c3.8-16.4,14-42,23.3-63.1c2.6-6,5.2-11.6,7.5-16.5c14.5,51.7,23.7,82.4,40.1,103.3c2.5,3.1,5.2,5.9,8,8.5 c-12.1,4-25.1,6.3-38.6,6.3c-14.8,0-29-2.7-42.1-7.5c-0.3-3.4-0.4-6.7-0.4-9.9C213.4,352.9,214.2,345.6,215.6,339.8L215.6,339.8z M184.3,355c-30.6-22.2-50.6-58.2-50.6-99c0.1-41.4,20.6-77.9,52-100c6.9,4,13.1,8.6,18.2,14c10.1,10.3,21.3,33.1,29.2,52.7 c2.1,5.3,4,10.3,5.7,14.9C210.6,295.1,191.8,323.7,184.3,355z M275.9,281.5c-6.3-16.3-10-28.5-10-28.5l0,0 c-11.5-44.3-23.5-84.7-48.3-113c12.1-4,25-6.2,38.4-6.2c67.6,0.1,122.2,54.8,122.3,122.3c-0.1,40.5-19.8,76.4-50.1,98.6 c-4.5-1.3-7.7-2.4-9.2-3.1c-4.8-2.1-11.8-9.1-18.2-18.9C291.1,317.9,282.2,297.8,275.9,281.5L275.9,281.5z M256,487.3 C128.2,487.3,24.6,383.8,24.6,256c0-8.4,0.5-16.7,1.3-24.9c19-64.5,64.5-88.3,107.6-88.7c8.8,0,17.3,1.1,25.5,3.1 C128.4,172.4,109,211.9,109,256c0,81.2,65.8,147,147,147c23.5,0,45.6-5.5,65.3-15.3c11.7,4.1,25.1,6.6,41.3,8.6 c6.1,0.7,12.6,1.1,19.6,1.1c18.7-0.1,40.4-2.8,63.1-8.4C403.3,448.4,334.2,487.3,256,487.3L256,487.3z"}]]]) + +(defn data-object [{:keys [size] + :or {size styles/gs-19s}}] + [:svg {:enable-background "new 0 0 24 24" + :height size + :viewBox "0 0 24 24" + :width size} + [:g + [:rect {:fill "none" :height "24" :width "24"}]] + [:g + [:g + [:path {:d "M4,7v2c0,0.55-0.45,1-1,1H2v4h1c0.55,0,1,0.45,1,1v2c0,1.65,1.35,3,3,3h3v-2H7c-0.55,0-1-0.45-1-1v-2 c0-1.3-0.84-2.42-2-2.83v-0.34C5.16,11.42,6,10.3,6,9V7c0-0.55,0.45-1,1-1h3V4H7C5.35,4,4,5.35,4,7z"}] + [:path {:d "M21,10c-0.55,0-1-0.45-1-1V7c0-1.65-1.35-3-3-3h-3v2h3c0.55,0,1,0.45,1,1v2c0,1.3,0.84,2.42,2,2.83v0.34 c-1.16,0.41-2,1.52-2,2.83v2c0,0.55-0.45,1-1,1h-3v2h3c1.65,0,3-1.35,3-3v-2c0-0.55,0.45-1,1-1h1v-4H21z"}]]]]) + +(defn data-array [{:keys [size] + :or {size styles/gs-19s}}] + [:svg {:enable-background "new 0 0 24 24" + :height size + :viewBox "0 0 24 24" + :width size} + [:g + [:rect {:fill "none" :height "24" :width "24"}]] + [:g + [:g + [:polygon {:points "15,4 15,6 18,6 18,18 15,18 15,20 20,20 20,4"}] + [:polygon {:points "4,20 9,20 9,18 6,18 6,6 9,6 9,4 4,4"}]]]]) + (defn add [{:keys [size]}] [:svg {:height size diff --git a/src/day8/re_frame_10x/navigation/events.cljs b/src/day8/re_frame_10x/navigation/events.cljs index 0d575b1f..9a81c615 100644 --- a/src/day8/re_frame_10x/navigation/events.cljs +++ b/src/day8/re_frame_10x/navigation/events.cljs @@ -36,7 +36,8 @@ [(rf/path [:settings :external-window?]) (local-storage/save "external-window?")] (fn [_ _] {:db false - :fx [[:dispatch-later {:ms 400 :dispatch [::settings.events/show-panel? true]}]]})) + :fx [[:dispatch-later {:ms 400 :dispatch [::settings.events/show-panel? true]}] + [::window/close-debugger-window]]})) (rf/reg-event-db ::dismiss-popup-failed diff --git a/src/day8/re_frame_10x/navigation/views.cljs b/src/day8/re_frame_10x/navigation/views.cljs index 9595a7ac..c0668bfd 100644 --- a/src/day8/re_frame_10x/navigation/views.cljs +++ b/src/day8/re_frame_10x/navigation/views.cljs @@ -33,7 +33,8 @@ [day8.re-frame-10x.panels.traces.views :as traces.views] [day8.re-frame-10x.material :as material] [day8.re-frame-10x.styles :as styles] - [day8.re-frame-10x.tools.shadow-dom :as tools.shadow-dom])) + [day8.re-frame-10x.tools.shadow-dom :as tools.shadow-dom] + [day8.re-frame-10x.popup :as popup])) #_(defglobal container-styles [:#--re-frame-10x-- @@ -333,50 +334,58 @@ [warnings external-window?] [errors external-window?] [tab-content]]]] - (if-not showing-settings? - [rc/v-split - :class (str (styles/normalize) " " (devtools-inner-style ambiance) " " (path-annotations-menu-style)) - :height "100%" - :width "100%" - :debug? debug? - :initial-split "10%" - :margin "0px" - :panel-1 [rc/v-box - :height "100%" - :width "100%" - :style {:overflow :auto} - :children - [[rc/h-box - :class (navigation-style ambiance) - :align :center - :height styles/gs-31s - :width "100%" - :gap styles/gs-19s + [rc/box + :style {:position "relative" + :width "100%" + :height "100%"} + :attr {:id "re-frame-10x__ui-container"} + :child + [:<> + (if-not showing-settings? + [rc/v-split + :class (str (styles/normalize) " " (devtools-inner-style ambiance) " " (path-annotations-menu-style)) + :height "100%" + :width "100%" + :debug? debug? + :initial-split "10%" + :margin "0px" + :panel-1 [rc/v-box + :height "100%" + :width "100%" + :style {:overflow :auto} :children - [(when-not (= @show-event-history? false) - [rc/label :label "Event History"]) + [[rc/h-box + :class (navigation-style ambiance) + :align :center + :height styles/gs-31s + :width "100%" + :gap styles/gs-19s + :children + [(when-not (= @show-event-history? false) + [rc/label :label "Event History"]) + (if-not (= @show-event-history? false) + [epochs.views/left-buttons] + [rc/box + :size "1" + :child [:div]]) + [epoch-filtering] + [show-history-button] + [rc/h-box + :gap styles/gs-12s + :style {:margin-right styles/gs-5s} + :children [[settings-button] + [popout-button external-window?] + [hide-panel-button external-window?]]]]] (if-not (= @show-event-history? false) - [epochs.views/left-buttons] - [rc/box - :size "1" - :child [:div]]) - [epoch-filtering] - [show-history-button] - [rc/h-box - :gap styles/gs-12s - :style {:margin-right styles/gs-5s} - :children [[settings-button] - [popout-button external-window?] - [hide-panel-button external-window?]]]]] - (if-not (= @show-event-history? false) - [epochs.views/epochs] - [rc/line])]] - :panel-2 panel-2] - [rc/box - :class (str (styles/normalize) " " (devtools-inner-style ambiance) " " (path-annotations-menu-style)) - :height "100%" - :width "100%" - :child panel-2]))) + [epochs.views/epochs] + [rc/line])]] + :panel-2 panel-2] + [rc/box + :class (str (styles/normalize) " " (devtools-inner-style ambiance) " " (path-annotations-menu-style)) + :height "100%" + :width "100%" + :child panel-2]) + [popup/menu]]])) (defn mount [popup-window popup-document] ;; When programming here, we need to be careful about which document and window diff --git a/src/day8/re_frame_10x/panels/app_db/events.cljs b/src/day8/re_frame_10x/panels/app_db/events.cljs index 25697206..7de74609 100644 --- a/src/day8/re_frame_10x/panels/app_db/events.cljs +++ b/src/day8/re_frame_10x/panels/app_db/events.cljs @@ -6,6 +6,7 @@ [clojure.string :as string] [zprint.core :as zp] [day8.re-frame-10x.inlined-deps.re-frame.v1v3v0.re-frame.core :as rf] + [day8.re-frame-10x.fx.window :refer [popout-window]] [day8.re-frame-10x.fx.local-storage :as local-storage] [day8.re-frame-10x.tools.reader.edn :as reader.edn])) @@ -214,3 +215,28 @@ ::save-to-file (fn [_ [_ s]] {::save-to-file s})) + +(rf/reg-cofx + ::window-bounds + (fn [cofx] + (let [rect (-> (or (some-> @popout-window .-document) js/document) + (.getElementById "--re-frame-10x--") + .-shadowRoot + (.getElementById "re-frame-10x__ui-container") + .getBoundingClientRect)] + (into cofx {::window-bounds [(.-x rect) (.-y rect)]})))) + +(rf/reg-event-fx + ::open-popup-menu + [(rf/inject-cofx ::window-bounds)] + (fn [{:keys [db] [wx wy] ::window-bounds} + [_ {:keys [path data data-path] [mx my] :mouse-position}]] + {:db (assoc db :popup-menu {:showing? true + :position [(- mx wx) (- my wy)] + :path path + :data data + :data-path data-path})})) + +(rf/reg-event-db + ::close-popup-menu + (fn [db _] (assoc db :popup-menu {:showing? false}))) diff --git a/src/day8/re_frame_10x/panels/app_db/subs.cljs b/src/day8/re_frame_10x/panels/app_db/subs.cljs index debd0ced..d603c6a6 100644 --- a/src/day8/re_frame_10x/panels/app_db/subs.cljs +++ b/src/day8/re_frame_10x/panels/app_db/subs.cljs @@ -65,3 +65,5 @@ :<- [::root] (fn [{:keys [expand-all?]} [_ path-id]] (get expand-all? path-id))) + +(rf/reg-sub ::popup-menu :-> :popup-menu) diff --git a/src/day8/re_frame_10x/panels/app_db/views.cljs b/src/day8/re_frame_10x/panels/app_db/views.cljs index e7a33d7b..ebf167af 100644 --- a/src/day8/re_frame_10x/panels/app_db/views.cljs +++ b/src/day8/re_frame_10x/panels/app_db/views.cljs @@ -284,11 +284,11 @@ :overflow-y "hidden"} :children [[cljs-devtools/simple-render-with-path-annotations - data - ["app-db-path" path] - {:path-id id - :sort? sort? - :object @app-db-after}]]]) + {:data data + :path path + :path-id id + :sort? sort? + :db @app-db-after}]]]) (when render-diff? (let [app-db-before (rf/subscribe [::app-db.subs/current-epoch-app-db-before]) [diff-before diff-after _] (when render-diff? diff --git a/src/day8/re_frame_10x/popup.cljs b/src/day8/re_frame_10x/popup.cljs new file mode 100644 index 00000000..a20aa174 --- /dev/null +++ b/src/day8/re_frame_10x/popup.cljs @@ -0,0 +1,49 @@ +(ns day8.re-frame-10x.popup + (:require [day8.re-frame-10x.inlined-deps.reagent.v1v2v0.reagent.core :as r] + [day8.re-frame-10x.components.re-com :as rc] + [day8.re-frame-10x.inlined-deps.re-frame.v1v3v0.re-frame.core :as rf] + [day8.re-frame-10x.components.buttons :as buttons] + [day8.re-frame-10x.panels.app-db.subs :as app-db.subs] + [day8.re-frame-10x.panels.app-db.events :as app-db.events] + + [day8.re-frame-10x.fx.clipboard :as clipboard] + [day8.re-frame-10x.material :as material])) + +(defn overlay [] + [:div {:style {:position "absolute" + :left 0 + :top 0 + :height "100%" + :width "100%" + :background-color "rgba(0,0,0,0.25)"} + :on-click #(rf/dispatch [::app-db.events/close-popup-menu])}]) + +(defn menu [] + (let [{[left top] :position + :keys [showing? path data data-path]} @(rf/subscribe [::app-db.subs/popup-menu])] + (when showing? + [:<> + [overlay] + [rc/v-box + :style {:position "absolute" + :left left + :top top + :text-align "center"} + :children + [[buttons/icon {:icon [material/data-array] + :label "Copy Path" + :on-click #(do (some-> data-path clipboard/copy!) + (rf/dispatch [::app-db.events/close-popup-menu]))}] + [buttons/icon {:icon [material/data-object] + :label "Copy Subtree" + :on-click #(do (some-> data + (get-in (drop (count path) (pop data-path))) + clipboard/copy!) + (rf/dispatch [::app-db.events/close-popup-menu]))}] + [buttons/icon {:icon [material/clojure] + :label "Copy REPL Cmd" + :on-click #(do (clipboard/copy! + (str + `(simple-render-with-path-annotations + ~{:data data :path path}))) + (rf/dispatch [::app-db.events/close-popup-menu]))}]]]])))