From 60ec9d7953e7780c3ea8b62f6021ce1c8679a1f8 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 | 44 ++++++++++++++----- 3 files changed, 40 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 c84de4e0e5..a991b8d1a2 100644 --- a/common/test/common_tests/types/absorb_assets_test.cljc +++ b/common/test/common_tests/types/absorb_assets_test.cljc @@ -7,6 +7,8 @@ (ns common-tests.types.absorb-assets-test (:require [app.common.data :as d] + [app.common.files.changes-builder :as pcb] + [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] @@ -60,22 +62,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))))))))