🐛 Fix variant reparent generates referential-integrity problem (#12706)

* 🐛 Fix variant reparent generates referential-integrity problem

* ✨ Mr changes
This commit is contained in:
Pablo Alba 2026-10-30 21:42:14 +01:00 committed by GitHub
parent 11c2292ac6
commit 57f4fc19ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 600 additions and 106 deletions

View File

@ -3182,7 +3182,8 @@
(cond-> (ctsl/grid-layout? obj)
(ctsl/remap-grid-cells ids-map))
(cond-> (ctk/is-variant-container? parent)
(cond-> (and (ctk/is-variant-container? parent)
(ctk/main-instance? obj))
(assoc :variant-id parent-id))
(cond-> (and (ctk/is-variant? obj) (not (ctk/is-variant-container? parent)))

View File

@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.shapes :as gsh]
[app.common.logic.variant-properties :as clvp]
[app.common.types.component :as ctk]
@ -450,6 +451,18 @@
[all-parents changes])))
(defn- find-empty-variant-containers
[objects excluded-parent-id ids parent-ids]
(let [ids (set ids)]
(reduce (fn [to-delete candidate-id]
(let [parent (get objects candidate-id)]
(if (and (ctk/is-variant-container? parent)
(empty? (remove ids (:shapes parent))))
(conj to-delete (:id parent))
to-delete)))
#{}
(remove #(= % excluded-parent-id) parent-ids))))
(defn generate-relocate
[changes parent-id to-index ids & {:keys [cell ignore-parents?]}]
(let [objects (pcb/get-objects changes)
@ -553,128 +566,146 @@
;; Parents that are a variant-container that becomes empty
empty-variant-cont (reduce
(fn [to-delete parent-id]
(let [parent (get objects parent-id)]
(if (and (ctk/is-variant-container? parent)
(empty? (remove (set ids) (:shapes parent))))
(conj to-delete (:id parent))
to-delete)))
#{}
(remove #(= % parent-id) all-parents))]
empty-variant-cont (find-empty-variant-containers objects parent-id ids all-parents)]
(-> changes
;; Remove layout-item properties and tokens when moving a shape outside a layout
(cond-> (not (ctl/any-layout? parent))
(-> (pcb/update-shapes ids ctl/remove-layout-item-data)
(pcb/update-shapes ids cto/unapply-layout-item-tokens)))
(if (and (ctk/is-variant-container? parent)
(not (every? ctk/main-instance? shapes)))
changes
(-> changes
;; Remove layout-item properties and tokens when moving a shape outside a layout
(cond-> (not (ctl/any-layout? parent))
(-> (pcb/update-shapes ids ctl/remove-layout-item-data)
(pcb/update-shapes ids cto/unapply-layout-item-tokens)))
;; Remove the hide in viewer flag
(cond-> (and (not= uuid/zero parent-id) (cfh/frame-shape? parent))
(pcb/update-shapes ids #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true))))
;; Remove the hide in viewer flag
(cond-> (and (not= uuid/zero parent-id) (cfh/frame-shape? parent))
(pcb/update-shapes ids #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true))))
;; Remove the swap slots if it is moving to a different component
(pcb/update-shapes
child-heads-ids
(fn [shape]
(cond-> shape
(not= component-main-parent (ctn/find-component-main objects shape false))
(ctk/remove-swap-slot))))
;; Remove the swap slots if it is moving to a different component
(pcb/update-shapes
child-heads-ids
(fn [shape]
(cond-> shape
(not= component-main-parent (ctn/find-component-main objects shape false))
(ctk/remove-swap-slot))))
;; Remove component-root property when moving a shape inside a component
(cond-> (ctn/get-instance-root objects parent)
(pcb/update-shapes children-ids #(dissoc % :component-root)))
;; Remove component-root property when moving a shape inside a component
(cond-> (ctn/get-instance-root objects parent)
(pcb/update-shapes children-ids #(dissoc % :component-root)))
;; Add component-root property when moving a component outside a component
(cond-> (not (ctn/get-instance-root objects parent))
(pcb/update-shapes child-heads-ids #(assoc % :component-root true)))
;; Add component-root property when moving a component outside a component
(cond-> (not (ctn/get-instance-root objects parent))
(pcb/update-shapes child-heads-ids #(assoc % :component-root true)))
;; Remove variant info and rename when moving outside a variant-container
(cond-> (not (ctk/is-variant-container? parent))
(clvp/generate-make-shapes-no-variant variant-shapes))
;; Remove variant info and rename when moving outside a variant-container
(cond-> (not (ctk/is-variant-container? parent))
(clvp/generate-make-shapes-no-variant variant-shapes))
;; Add variant info and rename when moving into a different variant-container
(cond-> (ctk/is-variant-container? parent)
(clvp/generate-make-shapes-variant child-heads parent))
;; Add variant info and rename when moving into a different variant-container
(cond-> (ctk/is-variant-container? parent)
(clvp/generate-make-shapes-variant child-heads parent))
;; Move the shapes
(pcb/change-parent parent-id
shapes
to-index)
;; Move the shapes
(pcb/change-parent parent-id
shapes
to-index)
;; Remove empty groups
(pcb/remove-objects groups-to-delete)
;; Remove empty groups
(pcb/remove-objects groups-to-delete)
;; Unmask groups whose mask have moved outside
(pcb/update-shapes groups-to-unmask
(fn [shape]
(assoc shape :masked-group false)))
;; Unmask groups whose mask have moved outside
(pcb/update-shapes groups-to-unmask
(fn [shape]
(assoc shape :masked-group false)))
;; Detach shapes moved out of their component
(pcb/update-shapes shapes-to-detach ctk/detach-shape)
;; Detach shapes moved out of their component
(pcb/update-shapes shapes-to-detach ctk/detach-shape)
;; Make non root a component moved inside another one
(pcb/update-shapes shapes-to-deroot
(fn [shape]
(assoc shape :component-root nil)))
;; Make non root a component moved inside another one
(pcb/update-shapes shapes-to-deroot
(fn [shape]
(assoc shape :component-root nil)))
;; Make root a subcomponent moved outside its parent component
(pcb/update-shapes shapes-to-reroot
(fn [shape]
(assoc shape :component-root true)))
;; Make root a subcomponent moved outside its parent component
(pcb/update-shapes shapes-to-reroot
(fn [shape]
(assoc shape :component-root true)))
;; Reset constraints depending on the new parent
(pcb/update-shapes ids
(fn [shape]
(let [frame-id (if (= (:type parent) :frame)
(:id parent)
(:frame-id parent))
moved-shape (assoc shape
:parent-id parent-id
:frame-id frame-id)]
(assoc shape
:constraints-h (gsh/default-constraints-h moved-shape)
:constraints-v (gsh/default-constraints-v moved-shape))))
{:ignore-touched true})
;; Reset constraints depending on the new parent
(pcb/update-shapes ids
(fn [shape]
(let [frame-id (if (= (:type parent) :frame)
(:id parent)
(:frame-id parent))
moved-shape (assoc shape
:parent-id parent-id
:frame-id frame-id)]
(assoc shape
:constraints-h (gsh/default-constraints-h moved-shape)
:constraints-v (gsh/default-constraints-v moved-shape))))
{:ignore-touched true})
;; Fix the sizing when moving a shape
(pcb/update-shapes parents
(fn [parent]
(if (ctl/flex-layout? parent)
(cond-> parent
(ctl/change-h-sizing? (:id parent) objects (:shapes parent))
(assoc :layout-item-h-sizing :fix)
;; Fix the sizing when moving a shape
(pcb/update-shapes parents
(fn [parent]
(if (ctl/flex-layout? parent)
(cond-> parent
(ctl/change-h-sizing? (:id parent) objects (:shapes parent))
(assoc :layout-item-h-sizing :fix)
(ctl/change-v-sizing? (:id parent) objects (:shapes parent))
(assoc :layout-item-v-sizing :fix))
parent)))
(ctl/change-v-sizing? (:id parent) objects (:shapes parent))
(assoc :layout-item-v-sizing :fix))
parent)))
;; Change the grid cell in a grid layout
(cond-> (ctl/grid-layout? objects parent-id)
(-> (pcb/update-shapes
[parent-id]
(fn [frame objects]
(let [[row column] cell]
(-> frame
;; Assign the cell when pushing into a specific grid cell
(cond-> (some? cell)
(-> (ctl/free-cell-shapes ids)
(ctl/push-into-cell ids row column)
(ctl/assign-cells objects)))
(ctl/assign-cell-positions objects))))
{:with-objects? true})
(pcb/reorder-grid-children [parent-id])))
;; Change the grid cell in a grid layout
(cond-> (ctl/grid-layout? objects parent-id)
(-> (pcb/update-shapes
[parent-id]
(fn [frame objects]
(let [[row column] cell]
(-> frame
;; Assign the cell when pushing into a specific grid cell
(cond-> (some? cell)
(-> (ctl/free-cell-shapes ids)
(ctl/push-into-cell ids row column)
(ctl/assign-cells objects)))
(ctl/assign-cell-positions objects))))
{:with-objects? true})
(pcb/reorder-grid-children [parent-id])))
;; If parent locked, lock the added shapes
(cond-> (:blocked parent)
(pcb/update-shapes ids #(assoc % :blocked true)))
;; If parent locked, lock the added shapes
(cond-> (:blocked parent)
(pcb/update-shapes ids #(assoc % :blocked true)))
;; Resize parent containers that need to
(pcb/resize-parents parents)
;; Resize parent containers that need to
(pcb/resize-parents parents)
;; Remove parents when are a variant-container that becomes empty
(cond-> (seq empty-variant-cont)
(#(second (generate-delete-shapes % empty-variant-cont {})))))))
;; Remove parents when are a variant-container that becomes empty
(cond-> (seq empty-variant-cont)
(#(second (generate-delete-shapes % empty-variant-cont {}))))))))
(defn generate-move-shapes-into-frame
"Reparent `shapes` into `frame-id`, stripping variant records when a
variant main leaves its container. The builder must already have
objects and library data."
[changes frame-id shapes objects remove-layout-data?]
(let [parent-id (dm/get-in objects [frame-id :parent-id])
ids (remove #(= % parent-id) shapes)
to-move (->> ids
(map (d/getf objects))
(filter some?))
new-parent (get objects frame-id)
variant-shapes (filter ctk/is-variant? to-move)
all-parents (into #{frame-id} (map #(cfh/get-parent-id objects %)) ids)
empty-variant-cont (find-empty-variant-containers objects frame-id ids all-parents)]
(if (empty? to-move)
changes
(-> changes
(cond-> (not (ctk/is-variant-container? new-parent))
(clvp/generate-make-shapes-no-variant variant-shapes))
(cfsh/prepare-move-shapes-into-frame frame-id shapes objects remove-layout-data?)
(cond-> (seq empty-variant-cont)
(#(second (generate-delete-shapes % empty-variant-cont {}))))))))
(defn change-show-in-viewer
[shape hide?]

View File

@ -0,0 +1,316 @@
;; 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 common-tests.logic.variants-reparent-test
(:require
[app.common.files.changes-builder :as pcb]
[app.common.logic.shapes :as cls]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.test-helpers.variants :as thv]
[app.common.types.component :as ctk]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
(defn- relocate
[file parent-id ids & {:keys [to-index] :or {to-index 0}}]
(let [page (thf/current-page file)]
(-> (pcb/empty-changes nil)
(pcb/with-page-id (:id page))
(pcb/with-objects (:objects page))
(pcb/with-library-data (:data file))
(cls/generate-relocate parent-id to-index (set ids)))))
(defn- move-into-frame
[file frame-id ids]
(let [page (thf/current-page file)
objects (:objects page)]
(-> (pcb/empty-changes nil (:id page))
(pcb/with-objects objects)
(pcb/with-library-data (:data file))
(cls/generate-move-shapes-into-frame frame-id ids objects true))))
(defn- property-names
[component]
(mapv :name (:variant-properties component)))
(defn- strip-component-times
"Undo restamps :modified-at on components when mains are updated.
Compare file data without that clock."
[data]
(update data :components update-vals #(dissoc % :modified-at)))
(t/deftest test-relocate-plain-shape-onto-container-is-rejected
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(tho/add-rect :rect1))
container (ths/get-shape file :v01)
rect (ths/get-shape file :rect1)
children (:shapes container)
changes (relocate file (:id container) [(:id rect)])
file' (thf/apply-changes file changes)
rect' (ths/get-shape file' :rect1)
container' (ths/get-shape file' :v01)]
(t/is (empty? (:redo-changes changes)))
(t/is (= (:parent-id rect) (:parent-id rect')))
(t/is (= (:constraints-h rect) (:constraints-h rect')))
(t/is (= (:constraints-v rect) (:constraints-v rect')))
(t/is (= children (:shapes container')))
(thf/validate-file! file')))
(t/deftest test-relocate-component-copy-onto-container-is-rejected
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(tho/add-simple-component-with-copy :c03 :m03 :r03 :copy03))
container (ths/get-shape file :v01)
copy (ths/get-shape file :copy03)
comp (thc/get-component file :c03)
children (:shapes container)
changes (relocate file (:id container) [(:id copy)])
file' (thf/apply-changes file changes)
copy' (ths/get-shape file' :copy03)
comp' (thc/get-component file' :c03)
container' (ths/get-shape file' :v01)]
(t/is (empty? (:redo-changes changes)))
(t/is (= (:parent-id copy) (:parent-id copy')))
(t/is (nil? (:variant-id copy')))
(t/is (= (:variant-id comp) (:variant-id comp')))
(t/is (= (:variant-properties comp) (:variant-properties comp')))
(t/is (= children (:shapes container')))
(thf/validate-file! file')))
(t/deftest test-relocate-mixed-selection-onto-container-is-rejected
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(tho/add-rect :rect1)
(tho/add-simple-component :c03 :m03 :r03))
container (ths/get-shape file :v01)
rect (ths/get-shape file :rect1)
main (ths/get-shape file :m03)
changes (relocate file (:id container) [(:id rect) (:id main)])
file' (thf/apply-changes file changes)
rect' (ths/get-shape file' :rect1)
main' (ths/get-shape file' :m03)]
(t/is (empty? (:redo-changes changes)))
(t/is (= (:parent-id rect) (:parent-id rect')))
(t/is (= (:parent-id main) (:parent-id main')))
(thf/validate-file! file')))
(t/deftest test-relocate-main-into-container-becomes-variant
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(tho/add-simple-component :c03 :m03 :r03))
container (ths/get-shape file :v01)
main (ths/get-shape file :m03)
sibling (thc/get-component file :c01)
changes (relocate file (:id container) [(:id main)])
file' (thf/apply-changes file changes)
file'' (thf/apply-undo-changes file' changes)
main' (ths/get-shape file' :m03)
comp' (thc/get-component file' :c03)
sibling' (thc/get-component file' :c01)
container' (ths/get-shape file' :v01)]
(t/is (= (:id container') (:parent-id main')))
(t/is (= (:id container') (:variant-id main')))
(t/is (= (:id container') (:variant-id comp')))
(t/is (some? (:variant-name main')))
(t/is (= (property-names sibling') (property-names comp')))
(t/is (= (property-names sibling) (property-names sibling')))
(t/is (= (strip-component-times (:data file))
(strip-component-times (:data file''))))
(thf/validate-file! file')))
(t/deftest test-relocate-reorder-mains-does-not-rebuild-properties
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02))
container (ths/get-shape file :v01)
m01 (ths/get-shape file :m01)
m02 (ths/get-shape file :m02)
c01 (thc/get-component file :c01)
c02 (thc/get-component file :c02)
changes (relocate file (:id container) [(:id m01)] :to-index 0)
file' (thf/apply-changes file changes)
file'' (thf/apply-undo-changes file' changes)
m01' (ths/get-shape file' :m01)
m02' (ths/get-shape file' :m02)
c01' (thc/get-component file' :c01)
c02' (thc/get-component file' :c02)]
(t/is (= (:variant-name m01) (:variant-name m01')))
(t/is (= (:variant-name m02) (:variant-name m02')))
(t/is (= (:variant-properties c01) (:variant-properties c01')))
(t/is (= (:variant-properties c02) (:variant-properties c02')))
(t/is (= (strip-component-times (:data file))
(strip-component-times (:data file''))))
(thf/validate-file! file')))
(t/deftest test-relocate-all-mains-within-container-keeps-container
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02))
container (ths/get-shape file :v01)
m01 (ths/get-shape file :m01)
m02 (ths/get-shape file :m02)
changes (relocate file (:id container) [(:id m01) (:id m02)])
file' (thf/apply-changes file changes)
container' (ths/get-shape file' :v01)]
(t/is (= (:id container) (:id container')))
(t/is (= (set (:shapes container)) (set (:shapes container'))))
(thf/validate-file! file')))
(t/deftest test-relocate-main-out-of-container-clears-variant-data
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(tho/add-frame :board1))
board (ths/get-shape file :board1)
m01 (ths/get-shape file :m01)
changes (relocate file (:id board) [(:id m01)])
file' (thf/apply-changes file changes)
file'' (thf/apply-undo-changes file' changes)
m01' (ths/get-shape file' :m01)
c01' (thc/get-component file' :c01)
container' (ths/get-shape file' :v01)]
(t/is (= (:id board) (:parent-id m01')))
(t/is (nil? (:variant-id m01')))
(t/is (nil? (:variant-name m01')))
(t/is (nil? (:variant-id c01')))
(t/is (nil? (:variant-properties c01')))
(t/is (some? container'))
(t/is (= (strip-component-times (:data file))
(strip-component-times (:data file''))))
(thf/validate-file! file')))
(t/deftest test-relocate-last-mains-out-deletes-empty-container
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(tho/add-frame :board1))
board (ths/get-shape file :board1)
m01 (ths/get-shape file :m01)
m02 (ths/get-shape file :m02)
changes (relocate file (:id board) [(:id m01) (:id m02)])
file' (thf/apply-changes file changes)
file'' (thf/apply-undo-changes file' changes)
container' (ths/get-shape file' :v01)
m01' (ths/get-shape file' :m01)
m02' (ths/get-shape file' :m02)
c01' (thc/get-component file' :c01)
c02' (thc/get-component file' :c02)]
(t/is (nil? container'))
(t/is (some? m01'))
(t/is (some? m02'))
(t/is (= (:id board) (:parent-id m01')))
(t/is (= (:id board) (:parent-id m02')))
(t/is (nil? (:variant-id m01')))
(t/is (nil? (:variant-id m02')))
(t/is (nil? (:variant-name m01')))
(t/is (nil? (:variant-name m02')))
(t/is (some? c01'))
(t/is (some? c02'))
(t/is (nil? (:variant-id c01')))
(t/is (nil? (:variant-id c02')))
(t/is (nil? (:variant-properties c01')))
(t/is (nil? (:variant-properties c02')))
(t/is (= (strip-component-times (:data file))
(strip-component-times (:data file''))))
(thf/validate-file! file')))
(t/deftest test-board-draw-strips-variant-data-when-main-leaves
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(tho/add-frame :board1))
board (ths/get-shape file :board1)
m01 (ths/get-shape file :m01)
changes (move-into-frame file (:id board) [(:id m01)])
file' (thf/apply-changes file changes)
file'' (thf/apply-undo-changes file' changes)
m01' (ths/get-shape file' :m01)
c01' (thc/get-component file' :c01)
container' (ths/get-shape file' :v01)]
(t/is (= (:id board) (:parent-id m01')))
(t/is (nil? (:variant-id m01')))
(t/is (nil? (:variant-name m01')))
(t/is (nil? (:variant-id c01')))
(t/is (nil? (:variant-properties c01')))
(t/is (some? container'))
(t/is (= (strip-component-times (:data file))
(strip-component-times (:data file''))))
(thf/validate-file! file')))
(t/deftest test-board-draw-last-main-deletes-empty-container
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(tho/add-frame :board1))
board (ths/get-shape file :board1)
m01 (ths/get-shape file :m01)
m02 (ths/get-shape file :m02)
file* (thf/apply-changes file (move-into-frame file (:id board) [(:id m01)]))
changes (move-into-frame file* (:id board) [(:id m02)])
file' (thf/apply-changes file* changes)
container' (ths/get-shape file' :v01)
m01' (ths/get-shape file' :m01)
m02' (ths/get-shape file' :m02)
c01' (thc/get-component file' :c01)
c02' (thc/get-component file' :c02)]
(t/is (nil? container'))
(t/is (some? m01'))
(t/is (some? m02'))
(t/is (= (:id board) (:parent-id m01')))
(t/is (= (:id board) (:parent-id m02')))
(t/is (nil? (:variant-id m01')))
(t/is (nil? (:variant-id m02')))
(t/is (nil? (:variant-name m01')))
(t/is (nil? (:variant-name m02')))
(t/is (some? c01'))
(t/is (some? c02'))
(t/is (nil? (:variant-id c01')))
(t/is (nil? (:variant-id c02')))
(t/is (nil? (:variant-properties c01')))
(t/is (nil? (:variant-properties c02')))
(thf/validate-file! file')))
(t/deftest test-board-draw-undo-restores-emptied-container
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(tho/add-frame :board1))
board (ths/get-shape file :board1)
m01 (ths/get-shape file :m01)
m02 (ths/get-shape file :m02)
changes* (move-into-frame file (:id board) [(:id m01)])
file* (thf/apply-changes file changes*)
changes (move-into-frame file* (:id board) [(:id m02)])
file' (thf/apply-changes file* changes)
file'' (thf/apply-undo-changes file' changes)
file''' (thf/apply-undo-changes file'' changes*)]
(t/is (= (strip-component-times (:data file*))
(strip-component-times (:data file''))))
(t/is (= (strip-component-times (:data file))
(strip-component-times (:data file'''))))))
(t/deftest test-duplicate-variant-main-keeps-variant-id
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02))
container (ths/get-shape file :v01)
file' (tho/duplicate-shape file :m01)
objects' (:objects (thf/current-page file'))
variant-mains (->> (vals objects')
(filter ctk/main-instance?)
(filter #(= (:id container) (:variant-id %))))]
(t/is (= 3 (count variant-mains)))
(thf/validate-file! file')))

View File

@ -77,6 +77,7 @@
[common-tests.logic.text-touched-test]
[common-tests.logic.token-apply-test]
[common-tests.logic.token-test]
[common-tests.logic.variants-reparent-test]
[common-tests.logic.variants-switch-test]
[common-tests.logic.variants-test]
[common-tests.math-test]
@ -184,6 +185,7 @@
'common-tests.logic.text-touched-test
'common-tests.logic.token-apply-test
'common-tests.logic.token-test
'common-tests.logic.variants-reparent-test
'common-tests.logic.variants-switch-test
'common-tests.logic.variants-test
'common-tests.math-test

View File

@ -336,10 +336,11 @@
(remove #(dm/get-in objects [% :blocked]))
(cfh/order-by-indexed-shapes objects))
data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))
changes (cfsh/prepare-move-shapes-into-frame changes frame-id shapes objects true)]
(pcb/with-objects objects)
(pcb/with-library-data data)
(cls/generate-move-shapes-into-frame frame-id shapes objects true))]
(if (some? changes)
(rx/of (dch/commit-changes changes))

View File

@ -358,6 +358,9 @@
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :appendChild "Plugin doesn't have 'content:write' permission")
(u/non-main-into-variant-container? shape child)
(u/not-valid plugin-id :appendChild "Only main instances can be added to a variant container")
:else
(st/emit!
(dwsh/relocate-shapes #{child-id} id index)

View File

@ -574,6 +574,9 @@
(u/changes-component-copy-structure? objects shape child-shape)
(u/not-valid plugin-id :appendChild "Cannot change the structure of a component copy")
(u/non-main-into-variant-container? shape child)
(u/not-valid plugin-id :appendChild "Only main instances can be added to a variant container")
:else
(st/emit! (dwt/move-shapes-to-frame #{child-id} id nil [row column])
(ptk/data-event :layout/update {:ids [id]})))))))

View File

@ -1263,6 +1263,9 @@
(u/changes-component-copy-structure? objects shape child-shape)
(u/not-valid plugin-id :appendChild "Cannot change the structure of a component copy")
(u/non-main-into-variant-container? shape child)
(u/not-valid plugin-id :appendChild "Only main instances can be added to a variant container")
:else
(st/emit!
(dwsh/relocate-shapes #{child-id} id index)
@ -1302,6 +1305,9 @@
(u/changes-component-copy-structure? objects shape child-shape)
(u/not-valid plugin-id :insertChild "Cannot change the structure of a component copy")
(u/non-main-into-variant-container? shape child)
(u/not-valid plugin-id :insertChild "Only main instances can be added to a variant container")
:else
(st/emit!
(dwsh/relocate-shapes #{child-id} id index)

View File

@ -604,3 +604,10 @@
[p]
(when-let [shape (proxy->shape p)]
(ctk/main-instance? shape)))
(defn non-main-into-variant-container?
"True when `parent` is a variant container and `child` (a shape proxy)
is not a main instance."
[parent child]
(and (ctk/is-variant-container? parent)
(not (is-main-component-proxy? child))))

View File

@ -18,6 +18,8 @@
[app.main.data.workspace.variants :as dwv]
[app.main.store :as st]
[app.plugins.api :as api]
[app.plugins.flex :as flex]
[app.plugins.grid :as grid]
[app.plugins.public-utils :as public-utils]
[app.plugins.register :as r]
[app.plugins.shape :as shape]
@ -410,3 +412,125 @@
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :combineAsVariants "Plugin doesn't have 'content:write' permission"]
(first @errors)))))))
;; ---------------------------------------------------------------------------
;; Variant container append / insert
;; ---------------------------------------------------------------------------
(def ^:private variant-append-msg
"Only main instances can be added to a variant container")
(defn- variant-append-objects
[child-attrs]
(let [container-id (uuid/next)
child-id (uuid/next)]
{:container-id container-id
:child-id child-id
:objects {container-id {:id container-id
:type :frame
:is-variant-container true
:shapes []}
child-id (merge {:id child-id
:type :rect
:parent-id uuid/zero}
child-attrs)}}))
(defn- with-append-stubs
[objects f]
(let [errors (atom [])
emitted (atom [])]
(with-redefs [u/locate-shape (fn [_file _page id] (get objects id))
;; Multi-arity: CLJS callers hit IFn$_invoke$arity$2; `constantly`
;; only installs arity-variadic and breaks those calls.
u/locate-objects (fn
([] objects)
([_file _page] objects))
u/page-active? (constantly true)
u/natural-child-ordering? (constantly false)
r/check-permission (constantly true)
u/not-valid (mock/stub (fn [pid prop msg]
(swap! errors conj [pid prop msg])))
st/emit! (fn [& args]
(swap! emitted conj args)
nil)]
(f errors emitted))))
(t/deftest append-child-rejects-plain-shape-into-variant-container
(let [{:keys [container-id child-id objects]} (variant-append-objects {})]
(with-append-stubs
objects
(fn [errors emitted]
(let [container (shape/shape-proxy plugin-id (uuid/next) (uuid/next) container-id)
child (shape/shape-proxy plugin-id (uuid/next) (uuid/next) child-id)]
(.appendChild container child)
(t/is (= 1 (count @errors)))
(t/is (= variant-append-msg (last (first @errors))))
(t/is (empty? @emitted)))))))
(t/deftest append-child-rejects-copy-into-variant-container
(let [{:keys [container-id child-id objects]}
(variant-append-objects {:type :frame
:component-id (uuid/next)
:component-root true})]
(with-append-stubs
objects
(fn [errors emitted]
(let [container (shape/shape-proxy plugin-id (uuid/next) (uuid/next) container-id)
child (shape/shape-proxy plugin-id (uuid/next) (uuid/next) child-id)]
(.appendChild container child)
(t/is (= 1 (count @errors)))
(t/is (= variant-append-msg (last (first @errors))))
(t/is (empty? @emitted)))))))
(t/deftest append-child-allows-main-into-variant-container
(let [{:keys [container-id child-id objects]}
(variant-append-objects {:type :frame
:main-instance true
:component-id (uuid/next)
:component-root true})]
(with-append-stubs
objects
(fn [errors emitted]
(let [container (shape/shape-proxy plugin-id (uuid/next) (uuid/next) container-id)
child (shape/shape-proxy plugin-id (uuid/next) (uuid/next) child-id)]
(.appendChild container child)
(t/is (empty? @errors))
(t/is (pos? (count @emitted))))))))
(t/deftest insert-child-rejects-plain-shape-into-variant-container
(let [{:keys [container-id child-id objects]} (variant-append-objects {})]
(with-append-stubs
objects
(fn [errors emitted]
(let [container (shape/shape-proxy plugin-id (uuid/next) (uuid/next) container-id)
child (shape/shape-proxy plugin-id (uuid/next) (uuid/next) child-id)]
(.insertChild container 0 child)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :insertChild variant-append-msg] (first @errors)))
(t/is (empty? @emitted)))))))
(t/deftest flex-append-child-rejects-plain-shape-into-variant-container
(let [{:keys [container-id child-id objects]} (variant-append-objects {})]
(with-append-stubs
objects
(fn [errors emitted]
(with-redefs [flex/shape-proxy? (constantly true)]
(let [layout (flex/flex-layout-proxy plugin-id (uuid/next) (uuid/next) container-id)
child (shape/shape-proxy plugin-id (uuid/next) (uuid/next) child-id)]
(.appendChild layout child)
(t/is (= 1 (count @errors)))
(t/is (= variant-append-msg (last (first @errors))))
(t/is (empty? @emitted))))))))
(t/deftest grid-append-child-rejects-plain-shape-into-variant-container
(let [{:keys [container-id child-id objects]} (variant-append-objects {})]
(with-append-stubs
objects
(fn [errors emitted]
(with-redefs [grid/shape-proxy? (constantly true)]
(let [layout (grid/grid-layout-proxy plugin-id (uuid/next) (uuid/next) container-id)
child (shape/shape-proxy plugin-id (uuid/next) (uuid/next) child-id)]
(.appendChild layout child 0 0)
(t/is (= 1 (count @errors)))
(t/is (= variant-append-msg (last (first @errors))))
(t/is (empty? @emitted))))))))