🐛 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
This commit is contained in:
alonso.torres 2026-10-15 15:11:24 +02:00
parent c7ef7b0088
commit a5d2ae277a
11 changed files with 483 additions and 156 deletions

View File

@ -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)

View File

@ -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

View File

@ -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]])

View File

@ -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})

View File

@ -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:

View File

@ -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

View File

@ -498,4 +498,77 @@
;; ==== Check
;; Without propagation the copy still reflects the overridden color
(t/is (= copy2-bottom-color "#fabada"))))
(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))))))

View File

@ -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))))))))

View File

@ -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

View File

@ -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)))))

View File

@ -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