From fb7497b4b70f6f1d4d1d1a6080a3561a31bf7cd6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 26 Aug 2026 17:09:17 +0000 Subject: [PATCH] :bug: Sync grid cells when converting a component frame to grid Converting a component's main frame to a grid only propagated the :layout and track attributes to its copies: update-grid-copy-attrs was gated on the copy shape (still a plain frame at sync time) and, once running, merge-cells kept the copy's empty cells instead of taking the main's. Copies ended up with no cells and a later reflow snapped their children back to the old order. Gate the grid sync on the main shape (like the flex branch) and take the main's remapped cells wholesale when the copy has not touched its :layout-grid-cells group. AI-assisted-by: deepseek-v4-pro --- common/src/app/common/logic/libraries.cljc | 10 +- .../common_tests/logic/comp_sync_test.cljc | 156 ++++++++++++++++++ 2 files changed, 164 insertions(+), 2 deletions(-) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index b234263468..f6a77d61df 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -1000,7 +1000,7 @@ changes (cond-> changes - (ctsl/grid-layout? shape-inst) + (ctsl/grid-layout? shape-main) (update-grid-copy-attrs (:id shape-inst) shape-main @@ -2477,9 +2477,15 @@ ;; Take cells from main and remap the shapes to assign it to the copy copy-cells (-> shape-copy :layout-grid-cells (remove-orphan-cells shape-copy)) main-cells (-> shape-main (ctsl/remap-grid-cells ids-map) :layout-grid-cells)] + + ;; When the copy hasn't overridden its grid cells, take the main's + ;; cells wholesale (positions + assignments + new/removed cells). + ;; Otherwise preserve the copy's overrides cell by cell. (-> shape-copy (assoc :layout-grid-cells - (ctsl/merge-cells main-cells copy-cells omit-touched?)) + (if (ctk/touched-group? shape-copy :layout-grid-cells) + (ctsl/merge-cells main-cells copy-cells omit-touched?) + main-cells)) (ctsl/assign-cells objects)))) {:ignore-touched true :with-objects? true}))) diff --git a/common/test/common_tests/logic/comp_sync_test.cljc b/common/test/common_tests/logic/comp_sync_test.cljc index 8a7b652328..6adac4d9ea 100644 --- a/common/test/common_tests/logic/comp_sync_test.cljc +++ b/common/test/common_tests/logic/comp_sync_test.cljc @@ -8,8 +8,11 @@ (:require [app.common.data :as d] [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.flex-layout :as flex] + [app.common.geom.shapes.grid-layout :as grid] [app.common.logic.libraries :as cll] [app.common.logic.shapes :as cls] [app.common.test-helpers.components :as thc] @@ -19,6 +22,7 @@ [app.common.test-helpers.shapes :as ths] [app.common.types.component :as ctk] [app.common.types.shape-tree :as ctst] + [app.common.types.shape.layout :as ctl] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) @@ -332,6 +336,158 @@ (t/is (= (first (:shapes copy-root')) (:id copy-child2'))) (t/is (= (nth (:shapes copy-root') 2) (:id copy-child3'))))) +(t/deftest test-sync-grid-cells-when-converting-main-to-grid + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy + :component1 + :main-root + [:main-child1 :main-child2 :main-child3] + :copy-root + :main-root-params {:width 400 :height 100} + :main-child-params-list [{:x 0 :y 0 :width 50 :height 50} + {:x 100 :y 0 :width 50 :height 50} + {:x 200 :y 0 :width 50 :height 50}] + :copy-root-params {:children-labels [:copy-child1 :copy-child2 :copy-child3]})) + + page (thf/current-page file) + main-root (ths/get-shape file :main-root) + + ;; ==== Action + ;; Convert the main frame into a grid, mirroring the editor's + ;; `get-layout-initializer` path (merge layout attrs + calculate-params + ;; + assign-cells + reorder-grid-children). + changes1 (cls/generate-update-shapes + (pcb/empty-changes nil (:id page)) + #{(:id main-root)} + (fn [shape objects] + (let [children (cfh/get-immediate-children objects (:id shape)) + shape (merge shape + {:layout :grid + :layout-grid-dir :row + :layout-grid-rows [] + :layout-grid-columns [] + :layout-grid-cells {} + :layout-gap {:row-gap 0 :column-gap 0} + :layout-align-items :start + :layout-justify-items :start + :layout-align-content :stretch + :layout-justify-content :stretch + :layout-padding-type :simple + :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}}) + shape (merge shape (grid/calculate-params objects children shape))] + (-> shape + (ctl/assign-cells objects) + ctl/reorder-grid-children))) + (:objects page) + {:with-objects? true}) + + updated-file (thf/apply-changes file changes1) + updated-main-root (ths/get-shape updated-file :main-root) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child1' (ths/get-shape file' :copy-child1) + copy-child2' (ths/get-shape file' :copy-child2) + copy-child3' (ths/get-shape file' :copy-child3) + + ids-map (into {} + (map (juxt :shape-ref :id)) + [copy-child1' copy-child2' copy-child3']) + + main-cells-remapped (ctl/remap-grid-cells updated-main-root ids-map) + + cell-signature + (fn [cells] + (->> cells + vals + (map (fn [cell] [(:row cell) (:column cell) (vec (:shapes cell))])) + set))] + + ;; ==== Check + (t/is (ctl/grid-layout? copy-root')) + (t/is (= (count (:layout-grid-cells updated-main-root)) + (count (:layout-grid-cells copy-root')))) + (t/is (= (cell-signature (:layout-grid-cells main-cells-remapped)) + (cell-signature (:layout-grid-cells copy-root')))) + (t/is (= (mapv ids-map (:shapes updated-main-root)) + (:shapes copy-root'))))) + +(t/deftest test-sync-flex-layout-when-converting-main-to-flex + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy + :component1 + :main-root + [:main-child1 :main-child2 :main-child3] + :copy-root + :main-root-params {:width 400 :height 100} + :main-child-params-list [{:x 0 :y 0 :width 50 :height 50} + {:x 100 :y 0 :width 50 :height 50} + {:x 200 :y 0 :width 50 :height 50}] + :copy-root-params {:children-labels [:copy-child1 :copy-child2 :copy-child3]})) + + page (thf/current-page file) + main-root (ths/get-shape file :main-root) + + ;; ==== Action + ;; Convert the main frame into a flex layout, mirroring the editor's + ;; `get-layout-initializer` path. + changes1 (cls/generate-update-shapes + (pcb/empty-changes nil (:id page)) + #{(:id main-root)} + (fn [shape objects] + (let [children (cfh/get-immediate-children objects (:id shape)) + shape (merge shape + {:layout :flex + :layout-flex-dir :row + :layout-gap-type :multiple + :layout-gap {:row-gap 0 :column-gap 0} + :layout-align-items :start + :layout-justify-content :start + :layout-align-content :stretch + :layout-wrap-type :nowrap + :layout-padding-type :simple + :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}})] + (merge shape (flex/calculate-params objects children shape)))) + (:objects page) + {:with-objects? true}) + + updated-file (thf/apply-changes file changes1) + updated-main-root (ths/get-shape updated-file :main-root) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root)] + + ;; ==== Check + (t/is (ctl/flex-layout? copy-root')) + (t/is (= (:layout-flex-dir updated-main-root) + (:layout-flex-dir copy-root'))) + (t/is (= (:layout-gap updated-main-root) + (:layout-gap copy-root'))))) + (t/deftest test-sync-when-changing-upper (let [;; ==== Setup file (-> (thf/sample-file :file1)