🐛 Track authored grid cells in the component oracle

Compare overridden cells with independent before-state and intent values,
while keeping copy child structure tied to the main. Remap child identities
across duplication, update-main, detach and frontend replay. Direction edits
track both changed groups. The former I4 reproduction stays unchanged.

Add grid actions and seven histories with exact undo/redo, plus controls
for corrupt values, membership, groups, child order and dropped main edits.
Keep random grid rollout pending track growth and switch carry coverage.

Validation: common JVM 2,009/32,967; JS 1,945/31,582; frontend
1,749/13,838; backend 1,793/9,007; exporter 16/40. All suites, lint and
format pass. Six properties run 40 programs at seed 712 in both runtimes.

AI-assisted-by: gpt-6
This commit is contained in:
alonso.torres 2026-10-29 10:08:10 +01:00
parent b84411c5c9
commit 7d2c20b804
8 changed files with 315 additions and 12 deletions

View File

@ -221,6 +221,39 @@
[target property value]
(tm/assign-id (->ChangeProperty target property value)))
(defrecord EditGrid [target action value]
IComponentOperation
(op-kind [_] :user)
tm/IOperation
(apply-to [this situation]
(let [file (tm/file situation)
page (thf/current-page file)
id (tm/target-shape-id situation target)
edit (fn [shape]
(case action
:direction (let [from (or (:layout-grid-dir shape) :row)]
(if (= from value)
(assoc shape :layout-grid-dir value)
(ctl/reflow-grid-auto-items-for-direction shape from value)))
:cell (update shape :layout-grid-cells
(fn [cells]
(update-vals cells #(if (= [(:row value) (:column value)] [(:row %) (:column %)])
(merge % (:props value)) %))))))
changes (-> (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) [id]
edit (:objects page)
{:skip-grid-reassignment? (= action :direction)})
(with-layout-reflow id)
(assoc ::layout-reflow? true))]
(-> situation
(tm/with-file (thf/apply-changes file changes))
(record-changes changes)
(tm/record-application this {:target target :action action :value value})))))
(defn edit-grid
[target action value]
(tm/assign-id (->EditGrid target action value)))
(defprotocol IPropertyCheck
"The check dual of an operation that changes a property: whether a shape the
test supplies holds the value the operation set."

View File

@ -24,6 +24,7 @@
[app.common.test-helpers.composable.comp.runner :as r]
[app.common.test-helpers.composable.comp.setups :as setup]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.composable.grid :as grid]
[app.common.test-helpers.composable.oracle :as oracle]
[app.common.test-helpers.composable.shadow :as shadow]
[app.common.test-helpers.composable.variants :as variants]
@ -98,6 +99,23 @@
situation ids)
(with-settled-layout name))))
(defn- with-square-grid
[situation name]
(let [id ((n/main-head-of name) situation)
shape (tm/shape-by-id situation id)
children (vec (reverse (:shapes shape)))
cells (into {} (map-indexed (fn [index [row column]]
(let [id (uuid/next)]
[id {:id id :row row :column column :row-span 1 :column-span 1
:position :auto :align-self :auto :justify-self :auto
:shapes (if-let [child (get children index)] [child] [])}]))
[[1 1] [1 2] [2 1] [2 2]]))
file (-> (tm/file situation)
(ths/update-shape-by-id (:id shape) :layout-grid-rows [{:type :fixed :value 60} {:type :fixed :value 60}])
(ths/update-shape-by-id (:id shape) :layout-grid-columns [{:type :fixed :value 80} {:type :fixed :value 80}])
(ths/update-shape-by-id (:id shape) :layout-grid-cells cells))]
(with-settled-layout (tm/with-file situation file) name)))
(defn- with-outer-layout
[situation name layout]
(if-not layout
@ -214,6 +232,8 @@
(with-layout-children name))
(cond-> (and (zero? index) (:layout opts) (:layout-item-margins opts))
(with-layout-item-margins name))
(cond-> (and (zero? index) (= :grid (:layout opts)) (:grid-square opts))
(with-square-grid name))
(as-> s (reduce #(tm/apply-to %2 %1) s
[(n/make-nested-component name depth)]))
(with-outer-layout name (when (and (zero? index) (pos? depth) (:layout-copies opts)) (:layout opts)))
@ -473,6 +493,10 @@
[:direction ["ltr" "rtl"]]
[:valign ["top" "bottom"]]])
(def grid-cell-edits
[{:align-self :end} {:justify-self :center} {:align-self :center :justify-self :end}
{:align-self :auto :justify-self :auto}])
(defn- edit-text-style
[shape unit value]
(let [[style values] (nth text-style-edits (mod (or unit 0) (count text-style-edits)))
@ -495,7 +519,10 @@
[style-property style-values] (nth style-edits (mod (or property 0) (count style-edits)))
items (cond->> (if (contains? library-source-kinds kind)
(library-candidates situation kind scope)
(candidates situation model (if (= kind :style) :edit kind) scope))
(candidates situation model (if (#{:style :grid-cell :grid-direction} kind) :edit kind) scope))
(#{:grid-cell :grid-direction} kind)
(filter #(and (= :grid (get-in % [:shape :layout]))
(seq (get-in % [:shape :layout-grid-cells]))))
(= kind :style)
(filter (fn [{:keys [shape]}]
(and (shadow/attribute-applicable? shape style-property)
@ -543,6 +570,11 @@
(when target
(case kind
(:edit :style) (n/change-property target property value)
:grid-direction (n/edit-grid target :direction (nth [:row :column] (mod (or (nth tuple 3 nil) 0) 2)))
:grid-cell (let [cells (grid/get-cells (:shape selected))
cell (nth cells (mod (or (nth tuple 2 nil) 0) (count cells)))
props (nth grid-cell-edits (mod (or (nth tuple 3 nil) 0) (count grid-cell-edits)))]
(n/edit-grid target :cell (assoc (select-keys cell [:row :column]) :props props)))
(:transform :library-transform) (let [action (nth [:move :width :height :rotate :flip-x :flip-y]
(mod (or (nth tuple 2 nil) 0) 6))
value (or (nth tuple 3 nil) 0)]
@ -747,6 +779,7 @@
id (:id shape)]
(case kind
(:edit :style) (shadow/edit model shape property value)
(:grid-cell :grid-direction) (grid/edit model shape (:action op) (:value op))
:reset (shadow/clear-subtree model objects id)
:update-main (shadow/update-main model objects (all-objects after) id)
:detach (shadow/detach model objects (all-objects after) id)
@ -842,6 +875,9 @@
(oracle/check-requested-geometry (:objects (thf/current-page (tm/file after)))
(tm/shape-by-id after (get-in resolved [:shape :id]))
(requested-geometry before resolved kind))))
(when (#{:grid-cell :grid-direction} kind)
(oracle/check-requested-grid (tm/shape-by-id after (get-in resolved [:shape :id]))
(:shape resolved) (:action op) (:value op)))
(when (#{:swap :switch} kind)
(let [head (tm/shape-by-id after (get-in resolved [:shape :id]))]
(when-not (= (get-in resolved [:destination :component-id]) (:component-id head))

View File

@ -0,0 +1,105 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.common.test-helpers.composable.grid
"Independent cell expectations. No production grid assignment or direction
helper supplies expected values.")
(defn get-cells
[shape]
(->> (vals (:layout-grid-cells shape))
(sort-by (juxt :row :column))
(mapv #(dissoc % :id))))
(defn remap-groups
[groups ids-map]
(cond-> groups
(:layout-grid-cells groups)
(update-in [:layout-grid-cells :layout-grid-cells]
(fn [cells]
(mapv #(update % :shapes (fn [ids] (mapv (fn [id] (get ids-map id id)) ids))) cells)))))
(defn get-child-map
"Pair identities through refs, never through resulting cell membership."
[objects source target]
(let [source-ids (set (:shapes source))
target-ids (set (:shapes target))
find-ref (fn [ids id]
(loop [candidate id seen #{}]
(cond
(contains? ids candidate) candidate
(or (nil? candidate) (contains? seen candidate)) nil
:else (recur (:shape-ref (get objects candidate)) (conj seen candidate)))))
forward (into {} (keep #(when-let [id (find-ref target-ids %)] [% id])) (:shapes source))
backward (into {} (keep #(when-let [id (find-ref source-ids %)] [id %])) (:shapes target))]
(merge forward backward)))
(defn direction-cells
[shape direction]
(let [cells (get-cells shape)
order (fn [dir] (if (= dir :column) (juxt :column :row) (juxt :row :column)))
eligible (fn [cell] (and (= :auto (:position cell)) (= 1 (:row-span cell) (:column-span cell))))
old (filter eligible (sort-by (order (:layout-grid-dir shape)) cells))
new (filter eligible (sort-by (order direction) cells))
members (vec (mapcat :shapes old))
by-cell (into {} (map-indexed (fn [index cell]
[[(:row cell) (:column cell)]
(if-let [id (nth members index nil)] [id] [])]) new))]
(if (= (or (:layout-grid-dir shape) :row) direction)
cells
(mapv (fn [cell]
(if-let [ids (get by-cell [(:row cell) (:column cell)])]
(assoc cell :shapes ids)
cell)) cells))))
(defn get-intended-values
[shape action value]
(let [before (get-cells shape)
cells (case action
:direction (direction-cells shape value)
:cell (mapv #(if (= [(:row value) (:column value)] [(:row %) (:column %)])
(merge % (:props value)) %) before))]
(cond-> {:layout-grid-cells cells}
(= action :direction) (assoc :layout-grid-dir value))))
(defn edit
[model shape action value]
(if (:shape-ref shape)
(reduce-kv (fn [model key value]
(if (= value (if (= key :layout-grid-cells) (get-cells shape) (get shape key)))
model
(assoc-in model [(:id shape) key] {key value})))
model (get-intended-values shape action value))
model))
(defn settle-cells
"Drop departed/absolute children; fill existing empty cells with participating
children in parent order. A gap, manual cell or span ends the auto prefix."
[cells shape objects]
(let [order (if (= :column (:layout-grid-dir shape)) (juxt :column :row) (juxt :row :column))
participating (filterv #(let [child (get objects %)]
(not (or (:hidden child) (:layout-item-absolute child)))) (:shapes shape))
members (set participating)
prefix (fn [cells]
(second
(reduce (fn [[ended result] cell]
(let [manual (or (= :manual (:position cell))
(not= 1 (:row-span cell) (:column-span cell))
(and ended (seq (:shapes cell)) (= :auto (:position cell))))]
[(or ended manual (empty? (:shapes cell)))
(conj result (cond-> cell
(and manual (= :auto (:position cell))) (assoc :position :manual)))]))
[false []] cells)))
cells (->> cells (sort-by order)
(mapv #(update % :shapes (fn [ids] (filterv members ids)))) prefix)
assigned (set (mapcat :shapes cells))
missing (remove assigned participating)
[cells _] (reduce (fn [[result pending] cell]
(if (and (seq pending) (empty? (:shapes cell)))
[(conj result (assoc cell :position :auto :shapes [(first pending)])) (rest pending)]
[(conj result cell) pending]))
[[] missing] cells)]
(->> cells prefix (sort-by (juxt :row :column)) vec)))

View File

@ -16,6 +16,7 @@
[app.common.test-helpers.composable.comp.undo-check :as uc]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.composable.geometry :as geom]
[app.common.test-helpers.composable.grid :as grid]
[app.common.test-helpers.composable.shadow :as shadow]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.normalize :as thn]
@ -33,6 +34,14 @@
(throw (ex-info (str "Component invariant " (name invariant) " failed")
(assoc details :invariant invariant))))
(defn check-requested-grid
[shape before action value]
(let [expected (grid/get-intended-values before action value)
actual (cond-> {:layout-grid-cells (grid/get-cells shape)}
(= action :direction) (assoc :layout-grid-dir (:layout-grid-dir shape)))]
(when-not (= expected actual)
(fail :I9 {:phase :requested-grid :expected expected :actual actual}))))
(defn snapshot
[situation]
(into {} (map (fn [[id file]]
@ -355,12 +364,14 @@
(->> cells end-auto-prefix (sort-by (juxt :row :column)) vec)))
(defn- check-structure
[objects shape main]
[objects shape main authored]
(let [refs (mapv #(get-in objects [% :shape-ref]) (:shapes shape))]
(when-not (= (vec (:shapes main)) refs)
(fail :I4 {:shape (:name shape) :group :structure
:expected (:shapes main) :actual refs}))
(when (= :grid (:layout main))
;; Authored cells have already been checked against independent intent
;; by I9. Child structure still follows the main in every case.
(when (and (= :grid (:layout main)) (not (contains? authored :layout-grid-cells)))
(let [expected (expected-grid-cells objects shape main)
actual (grid-cells shape)]
(when-not (= expected actual)
@ -414,7 +425,7 @@
:expected value :actual (shadow/values shape group)})))
(when (and settled (:shape-ref shape))
(when-let [main (ctf/find-content-ref-shape file container libraries shape :include-deleted? true)]
(check-structure (:objects page) shape main)
(check-structure (:objects page) shape main expected)
(doseq [group (keys shadow/attributes)
:when (and (not= group :geometry-group) (not (contains? touched group)))
:when (not (and (= group :name-group) (= :text (:type shape))

View File

@ -12,6 +12,7 @@
[app.common.math :as mth]
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.composable.geometry :as geom]
[app.common.test-helpers.composable.grid :as grid]
[app.common.test-helpers.texts :as thtx]
[app.common.types.container :as ctn]))
@ -104,9 +105,11 @@
(defn values
[shape group]
(if (contains? text-groups group)
(cond
(= group :layout-grid-cells) {:layout-grid-cells (grid/get-cells shape)}
(contains? text-groups group)
{:content (:content shape)}
(into {} (map #(vector % (get shape %))) (get attributes group))))
:else (into {} (map #(vector % (get shape %))) (get attributes group))))
(defn author-geometry
[shape parent]
@ -161,7 +164,8 @@
(defn duplicate
[model before source-id after copy-id]
(letfn [(copy-groups [result source-id copy-id]
(let [groups (get model source-id)
(let [groups (grid/remap-groups (get model source-id)
(zipmap (:shapes (get before source-id)) (:shapes (get after copy-id))))
result (cond-> result (seq groups) (assoc copy-id groups))]
(reduce (fn [result [source child]] (copy-groups result source child)) result
(map vector (:shapes (get before source-id)) (:shapes (get after copy-id))))))]
@ -202,7 +206,7 @@
(loop [shape shape groups (get model (:id shape)) seen #{}]
(let [ref (:shape-ref shape)
source (get before ref)
inherited (get model ref)
inherited (grid/remap-groups (get model ref) (grid/get-child-map before source original))
inherited (cond-> inherited
(:geometry-group inherited)
(update :geometry-group in-orientation source original))]
@ -305,7 +309,7 @@
(assoc id {:swap-group {}})
(and (:shape-ref ref) (seq groups))
(update (:id ref) merge
(cond-> groups
(cond-> (grid/remap-groups groups (grid/get-child-map after shape ref))
(= (:component-id ref)
(:component-id (get after (:shape-ref ref))))
(dissoc :swap-group))))]
@ -342,7 +346,10 @@
ordinary? (and shape position
(nil? (:component-id shape))
(nil? (:component-id position)))
groups (cond-> groups ordinary? (dissoc :swap-group))]
groups (cond-> groups
ordinary? (dissoc :swap-group)
(:layout-grid-cells groups)
(update-in [:layout-grid-cells :layout-grid-cells] grid/settle-cells shape objects))]
(if (seq groups) (assoc result id groups) (dissoc result id))))
model model))

View File

@ -29,6 +29,83 @@
(t/use-fixtures :each thi/test-fixture)
(t/deftest authored-grid-cell-values-are-checked-against-intent-instead-of-the-main
(let [situation (cg/world [1 0 {:layout :grid :layout-children true}])
id ((n/nth-copy-head-of "random-0" 0) situation)
shape (tm/shape-by-id situation id)
cell-id (:id (first (sort-by (juxt :row :column) (vals (:layout-grid-cells shape)))))
intended (assoc-in shape [:layout-grid-cells cell-id :align-self] :end)
file (tm/file situation)
page (thf/current-page file)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) [id]
(constantly intended) (:objects page) {})
edited (tm/with-file situation (thf/apply-changes file changes))
model {id {:layout-grid-cells (shadow/values intended :layout-grid-cells)}}
failure (try (oracle/check edited model) nil
(catch #?(:clj Exception :cljs js/Error) error (ex-data error)))]
(t/is (= #{:layout-grid-cells} (:touched (tm/shape-by-id edited id))))
(t/is (nil? failure) (pr-str failure))))
(t/deftest grid-overrides-survive-component-histories
(doseq [ops [[[:grid-direction 0 0 1 1] [:grid-direction 0 0 1 0]
[:grid-direction 0 0 0 0] [:update-main 0]
[:grid-direction 0 0 0 1] [:reset 0] [:undo] [:redo]]
[[:grid-cell 0 0 0 1] [:grid-cell 0 1 1 0] [:grid-cell 0 0 2 0]
[:update-main 0] [:grid-cell 0 1 2 1] [:reset 0] [:undo] [:redo]]
[[:grid-cell 0 0 0 1] [:duplicate 0] [:undo] [:redo] [:update-main 0]]
[[:grid-direction 0 0 1 1] [:add-child 0] [:undo] [:redo]]
[[:grid-cell 0 0 0 1] [:remove-child 0] [:undo] [:redo]]
[[:grid-cell 0 0 0 1] [:detach 0] [:undo] [:redo]]]]
(let [result (cg/run {:world [1 0 {:layout :grid :layout-children true :grid-square true}] :ops ops})]
(doseq [kind (set (map first ops))]
(t/is (zero? (get-in result [:coverage :random kind :skipped] 0)) (str kind " in " ops))
(when-not (#{:undo :redo} kind)
(t/is (pos? (get-in result [:coverage :random kind :changed] 0)) (str kind " in " ops)))))))
(t/deftest grid-directions-track-both-groups-and-reject-dropped-main-edits
(let [initial (cg/initial-state [1 0 {:layout :grid :layout-children true :grid-square true}])
resolved (cg/resolve-op (:situation initial) {} [:grid-direction 0 0 1 1])
id (get-in resolved [:shape :id])
edited (-> initial (cg/step [:grid-direction 0 0 1 1]) cg/settle-state)
file (tm/file (:situation edited))
path [:data :pages-index (:id (thf/current-page file)) :objects id]
check (fn [file]
(try (oracle/check (tm/with-file (:situation edited) file) (:model edited)) nil
(catch #?(:clj Exception :cljs js/Error) error (:invariant (ex-data error)))))
main (tm/shape-by-id (:situation initial) (:shape-ref (:shape resolved)))
dropped (fn [action value]
(try (oracle/check-requested-grid main main action value) nil
(catch #?(:clj Exception :cljs js/Error) error (:invariant (ex-data error)))))]
(t/is (= #{:layout-grid-dir :layout-grid-cells} (set (keys (get (:model edited) id)))))
(t/is (= :I9 (check (assoc-in file (conj path :layout-grid-dir) :row))))
(t/is (= :I3 (check (update-in file (conj path :touched) disj :layout-grid-dir))))
(t/is (= :I3 (check (update-in file (conj path :touched) disj :layout-grid-cells))))
(t/is (= :I9 (dropped :direction :column)))))
(t/deftest detaching-an-outer-copy-remaps-inherited-grid-cell-intent
(let [result (cg/run {:world [1 1 {:layout :grid :layout-children true :grid-square true}]
:ops [[:grid-cell 0 0 0 1] [:detach 0] [:undo] [:redo]]})]
(t/is (= 1 (get-in result [:coverage :random :grid-cell :changed])))
(t/is (= 1 (get-in result [:coverage :random :detach :changed])))))
(t/deftest grid-intent-rejects-corrupt-values-members-and-missing-groups
(let [initial (cg/initial-state [1 0 {:layout :grid :layout-children true :grid-square true}])
resolved (cg/resolve-op (:situation initial) {} [:grid-cell 0 0 0 1])
id (get-in resolved [:shape :id])
cell-id (:id (first (sort-by (juxt :row :column) (vals (get-in resolved [:shape :layout-grid-cells])))))
edited (-> initial (cg/step [:grid-cell 0 0 0 1]) cg/settle-state)
situation (:situation edited)
file (tm/file situation)
path [:data :pages-index (:id (thf/current-page file)) :objects id]
invariant (fn [file]
(try (oracle/check (tm/with-file situation file) (:model edited)) nil
(catch #?(:clj Exception :cljs js/Error) error (:invariant (ex-data error)))))]
(t/is (= :I9 (invariant (assoc-in file (into path [:layout-grid-cells cell-id :align-self]) :start))))
(t/is (= :I9 (invariant (assoc-in file (into path [:layout-grid-cells cell-id :justify-self]) :end))))
(t/is (= :I9 (invariant (assoc-in file (into path [:layout-grid-cells cell-id :shapes]) []))))
(t/is (= :I4 (invariant (update-in file (conj path :shapes) #(vec (reverse %))))))
(t/is (= :I3 (invariant (update-in file (conj path :touched) disj :layout-grid-cells))))))
(t/deftest nesting-a-copy-alongside-a-hidden-layout-child-stays-settled
(t/is (map? (cg/run {:world [3 0 {:text true :text-styles true :layout :flex}]
:ops [[:delete 9 9 1 2] [:text 5 9 1 3] [:style 3 1 1]

View File

@ -9,6 +9,7 @@
[app.common.test-helpers.composable.comp.undo-check :as uc]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.composable.gen :as cg]
[app.common.test-helpers.composable.grid :as grid]
[app.common.test-helpers.composable.oracle :as oracle]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.normalize :as thn]
@ -23,11 +24,14 @@
(defn- live-model
[state live]
(let [by-path (into {} (map (juxt :path (comp :id :shape))) (cg/forest live))]
(let [by-path (into {} (map (juxt :path (comp :id :shape))) (cg/forest live))
ids-map (into {} (keep (fn [{:keys [path shape]}]
(when-let [id (get by-path path)] [(:id shape) id])))
(cg/forest (:situation state)))]
(into {}
(keep (fn [{:keys [path shape]}]
(when-let [groups (get (:model state) (:id shape))]
(when-let [id (get by-path path)] [id groups]))))
(when-let [id (get by-path path)] [id (grid/remap-groups groups ids-map)]))))
(cg/forest (:situation state)))))
(defn- ^:async replay-program
@ -61,6 +65,10 @@
(:objects (cthf/current-page (tm/file next-live)))
(tm/shape-by-id next-live (get-in actual [:shape :id]))
(cg/requested-geometry live actual (first tuple))))
(when (and (:op actual) (#{:grid-cell :grid-direction} (first tuple)))
(oracle/check-requested-grid
(tm/shape-by-id next-live (get-in actual [:shape :id])) (:shape actual)
(get-in actual [:op :action]) (get-in actual [:op :value])))
(try
(oracle/check next-live (live-model next-state next-live))
(catch :default error
@ -88,6 +96,22 @@
rx/timer mock/timer-mock}
(await (replay-program program)))))
(t/deftest ^:async authored-grid-histories-use-independent-cell-expectations
(doseq [ops [[[:grid-direction 0 0 1 1] [:grid-direction 0 0 1 0]
[:grid-direction 0 0 0 0] [:update-main 0]
[:grid-direction 0 0 0 1] [:reset 0] [:undo] [:redo]]
[[:grid-cell 0 0 0 1] [:grid-cell 0 1 1 0] [:grid-cell 0 0 2 0]
[:update-main 0] [:grid-cell 0 1 2 1] [:reset 0] [:undo] [:redo]]
[[:grid-cell 0 0 0 1] [:duplicate 0] [:undo] [:redo] [:update-main 0]]
[[:grid-direction 0 0 1 1] [:add-child 0] [:undo] [:redo]]
[[:grid-cell 0 0 0 1] [:remove-child 0] [:undo] [:redo]]
[[:grid-cell 0 0 0 1] [:detach 0] [:undo] [:redo]]]]
(await (replay {:world [1 0 {:layout :grid :layout-children true :grid-square true}] :ops ops}))))
(t/deftest ^:async detaching-an-outer-copy-remaps-inherited-grid-cell-intent
(await (replay {:world [1 1 {:layout :grid :layout-children true :grid-square true}]
:ops [[:grid-cell 0 0 0 1] [:detach 0] [:undo] [:redo]]})))
(t/deftest ^:async layout-container-histories-survive-component-operations
(doseq [[property layouts] [[20 [:flex]] [21 [:flex]] [22 [:flex :grid]] [23 [:flex :grid]]
[24 [:flex :grid]] [25 [:flex :grid]] [26 [:grid]]]

View File

@ -140,6 +140,16 @@
built from `situation` and from the same helpers the common node uses."
[op situation]
(cond
(instance? n/EditGrid op)
(let [{:keys [target action value]} op
id (tm/target-shape-id situation target)
shape (tm/shape-by-id situation id)]
(case action
:direction [(dwsl/change-grid-direction [id] value)]
:cell (let [cell (first (filter #(= [(:row value) (:column value)] [(:row %) (:column %)])
(vals (:layout-grid-cells shape))))]
[(dwsl/update-grid-cells id [(:id cell)] (:props value))])))
(instance? n/ChangeProperty op)
(let [{:keys [target property value]} op
id (tm/target-shape-id situation target)]