diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 3a87e923c4..3afa807ead 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -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))) diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc index 30d6067889..8cb30231d8 100644 --- a/common/src/app/common/logic/shapes.cljc +++ b/common/src/app/common/logic/shapes.cljc @@ -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?] diff --git a/common/test/common_tests/logic/variants_reparent_test.cljc b/common/test/common_tests/logic/variants_reparent_test.cljc new file mode 100644 index 0000000000..5ff04f9153 --- /dev/null +++ b/common/test/common_tests/logic/variants_reparent_test.cljc @@ -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'))) diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 884c9a7356..d28a8adb41 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -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 diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 447a192a7d..6ddf66ddc8 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -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)) diff --git a/frontend/src/app/plugins/flex.cljs b/frontend/src/app/plugins/flex.cljs index 57bf9f1953..0b5f9243b2 100644 --- a/frontend/src/app/plugins/flex.cljs +++ b/frontend/src/app/plugins/flex.cljs @@ -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) diff --git a/frontend/src/app/plugins/grid.cljs b/frontend/src/app/plugins/grid.cljs index f15980bf2a..8860aa5d58 100644 --- a/frontend/src/app/plugins/grid.cljs +++ b/frontend/src/app/plugins/grid.cljs @@ -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]}))))))) diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index aea764dc28..408cf84b25 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -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) diff --git a/frontend/src/app/plugins/utils.cljs b/frontend/src/app/plugins/utils.cljs index 22f57d45fd..581a41afe2 100644 --- a/frontend/src/app/plugins/utils.cljs +++ b/frontend/src/app/plugins/utils.cljs @@ -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)))) diff --git a/frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs b/frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs index 7dca8b7af8..3d05d8abc1 100644 --- a/frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs +++ b/frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs @@ -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))))))))