🐛 Restore parent sync state when undoing child structure edits

Add, remove and move inverses restore the copy parent's remote-synced
flag with its touched groups. Swapping a child clears that flag during
delete; undo must restore the original parent state (F157).

Swap and add/remove regressions fail before the change. The focused swap
suite passes 49 tests and 191 assertions; common lint and format pass.
Benchmark ratios: mains 0.49/0.57/0.62; levels 0.53; chain 0.57; library
0.69; many shapes 0.62; switch 0.62, no regression flags.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-24 00:54:58 +02:00
parent bc4edefd81
commit c48afa9f35
3 changed files with 51 additions and 11 deletions

View File

@ -36,4 +36,9 @@ For component touched semantics and sync groups, read `mem:common/component-data
## Inspection
To inspect what a UI action emitted, use `mem:frontend/cljs-repl` with the snippets in `mem:common/component-debugging-recipes` rather than adding temporary source instrumentation.
To inspect what a UI action emitted, use `mem:frontend/cljs-repl` with the snippets in `mem:common/component-debugging-recipes` rather than adding temporary source instrumentation.
- Structural add/remove and change-parent inverses restore the copy parent's
`:remote-synced` as well as `:touched`. Removing or adding a child clears
the former even when touched groups do not change; restoring only the
groups loses prior state on undo (F157).

View File

@ -458,6 +458,15 @@
;; Shape tree changes
(defn- restore-parent-state-change
[page-id parent]
{:type :mod-obj
:page-id page-id
:id (:id parent)
:operations (cond-> [{:type :set-touched :touched (:touched parent)}]
(:remote-synced parent)
(conj {:type :set-remote-synced :remote-synced true}))})
(defn add-object
([changes obj]
(add-object changes obj nil))
@ -489,11 +498,7 @@
:ignore-touched true}
restore-touched-change
{:type :mod-obj
:page-id (::page-id (meta changes))
:id (:id parent)
:operations [{:type :set-touched
:touched (:touched parent)}]}]
(restore-parent-state-change (::page-id (meta changes)) parent)]
(-> changes
(update :redo-changes conj add-change)
@ -552,11 +557,7 @@
(assoc :allow-altering-copies true)))))
restore-touched-change
{:type :mod-obj
:page-id (::page-id (meta changes))
:id (:id parent)
:operations [{:type :set-touched
:touched (:touched parent)}]}]
(restore-parent-state-change (::page-id (meta changes)) parent)]
(-> changes
(update :redo-changes conj set-parent-change)
@ -723,6 +724,13 @@
(-> changes
(update :redo-changes #(reduce add-redo-change % ids))
(update :undo-changes #(as-> % $
(reduce (fn [undo parent-id]
(let [parent (get objects parent-id)]
(cond-> undo
(and (not ignore-touched) (:shape-ref parent)
(:remote-synced parent))
(conj (restore-parent-state-change page-id parent)))))
$ (distinct (keep (fn [id] (:parent-id (get objects id))) ids)))
(reduce add-undo-change-parent $ ids)
(reduce add-undo-change-shape $ ids))))))

View File

@ -821,3 +821,30 @@
undone (r/step moved (n/undo))]
(t/is (nil? (:touched head)))
(t/is (= (thn/normalize-file file) (thn/normalize-file (tm/file undone))))))
(t/deftest swapping-a-child-restores-its-parents-remote-sync-flag-on-undo
(let [initial (tm/make-situation (ths/update-shape (setup-sized) :copy :remote-synced true))
swapped (-> initial (r/step (n/swap-shape :copy-star :heart)) r/settle)
undone (r/step swapped (n/undo))]
(t/is (:remote-synced (tm/resolve-shape initial :copy)))
(t/is (not (:remote-synced (tm/resolve-shape swapped :copy))))
(t/is (:remote-synced (tm/resolve-shape undone :copy)))
(t/is (= (thn/normalize-file (tm/file initial))
(thn/normalize-file (tm/file undone))))))
(t/deftest structural-child-changes-restore-the-parents-remote-sync-state
(doseq [operation [:add :remove]]
(let [file (ths/update-shape (setup-sized) :copy-star :remote-synced true)
page (thf/current-page file)
parent (ths/get-shape file :copy-star)
changes (-> (pcb/empty-changes nil (:id page))
(pcb/with-objects (:objects page)))
changes (case operation
:add (pcb/add-object changes (ths/sample-shape :added
:parent-id (:id parent)
:frame-id (:id parent)))
:remove (pcb/remove-objects changes [(first (:shapes parent))]))
changed (thf/apply-changes file changes :validate? false)
undone (thf/apply-undo-changes changed changes)]
(t/is (not (:remote-synced (ths/get-shape changed :copy-star))) (name operation))
(t/is (= (thn/normalize-file file) (thn/normalize-file undone)) (name operation)))))