From 29072e4c6b7e9a0e485f52ff3826b9387ca5aecc Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Wed, 7 Oct 2026 13:07:07 +0200 Subject: [PATCH] :bug: Fix path editor crash on shortcut (#11996) --- .../app/main/data/workspace/path/drawing.cljs | 42 ++++++---- .../main/data/workspace/path/shortcuts.cljs | 1 + .../logic/path_lifecycle_test.cljs | 77 +++++++++++++++---- 3 files changed, 89 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index f2a0ab1377..6a2fe3a174 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -37,13 +37,16 @@ (declare change-edit-mode) (defn start-created-path-edition - [id] - (ptk/reify ::start-created-path-edition - ptk/WatchEvent - (watch [_ _ _] - (rx/of (dwe/start-edition-mode id) - (edition/start-path-edit id) - (change-edit-mode :draw))))) + "Edits a just created path in `edit-mode` (`:draw` by default)." + ([id] + (start-created-path-edition id :draw)) + ([id edit-mode] + (ptk/reify ::start-created-path-edition + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dwe/start-edition-mode id) + (edition/start-path-edit id) + (change-edit-mode edit-mode)))))) ;; Draw-loop stop signals either restart the same path or exit drawing. @@ -434,11 +437,12 @@ (when-let [content (dm/get-in state [:workspace-drawing :object :content])] (cond (and (> (count content) 1) restart?) - (rx/of (common/finish-path) - (clean-drawn-content) - (setup-frame) - (dwdc/handle-finish-drawing) - (start-created-path-edition shape-id)) + (let [edit-mode (dm/get-in state [:workspace-local :edit-path shape-id :edit-mode])] + (rx/of (common/finish-path) + (clean-drawn-content) + (setup-frame) + (dwdc/handle-finish-drawing) + (start-created-path-edition shape-id (d/nilv edit-mode :draw)))) (> (count content) 1) (rx/of (clean-drawn-content) @@ -545,6 +549,10 @@ state))) (defn change-edit-mode + "Switches the active path between `:draw` and `:move`. + + A new path being drawn has no edition yet; switching it to `:move` + finishes the drawing, which then reenters edition in `:move`." [mode] (ptk/reify ::change-edit-mode ptk/UpdateEvent @@ -552,16 +560,20 @@ (if-let [id (dm/get-in state [:workspace-local :edition])] (cond-> (d/update-in-when state [:workspace-local :edit-path id] assoc :edit-mode mode) (= mode :draw) (enter-draw-from-selected-node id)) - state)) + (cond-> state + (and (= mode :move) (st/drawing? state)) + (assoc-in [:workspace-local :edit-path (st/get-path-id state) :edit-mode] :move)))) ptk/WatchEvent (watch [_ state _] - (when-let [id (dm/get-in state [:workspace-local :edition])] + (if-let [id (dm/get-in state [:workspace-local :edition])] (let [mode (dm/get-in state [:workspace-local :edit-path id :edit-mode])] (case mode :move (rx/of (common/finish-path)) :draw (rx/of (start-draw-mode)) - (rx/empty))))))) + (rx/empty))) + (when (and (= mode :move) (st/drawing? state)) + (rx/of (common/finish-path))))))) (defn reset-last-handler [] diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs index 6a4b620afa..c1d2269383 100644 --- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -157,6 +157,7 @@ :label (fn [] (tr "shortcuts.deselect-all")) :command (ds/c-mod "shift+a") :subsections [:path-editor] + :overwrite true :fn #(st/emit! (drp/deselect-all))} :flip-horizontal {:tooltip (ds/shift "H") diff --git a/frontend/test/frontend_tests/logic/path_lifecycle_test.cljs b/frontend/test/frontend_tests/logic/path_lifecycle_test.cljs index 1e88902f90..2dca2a405f 100644 --- a/frontend/test/frontend_tests/logic/path_lifecycle_test.cljs +++ b/frontend/test/frontend_tests/logic/path_lifecycle_test.cljs @@ -25,6 +25,7 @@ [app.main.data.workspace.path.streams :as path.streams] [app.main.data.workspace.path.tools :as path.tools] [app.main.data.workspace.path.undo :as path.undo] + [app.main.data.workspace.shortcuts :as wsc] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true] [frontend-tests.helpers.state :as ths] @@ -138,22 +139,23 @@ (defn- run-handle-drawing-end "Runs the draw-ending flow and passes its events to `callback`." - [restart? callback] - (let [state (pth/drawing-path-state) - stream (rx/subject) - emissions (atom [])] - (->> (ptk/watch (path.drawing/handle-drawing) state stream) - (rx/subs! #(swap! emissions conj %))) - (rx/push! stream (ptk/data-event ::path.drawing/end-edition - {:restart? restart?})) - ;; Wait for the asynchronous drawing-end event. - (js/setTimeout - (fn [] - (let [end-event (last @emissions) - end-emissions (atom [])] - (->> (ptk/watch end-event state stream) - (rx/subs! #(swap! end-emissions conj %))) - (callback @end-emissions)))))) + ([restart? callback] + (run-handle-drawing-end (pth/drawing-path-state) restart? callback)) + ([state restart? callback] + (let [stream (rx/subject) + emissions (atom [])] + (->> (ptk/watch (path.drawing/handle-drawing) state stream) + (rx/subs! #(swap! emissions conj %))) + (rx/push! stream (ptk/data-event ::path.drawing/end-edition + {:restart? restart?})) + ;; Wait for the asynchronous drawing-end event. + (js/setTimeout + (fn [] + (let [end-event (last @emissions) + end-emissions (atom [])] + (->> (ptk/watch end-event state stream) + (rx/subs! #(swap! end-emissions conj %))) + (callback @end-emissions))))))) (t/deftest escape-ending-new-path-draw-does-not-reenter-edition (t/async @@ -182,6 +184,43 @@ (mapv ptk/type emissions))) (done))))) +(t/deftest finishing-new-path-draw-in-move-mode-reenters-edition-in-move-mode + (t/async + done + (let [state (pth/drawing-path-state) + id (get-in state [:workspace-drawing :object :id]) + state (assoc-in state [:workspace-local :edit-path id :edit-mode] :move)] + (run-handle-drawing-end + state + true + (fn [emissions] + (let [edition-emissions (atom [])] + (->> (ptk/watch (last emissions) state nil) + (rx/subs! #(swap! edition-emissions conj %))) + (let [change-mode (last @edition-emissions) + edited {:workspace-local {:edition id + :edit-path {id {:edit-mode :draw}}}}] + (t/is (= ::path.drawing/change-edit-mode (ptk/type change-mode))) + (t/is (= :move (get-in (ptk/update change-mode edited) + [:workspace-local :edit-path id :edit-mode]))))) + (done)))))) + +(t/deftest move-mode-while-creating-path-finishes-it + (let [id (random-uuid) + state {:workspace-local + {:edit-path {id {:edit-mode :draw + :last-point (gpt/point 10 10)}}} + :workspace-drawing {:tool :path + :object {:id id :type :path}}} + event (path.drawing/change-edit-mode :move) + state' (ptk/update event state) + emissions (atom [])] + (t/is (= :move (get-in state' [:workspace-local :edit-path id :edit-mode]))) + (->> (ptk/watch event state' nil) + (rx/subs! #(swap! emissions conj %))) + (t/is (= [::path.common/finish-path] + (mapv ptk/type @emissions))))) + (t/deftest ending-a-draw-collapses-the-nodes-drawn-on-top-of-each-other (t/async done @@ -237,6 +276,12 @@ (t/is (= [::path.common/finish-path] (mapv ptk/type @emissions))))) +(t/deftest deselect-all-shortcut-replaces-the-grid-layout-toggle + (let [deselect-all (:deselect-all path.shortcuts/shortcuts) + grid-toggle (:toggle-layout-grid wsc/shortcuts)] + (t/is (= (:command grid-toggle) (:command deselect-all))) + (t/is (true? (:overwrite deselect-all))))) + (t/deftest escape-without-pending-segment-interrupts-edition (let [id (random-uuid) state {:workspace-local