From 03afe5de80a06c37fb1a9d19d81f43a44bf0d201 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 22 Oct 2026 14:32:04 +0200 Subject: [PATCH] :bug: Preserve nested overrides when absorbing a component Keep touched groups and remote sync state when cloning an absorbed main. New component copies still start clean. Extend the nested absorption test with head and descendant overrides, then sync the inner component (F107). Validation: red regression before the fix; common JVM 1705 tests and JS 1633 tests, backend 1610 tests, exporter 14 tests pass. AI-assisted-by: GPT-6 --- .../memories/common/component-data-model.md | 3 ++ common/src/app/common/types/container.cljc | 5 ++- .../types/absorb_assets_test.cljc | 43 ++++++++++++++----- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.serena/memories/common/component-data-model.md b/.serena/memories/common/component-data-model.md index e7f062df35..0a176a77c1 100644 --- a/.serena/memories/common/component-data-model.md +++ b/.serena/memories/common/component-data-model.md @@ -40,6 +40,9 @@ Masters are not normally touched through `set-shape-attr`, but touched flags can `make-component-instance` in `app.common.types.container` produces a clean component copy through `update-new-shape`, dissociating attrs such as `:touched`, `:remote-synced`, `:variant-id`, and `:variant-name` on cloned shapes. +With `:main-instance? true` (library absorption), nested copies keep their +`:touched` and `:remote-synced`; they remain copies inside the new main. + `duplicate-component` in `app.common.logic.duplicate` creates a new component master by cloning existing component shapes, setting component metadata, and applying a position delta. It does not have the same clean-copy semantics as `make-component-instance`, so inherited attrs on the source can matter. When a bug depends on touched state, identify which cloning path produced the shape before changing sync logic. diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index 6d160cb25e..24e23cc28e 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -347,7 +347,10 @@ (cond-> new-shape :always (-> (gsh/move delta) - (dissoc :touched :remote-synced :variant-id :variant-name)) + (dissoc :variant-id :variant-name)) + + (not main-instance?) + (dissoc :touched :remote-synced) (and main-instance? root?) (assoc :main-instance true) diff --git a/common/test/common_tests/types/absorb_assets_test.cljc b/common/test/common_tests/types/absorb_assets_test.cljc index d053fa6735..f8b5bb560b 100644 --- a/common/test/common_tests/types/absorb_assets_test.cljc +++ b/common/test/common_tests/types/absorb_assets_test.cljc @@ -10,6 +10,7 @@ [app.common.files.changes-builder :as pcb] [app.common.files.validate :as cfv] [app.common.logic.shapes :as cls] + [app.common.logic.libraries :as cll] [app.common.test-helpers.components :as thc] [app.common.test-helpers.compositions :as tho] [app.common.test-helpers.files :as thf] @@ -106,22 +107,42 @@ (t/deftest absorb-nested-components ;; component2's main nests a copy of component1, and has an annotation; ;; the file has a copy of component2 - (let [library (-> (thf/sample-file :library :is-shared true) - (tho/add-nested-component :component1 :main1-root :main1-child - :component2 :main2-root :nested-head) - (thc/update-component :component2 :annotation "Note")) - file (-> (thf/sample-file :file) - (thc/instantiate-component :component2 :copy2-root :library library - :children-labels [:copy-nested-head])) - file' (ctf/update-file-data file #(ctf/absorb-assets % (:data library))) - data' (ctf/file-data file') - page' (second (ctpl/pages-seq data')) - nested' (get-in page' [:objects (thi/id :nested-head)])] + (let [library (-> (thf/sample-file :library :is-shared true) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (ths/update-shape :nested-head :opacity 0.5) + (as-> library + (ths/update-shape-by-id + library (first (:shapes (ths/get-shape library :nested-head))) + :fills (ths/sample-fills-color :fill-color "#FF0000"))) + (thc/update-component :component2 :annotation "Note")) + file (-> (thf/sample-file :file) + (thc/instantiate-component :component2 :copy2-root :library library + :children-labels [:copy-nested-head])) + file' (ctf/update-file-data file #(ctf/absorb-assets % (:data library))) + data' (ctf/file-data file') + page' (second (ctpl/pages-seq data')) + nested' (get-in page' [:objects (thi/id :nested-head)]) + child-id (first (:shapes nested')) + synced (thf/apply-changes + file' + (:changes (cll/generate-sync-components-cascade + (pcb/empty-changes) (:id file') #{(thi/id :component1)} + {(:id file') file'} (:id file')))) + nested-synced (get-in synced [:data :pages-index (:id page') :objects (thi/id :nested-head)])] (thf/validate-file! file') (t/is (= 2 (count (ctkl/components-seq data')))) (t/testing "the nested copy in the absorbed main keeps its link" (t/is (= (thi/id :main1-root) (:shape-ref nested'))) (t/is (= (:id file') (:component-file nested')))) + (t/testing "nested overrides survive absorption and a later sync" + (t/is (= #{:layer-effects-group} (:touched nested'))) + (t/is (= 0.5 (:opacity nested-synced))) + (t/is (= #{:layer-effects-group} (:touched nested-synced)))) + (t/testing "the nested descendant keeps its fill override" + (let [child (get-in synced [:data :pages-index (:id page') :objects child-id])] + (t/is (= "#FF0000" (-> child :fills first :fill-color))) + (t/is (= #{:fill-group} (:touched child))))) (t/testing "the component keeps its annotation" (t/is (= "Note" (:annotation (ctkl/get-component data' (thi/id :component2))))))))