From aedd8cc11ea8ad0b3816a80652df6cf3eb4452ea Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 3 Dec 2025 17:01:38 +0100 Subject: [PATCH 1/2] :bug: Fix problem when renaming variants in plugins --- frontend/src/app/main/data/workspace.cljs | 40 ++++++++++++++++++- .../main/ui/workspace/sidebar/layer_name.cljs | 11 +---- frontend/src/app/plugins/shape.cljs | 2 +- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index c3d0990d6e..c550c150a5 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -20,7 +20,9 @@ [app.common.path-names :as cpn] [app.common.transit :as t] [app.common.types.component :as ctc] + [app.common.types.components-list :as ctkl] [app.common.types.shape :as cts] + [app.common.types.variant :as ctv] [app.common.uuid :as uuid] [app.main.data.changes :as dch] [app.main.data.comments :as dcmt] @@ -551,7 +553,6 @@ component-id (:component-id shape) undo-id (js/Symbol)] - (when valid? (if (ctc/is-variant-container? shape) ;; Rename the full variant when it is a variant container @@ -566,6 +567,43 @@ (dwl/rename-component component-id clean-name)) (dwu/commit-undo-transaction undo-id)))))))))) +(defn rename-shape-or-variant + ([id name] + (rename-shape-or-variant nil nil id name)) + ([file-id page-id id name] + (ptk/reify ::rename-shape-or-variant + ptk/WatchEvent + (watch [_ state _] + (let [file-id (d/nilv file-id (:current-file-id state)) + page-id (d/nilv page-id (:current-page-id state)) + + file-data (dsh/lookup-file-data state file-id) + shape + (-> (dsh/lookup-page-objects state file-id page-id) + (get id)) + + is-variant? (ctc/is-variant? shape) + variant-id (when is-variant? (:variant-id shape)) + variant-name (when is-variant? (:variant-name shape)) + component-id (:component-id shape) + component (ctkl/get-component file-data (:component-id shape)) + variant-properties (:variant-properties component)] + (cond + (and variant-name (ctv/valid-properties-formula? name)) + (rx/of (dwva/update-properties-names-and-values + component-id variant-id variant-properties (ctv/properties-formula->map name)) + (dwva/remove-empty-properties variant-id) + (dwva/update-error component-id)) + + variant-name + (rx/of (dwva/update-properties-names-and-values + component-id variant-id variant-properties {}) + (dwva/remove-empty-properties variant-id) + (dwva/update-error component-id name)) + + :else + (rx/of (end-rename-shape id name)))))))) + ;; --- Update Selected Shapes attrs (defn update-selected-shapes diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs index 144e27a2e6..642b3d27f5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs @@ -11,7 +11,6 @@ [app.common.data.macros :as dm] [app.common.types.variant :as ctv] [app.main.data.workspace :as dw] - [app.main.data.workspace.variants :as dwv] [app.main.store :as st] [app.util.debug :as dbg] [app.util.dom :as dom] @@ -69,15 +68,7 @@ name (str/trim (dom/get-value name-input))] (on-stop-edit) (reset! edition* false) - (if variant-name - (if (ctv/valid-properties-formula? name) - (st/emit! (dwv/update-properties-names-and-values component-id variant-id variant-properties (ctv/properties-formula->map name)) - (dwv/remove-empty-properties variant-id) - (dwv/update-error component-id)) - (st/emit! (dwv/update-properties-names-and-values component-id variant-id variant-properties {}) - (dwv/remove-empty-properties variant-id) - (dwv/update-error component-id name))) - (st/emit! (dw/end-rename-shape shape-id name)))))) + (st/emit! (dw/rename-shape-or-variant shape-id name))))) cancel-edit (mf/use-fn diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index b047954397..c7bdbb3866 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -218,7 +218,7 @@ (u/display-not-valid :name value) :else - (st/emit! (dw/end-rename-shape id value)))))} + (st/emit! (dw/rename-shape-or-variant file-id page-id id value)))))} :blocked {:this true From 6251fa6b22e8495ba81a51ad37c74bce6776b936 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 3 Dec 2025 18:50:00 +0100 Subject: [PATCH 2/2] :bug: Close other open context menus on open a context menu (#7895) --- .../main/ui/components/context_menu_a11y.cljs | 41 ++++++++++++++----- frontend/src/app/util/globals.js | 16 +++++++- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.cljs b/frontend/src/app/main/ui/components/context_menu_a11y.cljs index 37f532d25f..5a6c55f2b2 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.cljs +++ b/frontend/src/app/main/ui/components/context_menu_a11y.cljs @@ -14,6 +14,7 @@ [app.main.ui.components.dropdown :refer [dropdown-content*]] [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] + [app.util.globals :as ug] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.timers :as tm] @@ -53,14 +54,13 @@ (def ^:private valid-option? (sm/lazy-validator schema:option)) -(mf/defc context-menu* - [{:keys [show on-close options selectable selected +(mf/defc context-menu-inner* + [{:keys [on-close options selectable selected top left fixed min-width origin width] :as props}] (assert (every? valid-option? options) "expected valid options") (assert (fn? on-close) "missing `on-close` prop") - (assert (boolean? show) "missing `show` prop") (assert (vector? options) "missing `options` prop") (let [width (d/nilv width "initial") @@ -80,14 +80,15 @@ offset-x (get state :offset-x) offset-y (get state :offset-y) levels (get state :levels) + internal-id (mf/use-id) on-local-close (mf/use-fn (mf/deps on-close) (fn [] - (swap! state* assoc :levels [{:parent nil - :options options}]) - (on-close))) + (swap! state* assoc :levels [{:parent nil :options options}]) + (when (fn? on-close) + (on-close)))) props (mf/spread-props props {:on-close on-local-close}) @@ -216,11 +217,22 @@ (swap! state* assoc :levels [{:parent nil :options options}])) + (mf/with-effect [internal-id] + (ug/dispatch! (ug/event "penpot:context-menu:open" #js {:id internal-id}))) + + (mf/with-effect [internal-id on-local-close] + (letfn [(on-event [event] + (when-let [detail (unchecked-get event "detail")] + (when (not= internal-id (unchecked-get detail "id")) + (on-local-close event))))] + (ug/listen "penpot:context-menu:open" on-event) + (partial ug/unlisten "penpot:context-menu:open" on-event))) + (mf/with-effect [ids] (tm/schedule-on-idle #(dom/focus! (dom/get-element (first ids))))) - (when (and show (some? levels)) + (when (some? levels) [:> dropdown-content* props (let [level (peek levels) options (:options level) @@ -229,7 +241,7 @@ [:div {:class (stl/css-case :is-selectable selectable :context-menu true - :is-open show + :is-open true :fixed fixed) :style {:top (+ top offset-y) :left (+ left offset-x)} @@ -241,7 +253,7 @@ :role "menu" :ref check-menu-offscreen} - (when-let [parent (:parent level)] + (when parent [:* [:li {:id "go-back-sub-option" :class (stl/css :context-menu-item) @@ -256,7 +268,7 @@ [:li {:class (stl/css :separator)}]]) - (for [[index option] (d/enumerate (:options level))] + (for [[index option] (d/enumerate options)] (let [name (:name option) id (:id option) sub-options (:options option) @@ -297,3 +309,12 @@ :data-testid id} name [:span {:class (stl/css :submenu-icon)} deprecated-icon/arrow]])]))))]])]))) + +(mf/defc context-menu* + {::mf/private true} + [{:keys [show] :as props}] + + (assert (boolean? show) "expected `show` prop to be a boolean") + + (when ^boolean show + [:> context-menu-inner* props])) diff --git a/frontend/src/app/util/globals.js b/frontend/src/app/util/globals.js index ecdff5fbb0..4f982a8bd7 100644 --- a/frontend/src/app/util/globals.js +++ b/frontend/src/app/util/globals.js @@ -35,14 +35,26 @@ goog.scope(function () { }; } - self.event = function(...args) { - return new CustomEvent(...args); + self.event = function(name, detail) { + const options = {}; + if (detail !== undefined) { + options.detail = detail; + } + return new CustomEvent(name, options); }; self.dispatch_BANG_ = function(...args) { self.document.dispatchEvent(...args); }; + self.listen = function(...args) { + self.document.addEventListener(...args); + }; + + self.unlisten = function(...args) { + self.document.removeEventListener(...args); + } + self.window = (function () { if (typeof goog.global.window !== "undefined") { return goog.global.window;