diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 815021ae3b..215fe4ffcf 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -53,13 +53,13 @@ ;; We don't do automatic update of the `layout-grid-cells` property. (remove #(= :layout-grid-cells %)))) -;; Flex-child layout attrs synced between main and copy (interactions are only kept on swap) -(def ^:private flex-child-attrs +;; Layout child attrs synced between main and copy (interactions are only kept on swap) +(def ^:private layout-child-attrs (disj ctk/swap-keep-attrs :interactions)) ;; Attributes that place a shape in its parent (def ^:private placement-attrs - (conj flex-child-attrs :constraints-h :constraints-v :fixed-scroll)) + (conj layout-child-attrs :constraints-h :constraints-v :fixed-scroll)) ;; Touched groups of the placement attributes and of the position (def placement-groups @@ -98,6 +98,7 @@ (declare mark-local-changes) (declare compare-children) +(declare reorder-copy-children) (declare add-shape-to-instance) (declare add-shape-to-main) (declare remove-shape) @@ -107,7 +108,7 @@ (declare update-attrs) (declare update-grid-main-attrs) (declare update-grid-copy-attrs) -(declare update-flex-children) +(declare update-layout-children) (declare update-swapped-placement) (declare orient-like) (declare reposition-shape) @@ -281,8 +282,7 @@ :library library :root-inst shape-inst :root-main shape-main - :reset? reset? - :remote-synced (when-not (:component-root shape-inst) true)} + :reset? reset?} shape-inst shape-main)) @@ -293,19 +293,21 @@ (defn- sync-copy-shape "Sync the copy shape `shape-inst` from `shape-main`, the main shape it - pairs with. A reset also clears its overrides and sets its - `:remote-synced`." - [changes {:keys [container main-container library component root-inst root-main reset? remote-synced]} + pairs with. A reset also clears its overrides and its `:remote-synced`, + as it follows its near main." + [changes {:keys [container main-container root-inst root-main reset?]} shape-inst shape-main] (let [omit-touched? (not reset?)] (cond-> (update-attrs changes shape-inst shape-main root-inst root-main container omit-touched?) - (ctsl/flex-layout? shape-main) - (update-flex-children container shape-inst shape-main - #(ctn/get-shape main-container (:shape-ref %)) omit-touched?) + ;; the children refer to the children of `shape-main`, in the + ;; container of the walk's main shapes + (ctsl/any-layout? shape-main) + (update-layout-children container shape-inst shape-main + #(ctn/get-shape main-container (:shape-ref %)) omit-touched?) reset? (-> (set-touched shape-inst container nil) - (change-remote-synced shape-inst container remote-synced))))) + (change-remote-synced shape-inst container nil))))) (defn- sync-copy-recursive "Sync the copy shape `shape-inst` from `shape-main`, the main shape it @@ -315,8 +317,7 @@ overrides and clears them. `ctx` holds what stays the same along the walk: the copy's `:container`, the `:main-container` of the main shapes, the `:component` and its `:library`, the roots `:root-inst` and - `:root-main`, `:reset?`, and the `:remote-synced` a reset gives the copy - shapes (true when it starts below the copy root)." + `:root-main`, and `:reset?`." [changes {:keys [container main-container library component reset?] :as ctx} shape-inst shape-main] (shape-log :debug (:id shape-inst) container :msg "Sync shape direct recursive" @@ -341,13 +342,8 @@ (remove-shape changes child-inst container)) :only-main (fn [changes child-main] (add-shape-to-instance changes ctx shape-inst child-main - (d/index-of children-main child-main))) - :moved (fn [changes child-inst child-main] - (move-shape changes - child-inst - (d/index-of children-inst child-inst) - (d/index-of children-main child-main) - container))})) + (d/index-of children-main child-main)))}) + (reorder-copy-children shape-inst children-main container)) (ctsl/grid-layout? shape-inst) (update-grid-copy-attrs (:id shape-inst) shape-main library component (not reset?)))))) @@ -414,12 +410,9 @@ (set-touched shape-main main-container (when-not (:remote-synced shape-inst) (set/union (:touched shape-main) (:touched shape-inst))))) - (ctsl/flex-layout? shape-main) - (update-flex-children main-container shape-main shape-inst - #(ctf/get-shape-in-copy container % shape-inst) false) - - (ctsl/grid-layout? shape-main) - (update-grid-main-attrs shape-main shape-inst main-container container) + (ctsl/any-layout? shape-main) + (update-layout-children main-container shape-main shape-inst + #(ctf/get-shape-in-copy container % shape-inst) false) :always (change-remote-synced shape-inst container remote-synced))) @@ -441,30 +434,35 @@ ;; This should not occur, but protect against it in any case changes (let [children-inst (vec (ctn/get-direct-children container shape-inst)) - children-main (vec (ctn/get-direct-children main-container shape-main))] + children-main (vec (ctn/get-direct-children main-container shape-main)) + ;; the main shapes added for copy children, by copy child id + added (volatile! {})] (vswap! synced-ids conj (:id shape-inst)) - (-> changes - (update-main-shape ctx shape-inst shape-main) - (compare-children - children-inst - children-main - {:pair? (fn [child-main child-inst] - (and (ctk/is-main-of? child-main child-inst) - (not (ctk/swapped? child-inst)))) - :on-pair (fn [changes child-inst child-main] - (update-main-recursive changes ctx child-inst child-main)) - :only-inst (fn [changes child-inst] - (add-shape-to-main changes ctx shape-main child-inst - (d/index-of children-inst child-inst))) - :only-main (fn [changes child-main] - (remove-shape changes child-main main-container)) - :moved (fn [changes child-inst child-main] - (move-shape changes - child-main - (d/index-of children-main child-main) - (d/index-of children-inst child-inst) - main-container)) - :inverse? true}))))) + (cond-> (-> changes + (update-main-shape ctx shape-inst shape-main) + (compare-children + children-inst + children-main + {:pair? (fn [child-main child-inst] + (and (ctk/is-main-of? child-main child-inst) + (not (ctk/swapped? child-inst)))) + :on-pair (fn [changes child-inst child-main] + (update-main-recursive changes ctx child-inst child-main)) + :only-inst (fn [changes child-inst] + (add-shape-to-main changes (assoc ctx :added added) shape-main child-inst + (d/index-of children-inst child-inst))) + :only-main (fn [changes child-main] + (remove-shape changes child-main main-container)) + :moved (fn [changes child-inst child-main] + (move-shape changes + child-main + (d/index-of children-main child-main) + (d/index-of children-inst child-inst) + main-container)) + :inverse? true})) + + (ctsl/grid-layout? shape-main) + (update-grid-main-attrs shape-main shape-inst children-inst @added main-container))))) (defn- mark-local-changes "Mark as `:local-change?` the changes of the copy shapes `ids`. The inverse @@ -497,9 +495,9 @@ - `on-pair` for a copy child and the main child it pairs with (`(pair? child-main child-inst)`); - `only-inst` and `only-main` for a child that pairs with none; - - `moved`, after `on-pair`, for a pair found out of place. The main's - order wins, so the copy child is looked up; with `inverse?` the - copy's order wins, and the main child is looked up. + - `moved` (optional), after `on-pair`, for a pair found out of place. + The main's order wins, so the copy child is looked up; with + `inverse?` the copy's order wins, and the main child is looked up. The children are walked in order, side by side. A child out of place is looked up by its reference (`index-positions`), not searched, so the walk @@ -576,15 +574,13 @@ inverse? (recur (inc i) j taken-insts (conj taken-mains (:id child-main')) - (-> changes - (on-pair child-inst child-main') - (moved child-inst child-main'))) + (cond-> (on-pair changes child-inst child-main') + moved (moved child-inst child-main'))) :else (recur i (inc j) (conj taken-insts (:id child-inst')) taken-mains - (-> changes - (on-pair child-inst' child-main) - (moved child-inst' child-main)))))))))) + (cond-> (on-pair changes child-inst' child-main) + moved (moved child-inst' child-main)))))))))) (defn- shape-label "The container and the shape, for the log." @@ -616,22 +612,18 @@ (defn- add-shape-to-instance "Add to the copy shape `parent-inst`, at `index`, a copy of `child-main`, a child of the main shape it pairs with." - [changes {:keys [container main-container root-inst root-main reset? remote-synced]} + [changes {:keys [container main-container root-inst root-main]} parent-inst child-main index] (shape-log :info (:id child-main) main-container :msg (str "ADD " (shape-label child-main container))) (let [all-parents (into [(:id parent-inst)] (cfh/get-parent-ids (:objects container) (:id parent-inst))) + ;; a new shape is synced with its main shape update-new-shape (fn [new-shape original-shape] - (cond-> (reposition-shape new-shape root-main root-inst) - (and reset? remote-synced) - (assoc :remote-synced true) - - ;; a new shape is synced with its main shape - :always - (-> (assoc :shape-ref (:id original-shape)) - (dissoc :touched)))) + (-> (reposition-shape new-shape root-main root-inst) + (assoc :shape-ref (:id original-shape)) + (dissoc :touched))) [_ new-shapes _] (ctst/clone-shape child-main @@ -649,7 +641,7 @@ "Add to the main shape `parent-main`, at `index`, a copy of `child-inst`, a child of the copy shape that pairs with it; `child-inst` then refers to the new main shape." - [changes {:keys [container main-container component root-inst root-main libraries]} + [changes {:keys [container main-container root-inst root-main libraries added synced-ids]} parent-main child-inst index] (shape-log :info (:id child-inst) container :msg (str "ADD " (shape-label child-inst main-container))) @@ -688,6 +680,10 @@ (:id parent-main) (:frame-id parent-main))) + _ (some-> added (vswap! assoc (:id child-inst) (:id new-head))) + ;; the copy shapes it relinks are changes of the copy's file + _ (some-> synced-ids (vswap! into (map :id) updated-shapes)) + new-shapes (if swap-in-copy? (let [objects (d/index-by :id new-shapes) @@ -707,19 +703,17 @@ [:component-id :component-file :component-root :shape-ref :touched])})] (-> changes + ;; each undo goes with its redo, and the undo list runs backwards: + ;; the new shapes are deleted, children first, then the parents refit (pcb/add-changes main-container - (map #(assoc (add-obj-change % index) :frame-id (:frame-id %)) new-shapes) - []) - ;; F102: this targets the component's own objects, which only a - ;; deleted component has, not the page that holds its main - (update :redo-changes conj {:type :reg-objects - :component-id (:id component) - :shapes all-parents}) + (conj (mapv #(assoc (add-obj-change % index) :frame-id (:frame-id %)) new-shapes) + {:type :reg-objects :shapes all-parents}) + (into [{:type :reg-objects :shapes all-parents}] + (map (comp del-obj-change :id)) + new-shapes)) (pcb/add-changes container (map set-link updated-shapes) - (concat (map #(set-link (ctn/get-shape container (:id %))) updated-shapes) - ;; F102: the new shapes are in the main's container - (map (comp del-obj-change :id) new-shapes)))))) + (map #(set-link (ctn/get-shape container (:id %))) updated-shapes))))) (defn- remove-shape [changes shape container] @@ -735,6 +729,63 @@ (map #(add-obj-change (get objects %) (cfh/get-position-on-parent objects %)) ids)))) +(defn- longest-increasing + "The set of the elements of `xs` that form the longest run, not + necessarily contiguous, whose `key-fn` values increase." + [key-fn xs] + (let [xs (vec xs) + ;; tails[l]: position of the smallest last key of a run of length l+1 + [tails prev] + (reduce (fn [[tails prev] i] + (let [k (key-fn (nth xs i)) + l (loop [lo 0 hi (count tails)] + (if (< lo hi) + (let [mid (quot (+ lo hi) 2)] + (if (< (key-fn (nth xs (nth tails mid))) k) + (recur (inc mid) hi) + (recur lo mid))) + lo))] + [(assoc tails l i) + (assoc prev i (when (pos? l) (nth tails (dec l))))])) + [[] {}] + (range (count xs)))] + (loop [i (peek tails) + run #{}] + (if (some? i) + (recur (get prev i) (conj run (nth xs i))) + run)))) + +(defn- reorder-copy-children + "Move the children of the copy shape `shape-inst`, as the changes so far + left them, into the order of their main shapes among `children-main`, + moving the fewest: the longest run already in order stays, and each other + child goes right after the one before it in the main's order." + [changes shape-inst children-main container] + (let [main-index (into {} (map-indexed (fn [i child] [(:id child) i])) children-main) + objects (pcb/get-objects changes) + current (vec (get-in objects [(:id shape-inst) :shapes])) + index-of #(get main-index (get-in objects [% :shape-ref]) ##Inf) + target (vec (sort-by index-of current))] + (if (= current target) + changes + (let [in-place (longest-increasing index-of current)] + (first + (reduce (fn [[changes order] [k id]] + (if (contains? in-place id) + [changes order] + (let [before (when (pos? k) (nth target (dec k))) + after (fn [order neighbour] + (if (some? neighbour) (inc (d/index-of order neighbour)) 0)) + from (d/index-of order id) + order' (d/insert-at-index order (after order before) [id])] + [(move-shape changes (get objects id) + (after order' (when (pos? from) (nth order (dec from)))) + (after order before) + container) + order']))) + [changes current] + (map-indexed vector target))))))) + (defn- move-shape [changes shape index-before index-after container] (shape-log :info (:id shape) container @@ -809,7 +860,9 @@ :ignore-touched true}]})] (if (= dest-tokens dest-tokens') changes - (pcb/add-changes changes container [(set-tokens dest-tokens')] [(set-tokens dest-tokens)])))) + (pcb/add-changes changes container + [(set-tokens dest-tokens')] + [(set-tokens (:applied-tokens dest-shape))])))) ;; The token attributes of each attribute whose tokens sync; the ;; `layout-grid-cells` are not synced automatically. @@ -1107,12 +1160,13 @@ (and (ctsl/any-layout-immediate-child? objects shape-inst) (not (ctsl/position-absolute? shape-inst)))))) -(defn- update-flex-children +(defn- update-layout-children "Copy into `dest`, a shape of `container`, the layout attributes of the - flex shape `src` it pairs with: its sizing, when `src` hugs or fixes it, - and the flex child attributes of each of its children, from the child of - `src` it pairs with (`src-child`). With `omit-touched?`, an attribute - whose group is touched is left as it is." + flex or grid shape `src` it pairs with: its sizing, when `src` hugs or + fixes it, and the layout child attributes of each of its children, from + the child of `src` it pairs with (`src-child`); a child with none is left + as it is. With `omit-touched?`, an attribute whose group is touched is + left as it is." [changes container dest src src-child omit-touched?] (pcb/concat-changes changes @@ -1131,10 +1185,9 @@ {:ignore-touched true}) (pcb/update-shapes (:shapes dest) - (fn [child] - (if-let [source (src-child child)] - (propagate-attrs child source flex-child-attrs omit-touched?) - child)) + #(if-let [src-child (src-child %)] + (propagate-attrs % src-child layout-child-attrs omit-touched?) + %) {:ignore-touched true})))) (defn- update-grid-copy-attrs @@ -1172,17 +1225,17 @@ {:ignore-touched true :with-objects? true}))) (defn- update-grid-main-attrs - "Synchronizes the `layout-grid-cells` property from the copy to the main shape" - [changes shape-main shape-copy main-container copy-container] + "Synchronizes the `layout-grid-cells` property from the copy to the main + shape, once the update main has paired `children-copy`, the copy's + children, with the main's: each with the main shape `added` for it, or + else with the one it refers to." + [changes shape-main shape-copy children-copy added main-container] (let [ids-map (into {} - (comp - (map #(dm/get-in main-container [:objects %])) - (keep - (fn [main-shape] - (let [copy-shape (ctf/get-shape-in-copy copy-container main-shape shape-copy)] - [(:id copy-shape) (:id main-shape)])))) - (:shapes shape-main)) + (keep (fn [child] + (when-let [main-id (get added (:id child) (:shape-ref child))] + [(:id child) main-id]))) + children-copy) new-changes (-> (pcb/empty-changes) diff --git a/common/src/app/common/logic/library_assets.cljc b/common/src/app/common/logic/library_assets.cljc index c90b48b283..d2616bdafd 100644 --- a/common/src/app/common/logic/library_assets.cljc +++ b/common/src/app/common/logic/library_assets.cljc @@ -27,7 +27,8 @@ [changes shape container update-node] (let [old-content (:content shape) new-content (txt/transform-nodes update-node old-content) - set-content (fn [content] + ;; the new content is measured again + set-content (fn [content position-data] {:type :mod-obj :id (:id shape) :operations [{:type :set @@ -36,10 +37,12 @@ :ignore-touched true} {:type :set :attr :position-data - :val nil}]})] + :val position-data}]})] (if (= new-content old-content) changes - (pcb/add-changes changes container [(set-content new-content)] [(set-content old-content)])))) + (pcb/add-changes changes container + [(set-content new-content nil)] + [(set-content old-content (:position-data shape))])))) (defn generate-sync-typographies "Copy into the text `shape` the text styles of each typography of the diff --git a/common/src/app/common/test_helpers/composable/comp/cases.cljc b/common/src/app/common/test_helpers/composable/comp/cases.cljc index b60af59ac3..c75c8c5f8b 100644 --- a/common/src/app/common/test_helpers/composable/comp/cases.cljc +++ b/common/src/app/common/test_helpers/composable/comp/cases.cljc @@ -179,11 +179,6 @@ (let [move (n/move-child :main-child1 :main-root 2)] {:setup setup/component-with-many-children :operation (tm/in-sequence [move]) - ;; the undo of the sync's move leaves the copy children reordered - :undo-check {:known-failures [{:bug "F41" - :fails {:undo #{[:object :copy-root :shapes]} - :variant-undo #{[:object :copy-root :shapes]}} - :op move}]} :asserter (fn [situation] (let [copy-root (setup/copy-root situation) order (vec (:shapes copy-root)) @@ -956,11 +951,6 @@ [] (let [move (n/move-child :sub1 :row-root 3)] {:setup setup/component-with-sibling-instances - ;; the undo of the sync's move leaves the copy children reordered - :undo-check {:known-failures [{:bug "F41" - :fails {:undo #{[:object :row-copy :shapes]} - :variant-undo #{[:object :row-copy :shapes]}} - :op move}]} :operation (tm/in-sequence [move]) :asserter (fn [s] (t/is (= [] (setup/misaligned-sub-heads s))))})) @@ -1032,20 +1022,10 @@ ;; the main's third column takes the copy child's component in-col-3 (mapv :component-id (get placement 3)) expected [(:component-id copy-child)]] - (if (tm/applied? s swap) - ;; the cells keep the id of the main child update main - ;; replaced, which is gone, and the new main child is in - ;; no cell - (tm/known-failure "F49" - [{:stray #{} :uncelled 0} expected] - [{:stray #{nil} :uncelled 1} [nil]] - [cells in-col-3]) - (do - (t/is (= {:stray #{} :uncelled 0} cells)) - (t/is (= expected in-col-3)) - ;; both main children instantiate the same component - (t/is (= [(:shape-ref copy-child)] (mapv :id (get placement 3)))) - (t/is (empty? (get placement 1))))) + (t/is (= {:stray #{} :uncelled 0} cells)) + (t/is (= expected in-col-3)) + (t/is (= [(:shape-ref copy-child)] (mapv :id (get placement 3)))) + (t/is (empty? (get placement 1))) (t/is (empty? (:touched (tm/role-shape s :copy-root))))))})) (defn update-main-renames-the-component @@ -1143,12 +1123,6 @@ [] (let [drop (n/nest-copy :loose :main-root :cell [1 3])] {:setup setup/grid-component-with-copy-and-loose-item - ;; the drop reorders the main's children; the undo of the sync's reorder - ;; leaves the copy's children reordered - :undo-check {:known-failures [{:bug "F41" - :fails {:undo #{[:object :grid-copy :shapes]} - :variant-undo #{[:object :grid-copy :shapes]}} - :op drop}]} :operation (tm/in-sequence [drop]) :asserter (fn [s] (let [main (tm/role-shape s :main-root) @@ -1635,13 +1609,6 @@ removal (n/unapply-token (n/main-rect-of m) "radius.l") rect (fn [s target] (tm/shape-by-id s (target s)))] {:setup setup/empty-situation - ;; the undo of the sync gives the copy rect, which had no tokens, an - ;; empty `:applied-tokens` map - :undo-check {:known-failures [{:bug "F55" - :fails {:undo #{[:object :unlabelled :applied-tokens]} - :variant-undo #{[:object :unlabelled :applied-tokens]}} - :op applied - :when #(not (tm/applied? % own))}]} :operation (tm/in-sequence [(n/create-component m base-color :size 100) (n/add-tokens [["radius.s" 10] ["radius.l" 30]]) diff --git a/common/src/app/common/test_helpers/composable/comp/setups.cljc b/common/src/app/common/test_helpers/composable/comp/setups.cljc index c10b93c643..78eccd3804 100644 --- a/common/src/app/common/test_helpers/composable/comp/setups.cljc +++ b/common/src/app/common/test_helpers/composable/comp/setups.cljc @@ -203,6 +203,7 @@ (tho/add-frame :grouped-root :width 200 :height 200) (tho/add-group :group :parent-label :grouped-root) (thc/instantiate-component :item :child1 :parent-label :group) + (tho/fit-group :group) (thc/make-component :grouped :grouped-root) (thc/instantiate-component :grouped :grouped-copy))] (label-copy-children! file :grouped-copy {:group :copy-group}) diff --git a/common/src/app/common/test_helpers/compositions.cljc b/common/src/app/common/test_helpers/compositions.cljc index 6012a24186..2f93466a79 100644 --- a/common/src/app/common/test_helpers/compositions.cljc +++ b/common/src/app/common/test_helpers/compositions.cljc @@ -61,6 +61,14 @@ :name "Group1"} params))) +(defn fit-group + "Fit the group `group-label` to its children, as the editor does once + they are in it." + [file group-label] + (thf/apply-changes file {:redo-changes [{:type :reg-objects + :page-id (:id (thf/current-page file)) + :shapes [(thi/id group-label)]}]})) + (defn add-frame-with-child [file frame-label child-label & {:keys [frame-params child-params]}] ;; Generated shape tree: diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 0e82eff410..a161e1f561 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -76,6 +76,8 @@ :width :geometry-group :height :geometry-group :rotation :geometry-group + :flip-x :geometry-group + :flip-y :geometry-group :transform :geometry-group :transform-inverse :geometry-group :opacity :layer-effects-group diff --git a/common/test/common_tests/logic/comp_reset_test.cljc b/common/test/common_tests/logic/comp_reset_test.cljc index 6df4ca8f47..605839f31c 100644 --- a/common/test/common_tests/logic/comp_reset_test.cljc +++ b/common/test/common_tests/logic/comp_reset_test.cljc @@ -498,4 +498,77 @@ ;; ==== Check ;; Without propagation the copy still reflects the overridden color - (t/is (= copy2-bottom-color "#fabada")))) \ No newline at end of file + (t/is (= copy2-bottom-color "#fabada")))) + +(defn- margin-of + [file label] + (:layout-item-margin (ths/get-shape file label))) + +(defn- nested-layout-board + "component1, a board with a `layout` whose child has the margin `margin`; + component2, which nests a copy of it; a copy of component2." + [layout margin] + (let [child {:parent-label :main1-root + :layout-item-margin margin + :layout-item-margin-type :simple}] + (-> (thf/sample-file :file1) + (cond-> (= layout :flex) + (-> (tho/add-frame :main1-root {:layout :flex :layout-flex-dir :row}) + (tho/add-rect :main1-child child))) + (cond-> (= layout :grid) + (-> (tho/add-grid-frame :main1-root) + (tho/add-rect :main1-child child) + (tho/place-in-grid-columns :main1-root 1 {1 :main1-child}))) + (thc/make-component :component1 :main1-root) + (tho/add-frame :main2-root) + (thc/instantiate-component :component1 :nested-head :parent-label :main2-root) + (thc/make-component :component2 :main2-root) + (thc/instantiate-component :component2 :copy2-root + :children-labels [:copy-nested-head :copy-nested-child])))) + +(t/deftest test-reset-of-a-nested-copy-of-a-layout-board + ;; a copy of component2 resets its nested copy of component1 + (doseq [layout [:flex :grid]] + (let [margin {:m1 7 :m2 7 :m3 7 :m4 7} + file (nested-layout-board layout margin) + page (thf/current-page file) + changes (cll/generate-reset-component (pcb/empty-changes) + file + {(:id file) file} + (ctn/make-container page :page) + (thi/id :copy-nested-head)) + file' (thf/apply-changes file changes)] + (t/testing (name layout) + (t/is (= margin (margin-of file :copy-nested-child))) + (t/is (= margin (margin-of file' :copy-nested-child))))))) + +(t/deftest test-update-main-after-a-reset-of-a-nested-copy-keeps-the-main-overrides + ;; component2's main overrides the fill of its nested copy of component1; + ;; a copy of component2 resets its nested copy, then pushes to the main + (let [file (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root + :children-labels [:copy-nested-head :copy-nested-child])) + page (thf/current-page file) + nested (first (:shapes (ths/get-shape file :nested-head))) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{nested} + #(assoc % :fills (ths/sample-fills-color :fill-color "#ff0000")) + (:objects page) + {}) + file (thf/apply-changes file changes) + page (thf/current-page file) + reset (cll/generate-reset-component (pcb/empty-changes) file {(:id file) file} + (ctn/make-container page :page) + (thi/id :copy-nested-head)) + file (thf/apply-changes file reset) + page (thf/current-page file) + container (ctn/make-container page :page) + update (-> (pcb/empty-changes) + (pcb/with-container container) + (cll/generate-sync-shape-inverse {(:id file) file} container (thi/id :copy2-root))) + file' (thf/apply-changes file update)] + (t/is (= #{:fill-group} (:touched (ths/get-shape-by-id file nested)))) + (t/is (= #{:fill-group} (:touched (ths/get-shape-by-id file' nested)))))) + diff --git a/common/test/common_tests/logic/comp_sync_test.cljc b/common/test/common_tests/logic/comp_sync_test.cljc index 4da84286c9..5d20980ce1 100644 --- a/common/test/common_tests/logic/comp_sync_test.cljc +++ b/common/test/common_tests/logic/comp_sync_test.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.files.changes :as ch] [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.logic.libraries :as cll] @@ -21,6 +22,7 @@ [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.modifiers :as ctm] [app.common.types.shape-tree :as ctst] [clojure.test :as t])) @@ -335,6 +337,56 @@ (t/is (= (first (:shapes copy-root')) (:id copy-child2'))) (t/is (= (nth (:shapes copy-root') 2) (:id copy-child3'))))) +(defn- reorder-main-and-sync + "A main with six children, and its copy; the main moves the children + `ids` to `to-index`, then syncs. Returns `[file-before file-after + sync-changes]`." + [labels to-index] + (let [main-labels [:m1 :m2 :m3 :m4 :m5 :m6] + file (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy + :component1 :main-root main-labels :copy-root + :copy-root-params {:children-labels [:c1 :c2 :c3 :c4 :c5 :c6]})) + page (thf/current-page file) + relocate (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) + (thi/id :main-root) + to-index + (into #{} (map thi/id) labels)) + moved (thf/apply-changes file relocate) + sync (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id moved) + (thi/id :component1) + (:id moved) + {(:id moved) moved} + (:id moved))] + [moved (thf/apply-changes moved sync) sync])) + +(defn- copy-order + [file] + (mapv #(:shape-ref (ths/get-shape-by-id file %)) (:shapes (ths/get-shape file :copy-root)))) + +(defn- moves + [changes] + (count (filter #(= :mov-objects (:type %)) (:redo-changes changes)))) + +(t/deftest test-sync-of-a-reorder-moves-each-displaced-child-once + (doseq [[labels to-index displaced] [[[:m1] 6 1] ; first to the end + [[:m6] 0 1] ; last to the start + [[:m2] 5 1] ; one to a later place + [[:m5] 1 1] ; one to an earlier place + [[:m1 :m2] 6 2]]] + (let [[moved synced sync] (reorder-main-and-sync labels to-index) + main-order (:shapes (ths/get-shape moved :main-root))] + (t/testing (str labels " to " to-index) + (t/is (= main-order (copy-order synced))) + (t/is (= displaced (moves sync))) + (t/testing "undo puts the copy's children back" + (t/is (= (copy-order moved) (copy-order (thf/apply-undo-changes synced sync))))))))) + (t/deftest test-sync-when-changing-upper (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -583,6 +635,44 @@ (t/is (true? (:hidden main-child2'))) (t/is (= [(thi/id :main-child2) (thi/id :main-child1)] (:shapes main-root'))))) +(t/deftest test-update-main-of-a-swap-into-a-main-on-another-page + ;; component2's main, on its own page, holds a copy of component1 inside a + ;; group; its copy, on another page, swaps that copy for a bigger component3 + ;; and pushes it to the main + (let [file (-> (thf/sample-file :file1 :page-label :main-page) + (tho/add-simple-component :c1 :main1 :main1-child + :root-params {:width 10 :height 10} + :child-params {:width 10 :height 10}) + (tho/add-simple-component :c3 :main3 :main3-child + :root-params {:width 80 :height 80} + :child-params {:width 80 :height 80}) + (tho/add-frame :main2 {:width 200 :height 200}) + (tho/add-group :group :parent-label :main2) + (thc/instantiate-component :c1 :nested :parent-label :group) + (tho/fit-group :group) + (thc/make-component :c2 :main2) + (thf/add-sample-page :copy-page) + (thc/instantiate-component :c2 :copy2 :children-labels [:copy-group :copy-nested]) + (tho/swap-component-in-shape :copy-nested :c3 {:page-label :copy-page + :new-shape-label :swapped})) + page (thf/current-page file) + container (ctn/make-container page :page) + changes (-> (pcb/empty-changes) + (pcb/with-container container) + (cll/generate-sync-shape-inverse {(:id file) file} container (thi/id :copy2))) + file' (thf/apply-changes file changes) + group' (ths/get-shape file' :group :page-label :main-page) + pushed (ths/get-shape-by-id file' (first (:shapes group')) :page-label :main-page)] + (t/is (= (thi/id :c3) (:component-id pushed))) + (t/testing "the group takes the size of the pushed copy" + (t/is (== 80 (-> group' :selrect :width) (-> group' :selrect :height)))) + (t/testing "undo gives the main back as it was" + (let [file'' (thf/apply-undo-changes file' changes)] + (t/is (= (:shapes (ths/get-shape file :group :page-label :main-page)) + (:shapes (ths/get-shape file'' :group :page-label :main-page)))) + (t/is (= (count (:objects (thf/get-page file :main-page))) + (count (:objects (thf/get-page file'' :main-page))))))))) + (t/deftest test-an-assign-on-a-main-changes-its-component (let [file (-> (thf/sample-file :file1) (tho/add-simple-component-with-copy :component1 :main-root :main-child :copy-root)) @@ -598,6 +688,62 @@ (ch/components-changed (:data file) (change {:type :assign :value {:opacity 0.5}})))) (t/is (nil? (ch/components-changed (:data file) (change {:type :assign :value {:interactions []}})))))) +(t/deftest test-sync-of-a-flip + ;; the main flips its child as the editor does: a transform that scales + ;; it by -1, which toggles :flip-x + (let [file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 :main-root :main-child :copy-root + :copy-root-params {:children-labels [:copy-child]})) + page (thf/current-page file) + flip (fn [shape] + (gsh/transform-shape shape (ctm/resize-modifiers (gpt/point -1 1) (gsh/shape->center shape)))) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(thi/id :main-child)} + flip + (:objects page) + {}) + flipped (thf/apply-changes file changes) + sync (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id flipped) + (thi/id :component1) + (:id flipped) + {(:id flipped) flipped} + (:id flipped)) + file' (thf/apply-changes flipped sync)] + (t/is (true? (:flip-x (ths/get-shape flipped :main-child)))) + (t/is (true? (:flip-x (ths/get-shape file' :copy-child)))) + (t/is (= (:transform (ths/get-shape file' :main-child)) + (:transform (ths/get-shape file' :copy-child)))) + (t/is (nil? (:touched (ths/get-shape file' :copy-child)))))) + +(t/deftest test-sync-of-a-grid-child-margin + (let [file (-> (thf/sample-file :file1) + (tho/add-grid-frame :main-root) + (tho/add-rect :main-child {:parent-label :main-root}) + (tho/place-in-grid-columns :main-root 1 {1 :main-child}) + (thc/make-component :component1 :main-root) + (thc/instantiate-component :component1 :copy-root :children-labels [:copy-child])) + page (thf/current-page file) + margin {:m1 9 :m2 9 :m3 9 :m4 9} + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(thi/id :main-child)} + #(assoc % :layout-item-margin margin :layout-item-margin-type :simple) + (:objects page) + {}) + edited (thf/apply-changes file changes) + sync (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id edited) + (thi/id :component1) + (:id edited) + {(:id edited) edited} + (:id edited)) + file' (thf/apply-changes edited sync)] + (t/is (= margin (:layout-item-margin (ths/get-shape file' :copy-child)))))) + (t/deftest test-which-changes-of-a-main-mark-its-component-modified (let [file (-> (thf/sample-file :file1) (tho/add-simple-component :component1 :main-root :main-child) @@ -619,3 +765,62 @@ (t/is (= :old (apply-op [pos-data])))) (t/testing "any synced attribute of the change counts, not only the last" (t/is (not= :old (apply-op [fills pos-data])))))) + +(t/deftest test-an-edit-of-a-nested-component-keeps-the-placement-of-its-copies + ;; a copy of button dropped into the card main, which then sets it to + ;; stick right (an override of the nested copy); an edit of the button + ;; main keeps it, in the card main and in its copy + (let [sync (fn [f component] + (thf/apply-changes f (cll/generate-sync-file-changes (pcb/empty-changes) nil :components + (:id f) (thi/id component) (:id f) + {(:id f) f} (:id f)))) + file (-> (thf/sample-file :file1) + (tho/add-simple-component :button :button-main :button-rect) + (tho/add-frame :card-main {:width 300 :height 300}) + (thc/make-component :card :card-main) + (thc/instantiate-component :card :card-copy) + (thc/instantiate-component :button :nested-button)) + page (thf/current-page file) + file (-> file + (thf/apply-changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) + (thi/id :card-main) 0 #{(thi/id :nested-button)})) + (ths/update-shape :nested-button :constraints-h :right) + (sync :card)) + page (thf/current-page file) + edit (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) #{(thi/id :button-rect)} + #(assoc % :opacity 0.5) (:objects page) {}) + file' (-> (thf/apply-changes file edit) (sync :button) (sync :card)) + copy #(ths/get-shape-by-id % (first (:shapes (ths/get-shape % :card-copy)))) + placing (juxt :constraints-h :constraints-v)] + (t/is (= [:right :top] (placing (copy file)))) + (t/is (= [:right :top] (placing (ths/get-shape file' :nested-button)))) + (t/is (= [:right :top] (placing (copy file')))) + (t/is (= 0.5 (:opacity (ths/get-shape-by-id file' (first (:shapes (copy file'))))))))) + +(t/deftest test-update-main-of-a-library-copy-files-the-copy-changes-as-local + ;; a copy of a library component swaps its nested copy and pushes it to + ;; the library main: the copy's own changes stay in the file + (let [library (-> (thf/sample-file :library :is-shared true :page-label :lib-page) + (tho/add-nested-component :c1 :main1 :main1-child :c2 :main2 :nested-head) + (tho/add-simple-component :c3 :main3 :main3-child)) + file (-> (thf/sample-file :file1 :page-label :page1) + (thc/instantiate-component :c2 :copy2 :library library + :children-labels [:copy-nested])) + file (tho/swap-component-in-shape file :copy-nested :c3 {:library library + :new-shape-label :swapped}) + page (thf/current-page file) + cont (ctn/make-container page :page) + changes (-> (pcb/empty-changes) + (pcb/with-container cont) + (cll/generate-sync-shape-inverse {(:id file) file (:id library) library} + cont (thi/id :copy2))) + [local _] (cll/split-local-changes changes) + objects (:objects page) + in-copy (into #{} (cfh/get-children-ids-with-self objects (thi/id :copy2)))] + (t/is (contains? (into #{} (map :id) (:redo-changes local)) (thi/id :swapped))) + (t/testing "every change of a shape of the copy is local" + (t/is (every? #(not (contains? in-copy (:id %))) + (:redo-changes (second (cll/split-local-changes changes)))))))) + diff --git a/common/test/common_tests/logic/golden_master.edn b/common/test/common_tests/logic/golden_master.edn index a36801b4c3..0c788be712 100644 --- a/common/test/common_tests/logic/golden_master.edn +++ b/common/test/common_tests/logic/golden_master.edn @@ -5,7 +5,7 @@ {:transcript "create-component {:component \"main\", :id sync-main-component}\n create-component {:component \"s1\", :id sync-s1-component}\n create-component {:component \"s2\", :id sync-s2-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n swap-shape {:target #fn, :to \"s1\", :keep-touched? false}\n one-of -> skip\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["9019918a02196b8f" "c4e06e2eab16398f" "9b149917696880e1"]} {:transcript "create-component {:component \"main\", :id sync-main-component}\n create-component {:component \"s1\", :id sync-s1-component}\n create-component {:component \"s2\", :id sync-s2-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n swap-shape {:target #fn, :to \"s1\", :keep-touched? false}\n one-of -> skip\n one-of -> skip", :states ["9019918a02196b8f" "b5373f3c8cae4fc1"]}] :ab - [{:transcript "create-component {:component \"main\", :id sync-main-component}\n create-component {:component \"s1\", :id sync-s1-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :opacity, :value 0.5}\n swap-shape {:target #fn, :to \"s1\", :keep-touched? false}\n reset-overrides {:target #fn, :reset 3}", :states ["2ec7fb83e8335e83" "6a105b37f6ebf73c" "8ec10656f376e332" "4ba9485ff33b0f2c"]}] + [{:transcript "create-component {:component \"main\", :id sync-main-component}\n create-component {:component \"s1\", :id sync-s1-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :opacity, :value 0.5}\n swap-shape {:target #fn, :to \"s1\", :keep-touched? false}\n reset-overrides {:target #fn, :reset 3}", :states ["2ec7fb83e8335e83" "6a105b37f6ebf73c" "2ec7fb83e8335e83" "1ed2373b434016f9"]}] :ac [{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-variant-container {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 3}\n make-nested-component-with-variant {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :opacity, :value 0.5}\n switch-variant {:target-component sync-vset-v1-component}\n test\n switch-variant {:target-component sync-vset-v2-component}", :states ["9f9ac61ce3c54295" "5bd9620c6fc0cf6e" "387d74ba1c75e52f" "62a55a068ebd6043"]}] :ad @@ -17,25 +17,25 @@ [{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n move-to-cell {:board :main-root, :child :main-child, :row 1, :column 3}", :states ["a3bca1c5fd1b9b29" "8a7fa3fde8815d35" "1c786eab89f462da"]} {:transcript "one-of -> skip\n move-to-cell {:board :main-root, :child :main-child, :row 1, :column 3}", :states ["7f1f0be7424c543f" "7c9214e430ebb4e8"]}] :ag - [{:transcript "move-child {:target :sub1, :parent :row-root, :to-index 3}", :states ["7a572aaeec3d11c7" "4ef6a524e41aa81f"]}] + [{:transcript "move-child {:target :sub1, :parent :row-root, :to-index 3}", :states ["7a572aaeec3d11c7" "e6899eae694479cf"]}] :ah [{:transcript "one-of -> recorded-choice\n remove-child {:target :copy-sub1}", :states ["3043434871927bf0" "c5acd77993218759"]} {:transcript "one-of -> recorded-choice\n remove-child {:target :copy-sub3}", :states ["fdddae12ff7dd7bc" "7efeb83144959be0"]} {:transcript "one-of -> skip", :states ["4f8193bba8dbfb0d"]}] :ak - [{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n move-to-cell {:board :copy-root, :child :copy-child, :row 1, :column 3}\n update-main {:target :copy-root, :updated grid-copy}", :states ["a3bca1c5fd1b9b29" "246adb9d062e7222" "c4587bbdc8952e96" "400ce49b85daec77"]} + [{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n move-to-cell {:board :copy-root, :child :copy-child, :row 1, :column 3}\n update-main {:target :copy-root, :updated grid-copy}", :states ["a3bca1c5fd1b9b29" "246adb9d062e7222" "725449d340ed4db7" "f7c72fa8b8699165"]} {:transcript "one-of -> skip\n move-to-cell {:board :copy-root, :child :copy-child, :row 1, :column 3}\n update-main {:target :copy-root, :updated grid-copy}", :states ["8aad596633c254fd" "7f1f0be7424c543f" "7c9214e430ebb4e8"]}] :al [{:transcript "change-property {:target :copy-root, :property :name, :value \"Renamed\"}\n change-property {:target :copy-instance, :property :name, :value \"Renamed child\"}\n update-main {:target :copy-root, :updated copy-root}", :states ["0166c98332ed23a7" "6c0c864aa286702f" "af3a00259b74b965" "6598d0729b91c5b3"]} {:transcript "change-property {:target :copy-root, :property :name, :value \"Renamed\"}\n one-of -> skip\n update-main {:target :copy-root, :updated copy-root}", :states ["0166c98332ed23a7" "e2849db0c0174bfc" "c307f8e2c66e8760"]}] :am - [{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n update-main {:target :copy-root, :updated grouped-copy}", :states ["97535386c5e6b064" "ae99b169cd56a48c" "40447fef13f32e5a"]}] + [{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n update-main {:target :copy-root, :updated grouped-copy}", :states ["606d0e9ed09ebe9b" "c96014db9eb364e5" "bc48794035d17e6f"]}] :an [{:transcript "change-property {:target :copy-instance, :property :margin, :value 8}\n change-property {:target :main-instance, :property :margin, :value 20}", :states ["4087468201a18ebc" "04328fe58e252ee9" "85db26a4ea6a899c"]} {:transcript "change-property {:target :copy-instance, :property :margin, :value 8}\n reset-overrides {:target :copy-root, :reset copy-root}", :states ["4087468201a18ebc" "cb5b3ab586333d10" "054cd426071de8e3"]} {:transcript "change-property {:target :copy-instance, :property :margin, :value 8}\n update-main {:target :copy-root, :updated copy-root}", :states ["4087468201a18ebc" "6b82a7e5bb516fc5" "8d389cd790e5cfb3"]}] :ao - [{:transcript "nest-copy {:copy :loose, :into :main-root, :cell [1 3]}", :states ["6e7144142468b3ea" "5e11927c1505e3f6"]}] + [{:transcript "nest-copy {:copy :loose, :into :main-root, :cell [1 3]}", :states ["6e7144142468b3ea" "16834d9e0ad8afe0"]}] :ap [{:transcript "sync-from-library {:library-id library}", :states ["3550b8df58ff0c95" "3872af2b94d47dea"]}] :aq @@ -54,8 +54,8 @@ :ay [{:transcript "create-component {:component \"main\", :id sync-main-component}\n add-tokens {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n apply-token {:target #fn, :token-name \"radius.s\", :value 10}\n apply-token {:target #fn, :token-name \"radius.l\", :value 30}\n test\n unapply-token {:target #fn, :token-name \"radius.l\"}", :states ["59efd69700884e48" "abf5f79fd6265aff" "39b3757cf3962625" "90094c5af982b268"]} {:transcript "create-component {:component \"main\", :id sync-main-component}\n add-tokens {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n apply-token {:target #fn, :token-name \"radius.s\", :value 10}\n apply-token {:target #fn, :token-name \"radius.l\", :value 30}\n test\n one-of -> skip", :states ["59efd69700884e48" "abf5f79fd6265aff" "35f26a160913fed7"]} - {:transcript "create-component {:component \"main\", :id sync-main-component}\n add-tokens {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> skip\n apply-token {:target #fn, :token-name \"radius.l\", :value 30}\n test\n unapply-token {:target #fn, :token-name \"radius.l\"}", :states ["b59fbe5fc9ca6e6b" "6f8d6e70db24f3ee" "23156b8978646a6a"]} - {:transcript "create-component {:component \"main\", :id sync-main-component}\n add-tokens {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> skip\n apply-token {:target #fn, :token-name \"radius.l\", :value 30}\n test\n one-of -> skip", :states ["b59fbe5fc9ca6e6b" "959ff508596afeae"]}] + {:transcript "create-component {:component \"main\", :id sync-main-component}\n add-tokens {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> skip\n apply-token {:target #fn, :token-name \"radius.l\", :value 30}\n test\n unapply-token {:target #fn, :token-name \"radius.l\"}", :states ["b59fbe5fc9ca6e6b" "6f8d6e70db24f3ee" "0ca3152225a95d53"]} + {:transcript "create-component {:component \"main\", :id sync-main-component}\n add-tokens {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> skip\n apply-token {:target #fn, :token-name \"radius.l\", :value 30}\n test\n one-of -> skip", :states ["b59fbe5fc9ca6e6b" "1b4cbbfda551811b"]}] :az [{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n paste-to-root {:target #fn, :new-head 5}\n test\n change-property {:target #fn, :property :fills, :value \"#ff0000\"}", :states ["c8c845596f55aa86" "1d1b26cfc6a035e4" "cdcb72926d45d3db" "c6ccfcd10b8e16a3"]} {:transcript "create-component {:component \"main\", :id sync-main-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n one-of -> skip\n paste-to-root {:target #fn, :new-head 3}\n test\n change-property {:target #fn, :property :fills, :value \"#ff0000\"}", :states ["c8c845596f55aa86" "a6a97a3d3298a63a" "a47933c7e373dfd7" "75123b44e98d2e2c"]}] @@ -86,7 +86,7 @@ :edit-through-a-chain-of-dependents [{:transcript "edit-through-a-chain-of-dependents", :states ["9ee47520f6b1a0e9"]}] :f - [{:transcript "move-child {:target :main-child1, :parent :main-root, :to-index 2}", :states ["4ca7ef4736e23ba7" "8082d4caea054434"]}] + [{:transcript "move-child {:target :main-child1, :parent :main-root, :to-index 2}", :states ["4ca7ef4736e23ba7" "a06b120581c9097f"]}] :h [{:transcript "sync-from-library {:library-id library}", :states ["ac5b8031232b4f6a" "7448e7e0603e54be"]}] :i diff --git a/common/test/common_tests/logic/library_asset_sync_test.cljc b/common/test/common_tests/logic/library_asset_sync_test.cljc index 3655134725..8a707fc645 100644 --- a/common/test/common_tests/logic/library_asset_sync_test.cljc +++ b/common/test/common_tests/logic/library_asset_sync_test.cljc @@ -147,3 +147,18 @@ (let [file' (change-library-typography (component-using-typography) "24") node (get-in (ths/get-shape file' :main-child) [:content :children 0 :children 0 :children 0])] (t/is (not (contains? node :modified-at))))) + +(t/deftest undo-of-a-library-typography-sync-gives-back-the-text + (let [pos-data [{:x 1 :y 2 :width 30 :height 14 :fills [] :text "hello"}] + file (-> (component-using-typography) + (ths/update-shape :main-child :position-data pos-data) + (update :data cttl/update-typography (thi/id :typography1) assoc :font-size "24")) + changes (cll/generate-sync-file-changes (pcb/empty-changes) nil :typographies (:id file) + (thi/id :typography1) (:id file) {(:id file) file} (:id file)) + file' (thf/apply-changes file changes) + file'' (thf/apply-undo-changes file' changes) + main (ths/get-shape file'' :main-child)] + (t/is (nil? (:position-data (ths/get-shape file' :main-child)))) + (t/is (= "14" (font-size-of main))) + (t/is (= pos-data (:position-data main))))) + diff --git a/frontend/test/frontend_tests/composable_tests/golden_master.edn b/frontend/test/frontend_tests/composable_tests/golden_master.edn index 276f936556..1f771c117d 100644 --- a/frontend/test/frontend_tests/composable_tests/golden_master.edn +++ b/frontend/test/frontend_tests/composable_tests/golden_master.edn @@ -5,7 +5,7 @@ {:transcript " {:component \"main\", :id sync-main-component}\n {:component \"s1\", :id sync-s1-component}\n {:component \"s2\", :id sync-s2-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :to \"s1\", :keep-touched? false}\n one-of -> \n {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["a1eb6e86bd5c7824" "16622fffacac0c8d" "964a553e029b1e19"]} {:transcript " {:component \"main\", :id sync-main-component}\n {:component \"s1\", :id sync-s1-component}\n {:component \"s2\", :id sync-s2-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :to \"s1\", :keep-touched? false}\n one-of -> \n one-of -> ", :states ["a1eb6e86bd5c7824" "4ce98879b40b53aa"]}] :case-ab-reset-of-nested-swap-keeps-top-copy-overrides - [{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"s1\", :id sync-s1-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :opacity, :value 0.5}\n {:target #fn, :to \"s1\", :keep-touched? false}\n {:target #fn}", :states ["18092b58aa495ad2" "783143fdc428e89e" "8398b5c6929d2064" "c13a6df26c01deb1"]}] + [{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"s1\", :id sync-s1-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :opacity, :value 0.5}\n {:target #fn, :to \"s1\", :keep-touched? false}\n {:target #fn}", :states ["18092b58aa495ad2" "783143fdc428e89e" "18092b58aa495ad2" "a7a9887e39859a9a"]}] :case-ac-second-variant-switch-keeps-root-overrides [{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 3}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :opacity, :value 0.5}\n {:target #fn, :value \"b\", :pos 0}\n \n {:target #fn, :value \"c\", :pos 0}", :states ["45b82e3d3433e503" "6aa8b050f6e60b75" "2464ce46aa6d70f8" "83ae3b7689a44277"]}] :case-ad-update-main-pushes-a-swapped-child @@ -17,7 +17,7 @@ [{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:board :main-root, :child :main-child, :row 1, :column 3}", :states ["5a6af6c0b8f86ac3" "ec2294c072f52ae5" "8cd63b096547fb1b"]} {:transcript "one-of -> \n {:board :main-root, :child :main-child, :row 1, :column 3}", :states ["ab911a9c4ef389ba" "56df09285036aa60"]}] :case-ag-main-reorder-keeps-copy-sub-heads-aligned - [{:transcript " {:target :sub1, :parent :row-root, :to-index 3}", :states ["8118be0e3d318e30" "bd3db9738a8851c0"]}] + [{:transcript " {:target :sub1, :parent :row-root, :to-index 3}", :states ["8118be0e3d318e30" "e0d17dd4c27d652c"]}] :case-ah-copy-sub-head-delete-keeps-others-aligned [{:transcript "one-of -> \n {:target :copy-sub1}", :states ["fff37cf722a40e61" "6fd3a8bad999968b"]} {:transcript "one-of -> \n {:target :copy-sub3}", :states ["efada86db353920f" "3364b0fbfb76067a"]} @@ -32,19 +32,19 @@ :case-aj-reflow-after-nested-sync-is-its-own-undo-step [{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#0000ff\"}\n {:times 1}", :states ["43ca8b2de80a51fc" "43ca8b2de80a51fc" "9c3022331b3fe9ae"]}] :case-ak-update-main-pushes-grid-cells - [{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:board :copy-root, :child :copy-child, :row 1, :column 3}\n {:target :copy-root}", :states ["5a6af6c0b8f86ac3" "278e4ff6bf0df746" "f611ebe15fb41317" "ead3c5e066d93cc4"]} + [{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:board :copy-root, :child :copy-child, :row 1, :column 3}\n {:target :copy-root}", :states ["5a6af6c0b8f86ac3" "278e4ff6bf0df746" "322e03c88f4a9dce" "432cfef7e9b05164"]} {:transcript "one-of -> \n {:board :copy-root, :child :copy-child, :row 1, :column 3}\n {:target :copy-root}", :states ["9900a7f71dc0cb5f" "de66ab1c3d4cc157" "6b5ff9fd86e2aa87"]}] :case-al-update-main-renames-the-component [{:transcript " {:target :copy-root, :property :name, :value \"Renamed\"}\n {:target :copy-instance, :property :name, :value \"Renamed child\"}\n {:target :copy-root}", :states ["d4e11348047fc1e1" "0021aeab497d25e6" "c0aa5a2b915a7542" "ca23729038aec91f"]} {:transcript " {:target :copy-root, :property :name, :value \"Renamed\"}\n one-of -> \n {:target :copy-root}", :states ["d4e11348047fc1e1" "409795fc3e2bb3a7" "6a882f51aaf16306"]}] :case-am-update-main-pushes-a-swap-inside-a-group - [{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:target :copy-root}", :states ["aba215c5bc93ff0d" "f2232d12b1707e79" "a9496203eed2406e"]}] + [{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:target :copy-root}", :states ["b4f808a12f723283" "9b695fc212f26b53" "32b94deb961ac763"]}] :case-an-flex-child-override-follows-sync-reset-and-update-main [{:transcript " {:target :copy-instance, :property :margin, :value 8}\n {:target :main-instance, :property :margin, :value 20}", :states ["234a21e54bf21bd5" "03664b1e6eb74856" "9c3b12abbc6089ce"]} {:transcript " {:target :copy-instance, :property :margin, :value 8}\n {:target :copy-root}", :states ["234a21e54bf21bd5" "1f92f9b4d2e50598" "a09954797e57930c"]} {:transcript " {:target :copy-instance, :property :margin, :value 8}\n {:target :copy-root}", :states ["234a21e54bf21bd5" "ace1e76c1da2c443" "8f3452e82544f1ad"]}] :case-ao-copy-dropped-into-a-grid-cell-reaches-copies - [{:transcript " {:copy :loose, :into :main-root, :cell [1 3]}", :states ["abc6e41a291516a2" "88ef39349d35f0b6"]}] + [{:transcript " {:copy :loose, :into :main-root, :cell [1 3]}", :states ["abc6e41a291516a2" "571d314ba8917a08"]}] :case-ap-library-main-child-removal-reaches-copy [{:transcript "", :states ["f53d220c42298128" "7c2dfde6478d399c"]}] :case-aq-main-change-reaches-a-copy-nested-in-a-sub-board @@ -79,8 +79,8 @@ :case-ay-token-on-a-main-reaches-copies [{:transcript " {:component \"main\", :id sync-main-component}\n {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :token-name \"radius.s\", :value 10}\n {:target #fn, :token-name \"radius.l\", :value 30}\n \n {:target #fn, :token-name \"radius.l\"}", :states ["04cddffca776d7fb" "24756b13d210f3a5" "8e8c0c21d4a0638b" "f656f9c8188793cf"]} {:transcript " {:component \"main\", :id sync-main-component}\n {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :token-name \"radius.s\", :value 10}\n {:target #fn, :token-name \"radius.l\", :value 30}\n \n one-of -> ", :states ["04cddffca776d7fb" "24756b13d210f3a5" "8fb17ce46cb883c8"]} - {:transcript " {:component \"main\", :id sync-main-component}\n {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> \n {:target #fn, :token-name \"radius.l\", :value 30}\n \n {:target #fn, :token-name \"radius.l\"}", :states ["d943d9831136267d" "ae544068bc050c2a" "eb8434e3f9dd763e"]} - {:transcript " {:component \"main\", :id sync-main-component}\n {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> \n {:target #fn, :token-name \"radius.l\", :value 30}\n \n one-of -> ", :states ["d943d9831136267d" "62fe93164996f7fe"]}] + {:transcript " {:component \"main\", :id sync-main-component}\n {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> \n {:target #fn, :token-name \"radius.l\", :value 30}\n \n {:target #fn, :token-name \"radius.l\"}", :states ["d943d9831136267d" "ae544068bc050c2a" "458ba8e9225d9ae7"]} + {:transcript " {:component \"main\", :id sync-main-component}\n {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> \n {:target #fn, :token-name \"radius.l\", :value 30}\n \n one-of -> ", :states ["d943d9831136267d" "b97f65e9e0e4e3c7"]}] :case-az-copy-moved-out-of-its-main-keeps-inherited-overrides [{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n {:target #fn, :new-head 5}\n \n {:target #fn, :property :fills, :value \"#ff0000\"}", :states ["d6802c2361c4f48a" "09526a373b2bf78d" "f6588d21ee3f82ea" "89696ef8abcbb71d"]} {:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n one-of -> \n {:target #fn, :new-head 3}\n \n {:target #fn, :property :fills, :value \"#ff0000\"}", :states ["d6802c2361c4f48a" "7443c8f4406f3704" "78edc4e41925495c" "62c32614a4d51244"]}] @@ -107,7 +107,7 @@ :case-e-remove-shape-from-main-auto-propagates-to-clean-copy [{:transcript " {:target :main-child2}", :states ["43c6f0fafdb79914" "d084fa213a85ee68"]}] :case-f-move-shape-in-main-auto-propagates-order-to-clean-copy - [{:transcript " {:target :main-child1, :parent :main-root, :to-index 2}", :states ["1994876f6bbe7d2e" "0ac4d4f98925b8d9"]}] + [{:transcript " {:target :main-child1, :parent :main-root, :to-index 2}", :states ["1994876f6bbe7d2e" "de144b280bc4fc82"]}] :case-h-library-change-propagates-across-file-boundary-on-sync [{:transcript "", :states ["d8c5e8bda3728127" "6ee281995d7d8f65"]}] :case-i-undo-reverts-edit-and-its-auto-propagation