From a5d2ae277a12b8e3baee88ec741805bbefcc7c65 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 15 Oct 2026 15:11:24 +0200 Subject: [PATCH] :bug: Fix component sync, reset and update main Fixes in the sync of copies from their mains, in reset and in update main (Phase 3c). - Undo a token sync to the tokens a copy shape had. The token sync of a copy shape wrote, as its undo, the shape's tokens read with an empty map as default, so undoing the sync of a main's token gave a copy shape that had none an empty :applied-tokens map instead of no map (F55). The undo now writes the value the shape had. Case AY no longer marks its round trip as a known failure; both golden masters record the round trip without failures in the two variants that hit it. - Reorder a copy's children with the fewest moves and exact undo. When a main reordered its children, sync wrote a move for every copy child the walk found out of place, from its position in the copy's original list to its main's index: - once one child was out of place, every following pair was too, so a main with 1,000 children that moved its first one to the end wrote 999 moves per copy, quadratic in the children (F85); - :mov-objects inserts before the index of the list that still holds the shape, so a move to a later place landed one short, and the undo, which used the original position, did not put the child back: undoing a reorder in a main left its copies reordered (F41). The walk now syncs the pairs, removals and additions first, and then moves the copy's children, as those changes left them, into their main shapes' order: the longest run already in order stays, and each other child moves right after the one before it in the main's order. Each move is computed on the order the moves before it left, so its index is exact, and its undo puts the child back after its old neighbour. The update main walk keeps its moves: the copy's order differs from the main's only after F41's undo, as the UI never reorders a copy. comp-sync-test: a reorder in a six-child main moves each displaced copy child once, gives the main's order, and undoes exactly. Cases F, AG and AO no longer mark their round trip as a known failure; both golden masters record it without failures. - Remap a main's grid cells after the update main pairs children. An update main from a copy of a grid main pushes the copy's cells into the main, with each copy child mapped to its main shape. The cells were remapped before the children walk, when a swapped copy child still referred to the main child it replaces; the walk then removed that child and added a new main child for the swapped one. The main's cell kept the removed child's id and the new child was in no cell (F49); the next sync of the copy took that broken cell back. The cells are now remapped after the walk: each copy child goes with the main shape the walk added for it, or else with the one it refers to. Case AK no longer pins F49: with a swap, the main's third cell holds the new main child, the main takes the copy's cells as they are (the swap had made the first one manual), and the copy keeps its child in the third cell. Both golden masters record that. - Add update main's new main shapes and their undo in the main. When update main pushes a copy child the main lacks (a swapped child), it adds a copy of it to the main (F102): - the refit of the groups around it was a :reg-objects that named the component, which reaches only a deleted component's stored shapes, so a group in the main kept its old bounds; - the undo deleted the new shapes from the copy's page, so when the main sat on another page the undo left them in the main. The new shapes, the refit of their parents and the undo of both now go to the main's container; undoing deletes the new shapes and refits the parents again. The case AM setup and the new test fit their groups to their children, as the editor does; an unfitted group's size is one the refit cannot give back on undo. Both golden masters record case AM with the fitted groups. comp-sync-test: an update main from a copy on one page into a main on another fits the group around the pushed copy, and undo gives the main back. - Sync flips with the rest of the geometry. A flip changes a shape's transform, points and selrect, which sync and touch as geometry, and toggles :flip-x or :flip-y, which were not in sync-attrs: when a main flipped a child, its copies took the flipped transform but kept their old flip flags, which the renderers and the placement of swapped shapes read (F38). A flip on a copy touched geometry through the transform all the same. :flip-x and :flip-y now sync and touch as :geometry-group. comp-sync-test: a copy follows its main's flip, flags and transform, untouched. - Reset a nested copy of a flex board keeping its children. Sync gives each child of a copy of a flex board the flex child attributes (margins, sizing, z-index, ...) of the main child it refers to. It looked that child up in the copy head's own component; for a nested copy that is not where its children's mains are (they are the children of the nested copy in the outer main), so the lookup gave nil, and a reset, which copies every attribute, wrote nil into all of them (F103). A child with no main was written nil the same way. The lookup now uses the container the walk reads the main shapes from, where every child's :shape-ref points, and a child with no main shape is left as it is. comp-reset-test: resetting a nested copy of a flex board keeps its child's margin (with a grid board too, whose children F15 leaves). - Sync layout child attributes of the children of grid mains. Sync and update main copy the layout child attributes of a board's children (margins, sizing limits, z-index, absolute position, align-self) from the shapes they pair with, but only when the main was a flex board: the children of a grid main never got them, so a margin set on a grid main's child never reached its copies (F15). Grid boards now go through the same step, renamed update-layout-children. comp-sync-test: a copy follows the margin its grid main gives a child. comp-reset-test checks the reset of a nested grid copy keeps it too. - Clear remote-synced when a nested copy is reset. A reset makes a copy equal to its near main, but the reset of a nested head marked every shape below the copy root :remote-synced, the mark of a shape last synced from a remote main. Update main reads that mark: it gives the main the copy's overrides only for shapes without it, and otherwise clears the main's touched groups. So after resetting a nested copy, an update main from the top copy cleared the overrides of the nested copy in the main, which the next sync from its own component then overwrote (F13). A reset now clears the mark, as the shapes follow their near main. comp-reset-test: after a reset of a nested copy, an update main from the top copy keeps the fill override of the nested copy in the main. Both golden masters record case AB without the marks. - Check that a nested copy keeps its constraints. A copy of one component dropped into another's main, whose constraints the outer main then sets, keeps them, in that main and in its copies, when the inner component's main is edited: the setting is an override of the nested copy. The test guards it; the reset of the constraints a drop writes, which looked like a loss (F45), changes nothing a user sees, as nil means the same left/top. - Undo a library typography sync to the text's measured layout. The library color and typography syncs rewrite a text's content and clear its :position-data, so the text is measured again. Their undo put back the old content but cleared the position data too, so an undone sync left the text unmeasured (F90). The undo now writes back the position data the text had. library-asset-sync-test: undoing a typography sync gives back the font size and the position data. - File the copy's relinks of an update main with the copy's file. Update main from a copy of a library component writes some changes in the library (the main) and some in the copy's file; the frontend commits each part to its file, telling them apart by the changes of the copy shapes the walk visited (F9). A swapped copy child is not visited: update main adds a main shape for it and relinks the copy shapes to it, and those relinks, changes of the copy, went with the library's changes. The shapes it relinks now count as the copy's. comp-sync-test: pushing a swap from a copy of a library component keeps every change of the copy's shapes in the local part. Benchmark (lower quantile, ms; baseline / now / ratio): deepest-edit-through-three-levels 35.2 / 16.9 / 0.48 edit-through-a-chain-of-dependents 68.9 / 33.0 / 0.48 library-update-into-copies 83.1 / 40.9 / 0.49 main-edit-among-many-shapes 4.8 / 2.3 / 0.48 main-edit-into-10-copies 1.8 / 0.8 / 0.45 main-edit-into-100-copies 15.4 / 7.8 / 0.51 main-edit-into-1000-copies 153.1 / 82.2 / 0.54 variant-switch-into-copies 32.1 / 18.2 / 0.57 AI-assisted-by: claude-opus-5-5 --- common/src/app/common/logic/libraries.cljc | 251 +++++++++++------- .../src/app/common/logic/library_assets.cljc | 9 +- .../test_helpers/composable/comp/cases.cljc | 41 +-- .../test_helpers/composable/comp/setups.cljc | 1 + .../app/common/test_helpers/compositions.cljc | 8 + common/src/app/common/types/component.cljc | 2 + .../common_tests/logic/comp_reset_test.cljc | 75 +++++- .../common_tests/logic/comp_sync_test.cljc | 205 ++++++++++++++ .../test/common_tests/logic/golden_master.edn | 16 +- .../logic/library_asset_sync_test.cljc | 15 ++ .../composable_tests/golden_master.edn | 16 +- 11 files changed, 483 insertions(+), 156 deletions(-) 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