mirror of
https://github.com/penpot/penpot.git
synced 2026-11-03 00:20:41 +00:00
🐛 Fix variant reparent generates referential-integrity problem (#12706)
* 🐛 Fix variant reparent generates referential-integrity problem * ✨ Mr changes
This commit is contained in:
parent
11c2292ac6
commit
57f4fc19ff
@ -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)))
|
||||
|
||||
@ -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?]
|
||||
|
||||
316
common/test/common_tests/logic/variants_reparent_test.cljc
Normal file
316
common/test/common_tests/logic/variants_reparent_test.cljc
Normal 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')))
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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]})))))))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))))
|
||||
|
||||
@ -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))))))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user