From d14cd01374a949601c2255337c10e0f10dfa975b Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 23 Oct 2026 09:23:08 +0200 Subject: [PATCH] :bug: Cascade structural edits through deleted component mains Add, delete and move changes inside a deleted main carry a component container ID. The change scanner ignored that destination, so it never synced the deleted component's surviving copies after a structural edit. Return the container's component ID for all three change kinds. The integration regression failed with dangling refs before the fix. It now removes the child from both the stored main and its copies and checks undo/redo. Focused common suites pass: 66 tests, 11263 assertions. Common lint passes. Sync benchmark ratios against the 1e baseline (lower quantile): main 10/100/1000 copies 0.55/0.65/0.61; three levels 0.52; chain 0.62; library 0.58; many shapes 0.69; variant switch 0.79. No regression flags. AI-assisted-by: GPT-6 --- common/src/app/common/files/changes.cljc | 21 +++++++++++-------- .../test/common_tests/files_changes_test.cljc | 10 +++++++++ .../logic/comp_remote_synced_test.cljc | 12 +++++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 84ec2f2b94..9703f68f32 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -1187,8 +1187,8 @@ #{component-id}))))) (defmethod components-changed :mov-objects - [file-data {:keys [page-id _component-id parent-id shapes] :as change}] - (when page-id + [file-data {:keys [page-id component-id parent-id shapes] :as change}] + (if page-id (let [page (ctpl/get-page file-data page-id) xform (comp (filter :main-instance) (map :component-id)) @@ -1201,27 +1201,30 @@ (reduce #(set/union %1 (check-shape %2 [])) (check-shape parent-id [parent-id]) - shapes)))) + shapes)) + (when component-id #{component-id}))) (defmethod components-changed :add-obj - [file-data {:keys [parent-id page-id _component-id] :as change}] - (when page-id + [file-data {:keys [parent-id page-id component-id] :as change}] + (if page-id (let [page (ctpl/get-page file-data page-id) parents (map (partial ctn/get-shape page) (cons parent-id (cfh/get-parent-ids (:objects page) parent-id))) xform (comp (filter :main-instance) (map :component-id))] - (into #{} xform parents)))) + (into #{} xform parents)) + (when component-id #{component-id}))) (defmethod components-changed :del-obj - [file-data {:keys [id page-id _component-id] :as change}] - (when page-id + [file-data {:keys [id page-id component-id] :as change}] + (if page-id (let [page (ctpl/get-page file-data page-id) shape-and-parents (map (partial ctn/get-shape page) (cons id (cfh/get-parent-ids (:objects page) id))) xform (comp (filter :main-instance) (map :component-id))] - (into #{} xform shape-and-parents)))) + (into #{} xform shape-and-parents)) + (when component-id #{component-id}))) (defmethod components-changed :default [_ _] diff --git a/common/test/common_tests/files_changes_test.cljc b/common/test/common_tests/files_changes_test.cljc index 935ff092fb..90a71dd452 100644 --- a/common/test/common_tests/files_changes_test.cljc +++ b/common/test/common_tests/files_changes_test.cljc @@ -1165,3 +1165,13 @@ (nil? (get-in result2 [:pages-index page-id :default-grids]))))) {:num 1000}))) + +(t/deftest structural-changes-in-a-deleted-main-trigger-its-component-sync + (let [component-id (uuid/next) + id (uuid/next) + parent-id (uuid/next)] + (doseq [change [{:type :add-obj :id id :parent-id parent-id} + {:type :del-obj :id id} + {:type :mov-objects :parent-id parent-id :shapes [id]}]] + (t/is (= #{component-id} + (ch/components-changed {} (assoc change :component-id component-id))))))) diff --git a/common/test/common_tests/logic/comp_remote_synced_test.cljc b/common/test/common_tests/logic/comp_remote_synced_test.cljc index b4dac4c336..9164e4a841 100644 --- a/common/test/common_tests/logic/comp_remote_synced_test.cljc +++ b/common/test/common_tests/logic/comp_remote_synced_test.cljc @@ -9,6 +9,7 @@ [app.common.test-helpers.composable.comp.nodes :as n] [app.common.test-helpers.composable.comp.runner :as r] [app.common.test-helpers.composable.comp.setups :as setup] + [app.common.test-helpers.composable.comp.undo-check :as uc] [app.common.test-helpers.composable.core :as tm] [app.common.test-helpers.ids-map :as thi] [clojure.test :as t])) @@ -36,3 +37,14 @@ main (tm/shape-by-id situation (nested-main-rect situation))] (t/is (= "Authored name" (:name main))) (t/is (contains? (:touched main) :name-group)))) + +(t/deftest inner-child-removal-propagates-through-a-deleted-outer-main + (let [situation (r/run-variant + {:setup setup/nested-component-with-copy + :operation (tm/in-sequence [(n/delete-component :main2-root) + (n/remove-child :main1-child)])}) + copy (tm/resolve-shape situation :copy2-child) + stored (get-in (tm/file situation) [:data :components (thi/id :component2) :objects])] + (t/is (empty? (:shapes copy))) + (t/is (= 2 (count stored))) + (t/is (empty? (uc/failures situation)))))