diff --git a/.serena/memories/common/component-data-model.md b/.serena/memories/common/component-data-model.md index 2ee13c9176..2734207f9e 100644 --- a/.serena/memories/common/component-data-model.md +++ b/.serena/memories/common/component-data-model.md @@ -61,3 +61,8 @@ A variant container is a frame with `:is-variant-container true`. Its children a Predicates are broad: `ctk/is-variant?` checks `:variant-id` and applies to both variant master shapes and component rows; `ctk/is-variant-container?` checks the container shape flag. Moving/dropping a shape into a variant container through the move-to-frame path can auto-convert it into a variant via `generate-make-shapes-variant`, which may duplicate the underlying component. Treat drag/drop into variant containers as a component/variant operation, not a plain reparent. + +- `ctk/remove-swap-group` removes `:touched` when it removes the final group. + New main clones must use the same empty-state representation as + `process-operation :set-touched`; otherwise a later move+undo changes + `:touched #{}` to a missing field and cannot restore the exact state (F156). diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index b99966435b..05b9f0599b 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -274,9 +274,12 @@ (defn remove-swap-group "Make the shape not swapped." [shape] - (cond-> shape - (swapped? shape) - (update :touched disj :swap-group))) + (if (swapped? shape) + (let [touched (disj (:touched shape) :swap-group)] + (if (seq touched) + (assoc shape :touched touched) + (dissoc shape :touched))) + shape)) (defn get-deleted-component-root [component] diff --git a/common/test/common_tests/logic/swap_group_test.cljc b/common/test/common_tests/logic/swap_group_test.cljc index 66b0557dc8..93766cce1b 100644 --- a/common/test/common_tests/logic/swap_group_test.cljc +++ b/common/test/common_tests/logic/swap_group_test.cljc @@ -805,3 +805,19 @@ head (tm/resolve-shape swapped :copy-star)] (t/is (mth/close? 45 (:rotation head) 0.001)) (t/is (= #{:swap-group} (:touched head))))) + +(t/deftest removing-the-last-swap-group-leaves-no-empty-override-field + (let [shape {:touched #{:swap-group}}] + (t/is (not (contains? (ctk/remove-swap-group shape) :touched))) + (t/is (= #{:geometry-group} + (:touched (ctk/remove-swap-group (assoc shape :touched #{:swap-group :geometry-group}))))))) + +(t/deftest moving-a-swapped-subtree-pushed-into-a-main-undoes-exactly + (let [updated (-> (tm/make-situation (setup)) + (r/step (n/update-main :copy)) r/settle) + file (tm/file updated) + head (tm/shape-by-id updated (:shape-ref (tm/resolve-shape updated :copy-star))) + moved (-> updated (r/step (n/nest-copy (constantly (:id head)) :star-main)) r/settle) + undone (r/step moved (n/undo))] + (t/is (nil? (:touched head))) + (t/is (= (thn/normalize-file file) (thn/normalize-file (tm/file undone))))))