From f5f915778620841fec2af8dee2daab61599e1ac2 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 10 Sep 2025 17:54:53 +0200 Subject: [PATCH] :bug: Fix paste behavior according to the selected element --- CHANGES.md | 1 + common/src/app/common/files/validate.cljc | 18 +- common/src/app/common/logic/libraries.cljc | 94 ++- .../app/common/test_helpers/components.cljc | 4 + common/src/app/common/types/container.cljc | 44 +- .../app/main/data/workspace/clipboard.cljs | 11 +- .../logic/pasting_in_containers_test.cljs | 669 ++++++++++++++++++ frontend/test/frontend_tests/runner.cljs | 2 + 8 files changed, 792 insertions(+), 51 deletions(-) create mode 100644 frontend/test/frontend_tests/logic/pasting_in_containers_test.cljs diff --git a/CHANGES.md b/CHANGES.md index 6913968dfe..94c4d436a3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ - Fix selection problems when devtools open [Taiga #11950](https://tree.taiga.io/project/penpot/issue/11950) - Fix long font names overlap [Taiga #11844](https://tree.taiga.io/project/penpot/issue/11844) +- Fix paste behavior according to the selected element [Taiga #11979](https://tree.taiga.io/project/penpot/issue/11979) ## 2.10.0 (Unreleased) diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index 02887b5f5e..348db903f5 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -543,7 +543,7 @@ ;; mains can't be nested into mains (if (or (= context :not-component) (= context :main-top)) (report-error :nested-main-not-allowed - "Nested main component only allowed inside other component" + "Component main not allowed inside other component" shape file page) (check-shape-main-root-nested shape file page libraries)) @@ -608,6 +608,20 @@ (str/ffmt "Shape % should be a variant" (:id main-component)) main-component file component-page)))) +(defn- check-main-inside-main + [component file] + (let [component-page (ctf/get-component-page (:data file) component) + main-instance (ctst/get-shape component-page (:main-instance-id component)) + main-parents? (->> main-instance + :id + (cfh/get-parents (:objects component-page)) + (some ctk/main-instance?) + boolean)] + (when main-parents? + (report-error :nested-main-not-allowed + "Component main not allowed inside other component" + main-instance file component-page)))) + (defn- check-component "Validate semantic coherence of a component. Report all errors found." [component file] @@ -615,6 +629,8 @@ (report-error :component-nil-objects-not-allowed "Objects list cannot be nil" component file nil)) + (when-not (:deleted component) + (check-main-inside-main component file)) (when (:deleted component) (check-component-duplicate-swap-slot component file) (check-ref-cycles component file)) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 7beec359cb..d7f4d602c0 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -11,6 +11,7 @@ [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] + [app.common.files.variant :as cfv] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.logging :as log] @@ -2514,9 +2515,10 @@ frames))) (defn- duplicate-variant - [changes library component base-pos parent-id page-id] + [changes library component base-pos parent page-id into-new-variant?] (let [component-page (ctpl/get-page (:data library) (:main-instance-page component)) - component-shape (dm/get-in component-page [:objects (:main-instance-id component)]) + objects (:objects component-page) + component-shape (get objects (:main-instance-id component)) orig-pos (gpt/point (:x component-shape) (:y component-shape)) delta (gpt/subtract base-pos orig-pos) new-component-id (uuid/next) @@ -2526,11 +2528,27 @@ new-component-id {:apply-changes-local-library? true :delta delta - :new-variant-id parent-id - :page-id page-id})] + :new-variant-id (if into-new-variant? nil (:id parent)) + :page-id page-id}) + value (when into-new-variant? + (str ctv/value-prefix + (-> (cfv/extract-properties-values (:data library) objects (:id parent)) + last + :value + count + inc)))] + [shape - (-> changes - (pcb/change-parent parent-id [shape]))])) + (cond-> changes + into-new-variant? + (clvp/generate-make-shapes-variant [shape] parent) + + ;; If it has the same parent, update the value of the last property + (and into-new-variant? (= (:variant-id component) (:id parent))) + (clvp/generate-update-property-value new-component-id (-> component :variant-properties count dec) value) + + :always + (pcb/change-parent (:id parent) [shape] 0))])) (defn generate-duplicate-component-change @@ -2542,11 +2560,13 @@ pos (as-> (gsh/move main delta) $ (gpt/point (:x $) (:y $))) + parent (get objects parent-id) + + ;; When we duplicate a variant alone, we will instanciate it ;; When we duplicate a variant along with its variant-container, we will duplicate it in-variant-container? (contains? ids-map (:variant-id main)) - restore-component #(let [{:keys [shape changes]} (prepare-restore-component changes @@ -2559,29 +2579,42 @@ frame-id)] [shape changes]) - [_shape changes] - (if (nil? component) - (restore-component) - (if (and (ctk/is-variant? main) in-variant-container?) - (duplicate-variant changes - (get libraries file-id) - component - pos - parent-id - (:id page)) - (generate-instantiate-component changes - objects - file-id - component-id - pos - page - libraries - main-id - parent-id - frame-id - ids-map - {})))] + [_shape changes] + (cond + (nil? component) + (restore-component) + + (and (ctk/is-variant? main) in-variant-container?) + (duplicate-variant changes + (get libraries file-id) + component + pos + parent + (:id page) + false) + + (ctk/is-variant-container? parent) + (duplicate-variant changes + (get libraries file-id) + component + pos + parent + (:id page) + true) + :else + (generate-instantiate-component changes + objects + file-id + component-id + pos + page + libraries + main-id + parent-id + frame-id + ids-map + {}))] changes)) (defn generate-duplicate-shape-change @@ -2740,7 +2773,8 @@ changes (-> changes (pcb/with-page page) - (pcb/with-objects all-objects)) + (pcb/with-objects all-objects) + (pcb/with-library-data library-data)) changes (->> shapes (reduce #(generate-duplicate-shape-change %1 diff --git a/common/src/app/common/test_helpers/components.cljc b/common/src/app/common/test_helpers/components.cljc index 4e82acd647..bd7aa98e40 100644 --- a/common/src/app/common/test_helpers/components.cljc +++ b/common/src/app/common/test_helpers/components.cljc @@ -72,6 +72,10 @@ [file id] (ctkl/get-component (:data file) id)) +(defn get-components + [file] + (ctkl/components (:data file))) + (defn- set-children-labels! [file shape-label children-labels] (doseq [[label id] diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index 5e030ffb56..751fe5518f 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -7,7 +7,6 @@ (ns app.common.types.container (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -494,29 +493,40 @@ all-main? (every? ctk/main-instance? top-children) + ascendants (cfh/get-parents-with-self objects parent-id) + any-main-ascendant (some ctk/main-instance? ascendants) + any-variant-container-ascendant (some ctk/is-variant-container? ascendants) + + get-variant-id (fn [shape] + (when (:component-id shape) + (-> (get-component-from-shape shape libraries) + :variant-id))) + + descendants (mapcat #(cfh/get-children-with-self objects %) children-ids) + any-variant-container-descendant (some ctk/is-variant-container? descendants) + descendants-variant-ids-set (->> descendants + (map get-variant-id) + set) any-main-descendant (some (fn [shape] (some ctk/main-instance? (cfh/get-children-with-self objects (:id shape)))) - children) + children)] - ;; Are all the top-children a main-instance of a cutted component? - all-comp-cut? - (when all-main? - (->> top-children - (map #(ctkl/get-component (dm/get-in libraries [(:component-file %) :data]) - (:component-id %) - true)) - (every? :deleted)))] (if (or no-changes? (and (not (invalid-structure-for-component? objects parent children pasting? libraries)) - ;; If we are moving into a main component, no descendant can be main - (or (nil? any-main-descendant) (not (ctk/main-instance? parent))) - ;; If we are moving into a variant-container, all the items should be main - ;; so if we are pasting, only allow main instances that are cut-and-pasted - (or (not (ctk/is-variant-container? parent)) - (and (not pasting?) all-main?) - all-comp-cut?))) + ;; If we are moving (not pasting) into a main component, no descendant can be main + (or pasting? (nil? any-main-descendant) (not (ctk/main-instance? parent))) + ;; Don't allow variant-container inside variant container nor main + (or (not any-variant-container-descendant) + (and (not any-variant-container-ascendant) (not any-main-ascendant))) + ;; If the parent is a variant-container, all the items should be main + (or (not (ctk/is-variant-container? parent)) all-main?) + ;; If we are pasting, the parent can't be a "brother" of any of the pasted items, + ;; so not have the same variant-id of any descendant + (or (not pasting?) + (not (ctk/is-variant? parent)) + (not (contains? descendants-variant-ids-set (:variant-id parent)))))) [parent-id (get-frame parent-id)] (recur (:parent-id parent) objects children pasting? libraries)))))) diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 55ae0b371e..251952dde6 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -874,12 +874,17 @@ orig-shapes (map (d/getf all-objects) selected) + children-after (-> (pcb/get-objects changes) + (dm/get-in [parent-id :shapes]) + set) + + ;; At the end of the process, we want to select the new created shapes + ;; that are a direct child of the shape parent-id selected (into (d/ordered-set) (comp (filter add-obj?) - (filter #(contains? selected (:old-id %))) - (map :obj) - (map :id)) + (map (comp :id :obj)) + (filter #(contains? children-after %))) (:redo-changes changes)) changes (cond-> changes diff --git a/frontend/test/frontend_tests/logic/pasting_in_containers_test.cljs b/frontend/test/frontend_tests/logic/pasting_in_containers_test.cljs new file mode 100644 index 0000000000..ac69e1cafd --- /dev/null +++ b/frontend/test/frontend_tests/logic/pasting_in_containers_test.cljs @@ -0,0 +1,669 @@ +;; 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 INC +(ns frontend-tests.logic.pasting-in-containers-test + (:require + [app.common.test-helpers.components :as cthc] + [app.common.test-helpers.compositions :as ctho] + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.ids-map :as cthi] + [app.common.test-helpers.shapes :as cths] + [app.common.test-helpers.variants :as thv] + [app.common.types.component :as ctk] + [app.common.uuid :as uuid] + [app.main.data.workspace :as dw] + [app.main.data.workspace.selection :as dws] + [cljs.test :as t :include-macros true] + [cuerdas.core :as str] + [frontend-tests.helpers.pages :as thp] + [frontend-tests.helpers.state :as ths])) + +(t/use-fixtures :each + {:before thp/reset-idmap!}) + +;; Related .penpot file: common/test/cases/remove-swap-slots.penpot +(defn- setup-file + [] + ;; {:frame-red} [:name Frame1] # [Component :red] + ;; {:frame-blue} [:name Frame1] # [Component :blue] + ;; {:frame-green} [:name Frame1] # [Component :green] + ;; :red-copy-green [:name Frame1] @--> :frame-red + ;; {:frame-b1} [:name Frame1] # [Component :b1] + ;; :blue1 [:name Frame1, :swap-slot-label :red-copy] @--> :frame-blue + ;; :frame-yellow [:name Frame1] + ;; :green-copy [:name Frame1] @--> :frame-green + ;; :blue-copy-in-green-copy [:name Frame1, :swap-slot-label :red-copy-green] @--> :frame-blue + ;; {:frame-b2} [:name Frame1] # [Component :b2] + (-> (cthf/sample-file :file1) + (ctho/add-frame :frame-red {:name "frame-red"}) + (ctho/add-frame :frame-red {:name "frame-red"}) + (ctho/add-frame :frame-blue :name "frame-blue") + (cthc/make-component :blue :frame-blue) + (ctho/add-frame :frame-green :name "frame-green") + (cthc/make-component :green :frame-green) + (cthc/instantiate-component :red :red-copy-green :parent-label :frame-green) + (ctho/add-frame :frame-b1) + (cthc/make-component :b1 :frame-b1) + (ctho/add-frame :frame-yellow :parent-label :frame-b1 :name "frame-yellow") + (cthc/instantiate-component :red :red-copy :parent-label :frame-b1) + (cthc/component-swap :red-copy :blue :blue1) + (cthc/instantiate-component :green :green-copy :parent-label :frame-b1 :children-labels [:red-copy-in-green-copy]) + (cthc/component-swap :red-copy-in-green-copy :blue :blue-copy-in-green-copy) + (ctho/add-frame :frame-b2) + (cthc/make-component :b2 :frame-b2))) + +(defn- find-copied-shape + ([original-shape page parent-id] + (find-copied-shape original-shape page parent-id false)) + ([original-shape page parent-id ignore-label?] + ;; copied shape has the same name, is in the specified parent, and doesn't have a label + ;; for restored components we can ignore the label part + (->> (vals (:objects page)) + (filter #(and (= (:name %) (:name original-shape)) + (= (:parent-id %) parent-id) + (or ignore-label? (str/starts-with? (cthi/label (:id %)) " (cthf/sample-file :file1) + (ctho/add-frame :frame-red {:name "frame-red"}) + (ctho/add-frame :frame-blue {:name "frame-blue"})) + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + frame-red (cths/get-shape file :frame-red) + frame-blue (cths/get-shape file :frame-blue) + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id frame-blue)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id frame-red)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + page' (cthf/current-page file') + frame-red' (cths/get-shape file' :frame-red) + frame-blue' (cths/get-shape file' :frame-blue) + copied-blue1' (find-copied-shape frame-blue' page' (:id frame-red'))] + + ;; ==== Check + (t/is (= (:parent-id copied-blue1') (:id frame-red'))))))))) + +(t/deftest copy-shape-to-component + "Coping a rect into a component results in a copy of the rect inside the component" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (ctho/add-frame :frame-red {:name "frame-red"}) + (cthc/make-component :red :frame-red) + (ctho/add-frame :frame-blue {:name "frame-blue"})) + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + frame-red (cths/get-shape file :frame-red) + frame-blue (cths/get-shape file :frame-blue) + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id frame-blue)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id frame-red)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + page' (cthf/current-page file') + frame-red' (cths/get-shape file' :frame-red) + frame-blue' (cths/get-shape file' :frame-blue) + copied-blue1' (find-copied-shape frame-blue' page' (:id frame-red'))] + + ;; ==== Check + (t/is (= (:parent-id copied-blue1') (:id frame-red'))))))))) + +(t/deftest copy-component-to-frame + "Coping a component c1 into a frame results in a copy of c1 inside the frame" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (ctho/add-frame :frame-red {:name "frame-red"}) + (ctho/add-frame :frame-blue {:name "frame-blue"}) + (cthc/make-component :blue :frame-blue)) + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + frame-red (cths/get-shape file :frame-red) + frame-blue (cths/get-shape file :frame-blue) + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id frame-blue)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id frame-red)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + page' (cthf/current-page file') + frame-red' (cths/get-shape file' :frame-red) + frame-blue' (cths/get-shape file' :frame-blue) + copied-blue1' (find-copied-shape frame-blue' page' (:id frame-red'))] + + ;; ==== Check + (t/is (= (:parent-id copied-blue1') (:id frame-red'))))))))) + +(t/deftest copy-component-to-component + "Coping a component c1 into the main of a component c2 results in a copy of c1 inside c2" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (ctho/add-frame :frame-red {:name "frame-red"}) + (cthc/make-component :red :frame-red) + (ctho/add-frame :frame-blue {:name "frame-blue"}) + (cthc/make-component :blue :frame-blue)) + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + frame-red (cths/get-shape file :frame-red) + frame-blue (cths/get-shape file :frame-blue) + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id frame-blue)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id frame-red)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + page' (cthf/current-page file') + frame-red' (cths/get-shape file' :frame-red) + frame-blue' (cths/get-shape file' :frame-blue) + copied-blue1' (find-copied-shape frame-blue' page' (:id frame-red'))] + + ;; ==== Check + (t/is (not (ctk/main-instance? copied-blue1'))) + (t/is (= (:parent-id copied-blue1') (:id frame-red'))))))))) + +(t/deftest cut-paste-component-to-component + "Cutting a component c1 and pasting it into the main of a component c2 results in a restored main of C1 on root, + because its not allowed to have a main inside a main" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (ctho/add-frame :frame-red {:name "frame-red"}) + (cthc/make-component :red :frame-red) + (ctho/add-frame :frame-blue {:name "frame-blue"}) + (cthc/make-component :blue :frame-blue)) + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + frame-red (cths/get-shape file :frame-red) + frame-blue (cths/get-shape file :frame-blue) + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id frame-blue)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id frame-blue)) + (dw/delete-selected) + (dws/select-shape (:id frame-red)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + page' (cthf/current-page file') + frame-red' (cths/get-shape file' :frame-red) + frame-blue' (cths/get-shape file' :frame-blue) + copied-blue1' (find-copied-shape frame-blue' page' (:id frame-red')) + copied-blue2' (find-copied-shape frame-blue' page' uuid/zero true)] + + ;; ==== Check + (t/is (nil? copied-blue1')) + (t/is (ctk/main-instance? copied-blue2')) + (t/is (= (:parent-id copied-blue2') uuid/zero)))))))) + +(t/deftest copy-variant-container-into-component + "Coping a variant container into the main of a component results in a new variant-container on root, + because its not allowed to have a variant container inside a main" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (ctho/add-frame :frame-red {:name "frame-red"}) + (cthc/make-component :red :frame-red) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + root (cths/get-shape-by-id file uuid/zero) + frame-red (cths/get-shape file :frame-red) + v01 (cths/get-shape file :v01) + + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id v01)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id frame-red)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + root' (cths/get-shape-by-id file' uuid/zero) + frame-red' (cths/get-shape file' :frame-red) + root-children' (->> (:shapes root') + (map #(cths/get-shape-by-id file' %)))] + + ;; ==== Check + ;; The main shape of the component have no children + (t/is (= 0 (count (:shapes frame-red')))) + ;; Root had two children, now have 3 + (t/is (= 2 (count (:shapes root)))) + (t/is (= 3 (count (:shapes root')))) + ;; Two of the children of root are variant-containers + (t/is (= 2 (count (filter ctk/is-variant-container? root-children')))))))))) + +(t/deftest cut-paste-variant-container-into-component + "Cuting and pasting a variant container into the main of a component results in a new variant-container on root, + because its not allowed to have a variant container inside a main" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (ctho/add-frame :frame-red {:name "frame-red"}) + (cthc/make-component :red :frame-red) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + root (cths/get-shape-by-id file uuid/zero) + frame-red (cths/get-shape file :frame-red) + v01 (cths/get-shape file :v01) + + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id v01)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id v01)) + (dw/delete-selected) + (dws/select-shape (:id frame-red)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + root' (cths/get-shape-by-id file' uuid/zero) + frame-red' (cths/get-shape file' :frame-red) + root-children' (->> (:shapes root') + (map #(cths/get-shape-by-id file' %)))] + + ;; ==== Check + ;; The main shape of the component have no children + (t/is (= 0 (count (:shapes frame-red')))) + ;; Root had two children, now it still have two (because we have cutted one of them, and then created a new one) + (t/is (= 2 (count (:shapes root)))) + (t/is (= 2 (count (:shapes root')))) + ;; One of the children of root is a variant-container + (t/is (= 1 (count (filter ctk/is-variant-container? root-children')))))))))) + +(t/deftest copy-variant-into-different-variant-container + "Coping a variant into a different variant-container creates a new variant inside that container" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02) + (thv/add-variant :v02 :c03 :m03 :c04 :m04)) + + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + m01 (cths/get-shape file :m01) + v02 (cths/get-shape file :v02) + components (cthc/get-components file) + + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id m01)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id v02)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + v02' (cths/get-shape file' :v02) + components' (cthc/get-components file')] + + ;; ==== Check + ;; v02 had two children, now it have 3 + (t/is (= 2 (count (:shapes v02)))) + (t/is (= 3 (count (:shapes v02')))) + + ;;There was 4 components, now there are 5 + (t/is (= 4 (count components))) + (t/is (= 5 (count components'))))))))) + +(t/deftest copy-variant-into-variant-another-container + "Coping a variant into a variant of a different variant-container creates a copy of the variant inside" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02) + (thv/add-variant :v02 :c03 :m03 :c04 :m04)) + + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + m01 (cths/get-shape file :m01) + v02 (cths/get-shape file :v02) + m03 (cths/get-shape file :m03) + components (cthc/get-components file) + + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id m01)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id m03)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + v02' (cths/get-shape file' :v02) + m03' (cths/get-shape file' :m03) + components' (cthc/get-components file')] + + ;; ==== Check + ;; v02 had two children, now it have still 2 + (t/is (= 2 (count (:shapes v02)))) + (t/is (= 2 (count (:shapes v02')))) + + ;; m03 had no children, now it have 1 + (t/is (= 0 (count (:shapes m03)))) + (t/is (= 1 (count (:shapes m03')))) + + ;;There was 4 components, now there is still 4 + (t/is (= 4 (count components))) + (t/is (= 4 (count components'))))))))) + +(t/deftest copy-variant-into-its-variant-container + "Coping a variant into its own variant-container creates a new variant inside that container" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + m01 (cths/get-shape file :m01) + v01 (cths/get-shape file :v01) + components (cthc/get-components file) + + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id m01)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id v01)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + v01' (cths/get-shape file' :v01) + components' (cthc/get-components file')] + + ;; ==== Check + ;; v01 had two children, now it have 3 + (t/is (= 2 (count (:shapes v01)))) + (t/is (= 3 (count (:shapes v01')))) + + ;;There was 2 components, now there are 3 + (t/is (= 2 (count components))) + (t/is (= 3 (count components'))))))))) + +(t/deftest copy-variant-into-itself + "Coping a variant into itself creates a new variant inside its container" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + m01 (cths/get-shape file :m01) + v01 (cths/get-shape file :v01) + components (cthc/get-components file) + + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id m01)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id m01)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + v01' (cths/get-shape file' :v01) + components' (cthc/get-components file')] + + ;; ==== Check + ;; v01 had two children, now it have 3 + (t/is (= 2 (count (:shapes v01)))) + (t/is (= 3 (count (:shapes v01')))) + + ;;There was 2 components, now there are 3 + (t/is (= 2 (count components))) + (t/is (= 3 (count components'))))))))) + +(t/deftest copy-variant-into-a-brother + "Coping a variant into a brother variant creates a new variant inside its container" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + m01 (cths/get-shape file :m01) + m02 (cths/get-shape file :m02) + v01 (cths/get-shape file :v01) + components (cthc/get-components file) + + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id m01)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id m02)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + v01' (cths/get-shape file' :v01) + components' (cthc/get-components file')] + + ;; ==== Check + ;; v01 had two children, now it have 3 + (t/is (= 2 (count (:shapes v01)))) + (t/is (= 3 (count (:shapes v01')))) + + ;;There was 2 components, now there are 3 + (t/is (= 2 (count components))) + (t/is (= 3 (count components'))))))))) + +(t/deftest copy-component-into-a-variant-container + "Coping a component into a variant-container creates a new variant inside that container" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (ctho/add-frame :frame-red {:name "frame-red"}) + (cthc/make-component :red :frame-red) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + frame-red (cths/get-shape file :frame-red) + v01 (cths/get-shape file :v01) + components (cthc/get-components file) + + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id frame-red)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id v01)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + v01' (cths/get-shape file' :v01) + components' (cthc/get-components file')] + + ;; ==== Check + ;; v01 had two children, now it have 3 + (t/is (= 2 (count (:shapes v01)))) + (t/is (= 3 (count (:shapes v01')))) + + ;;There was 3 components, now there are 4 + (t/is (= 3 (count components))) + (t/is (= 4 (count components'))))))))) + +(t/deftest copy-component-into-a-variant + "Coping a component into a variant creates a copy of the component inside the variant" + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (ctho/add-frame :frame-red {:name "frame-red"}) + (cthc/make-component :red :frame-red) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + + store (ths/setup-store file) + + ;; ==== Action + page (cthf/current-page file) + frame-red (cths/get-shape file :frame-red) + v01 (cths/get-shape file :v01) + m01 (cths/get-shape file :m01) + components (cthc/get-components file) + + features #{} + version 67 + + pdata (thp/simulate-copy-shape #{(:id frame-red)} (:objects page) {(:id file) file} page file features version) + + events + [(dws/select-shape (:id m01)) + (dw/paste-shapes pdata)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-state new-state) + v01' (cths/get-shape file' :v01) + m01' (cths/get-shape file' :m01) + components' (cthc/get-components file')] + + ;; ==== Check + ;; v01 had two children, now it have still 2 + (t/is (= 2 (count (:shapes v01)))) + (t/is (= 2 (count (:shapes v01')))) + + ;; m01 had no children, now it have 1 + (t/is (= 0 (count (:shapes m01)))) + (t/is (= 1 (count (:shapes m01')))) + + ;;There was 3 components, now there are still 3 + (t/is (= 3 (count components))) + (t/is (= 3 (count components'))))))))) \ No newline at end of file diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index c3def3b7de..1b0f413364 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -9,6 +9,7 @@ [frontend-tests.logic.copying-and-duplicating-test] [frontend-tests.logic.frame-guides-test] [frontend-tests.logic.groups-test] + [frontend-tests.logic.pasting-in-containers-test] [frontend-tests.plugins.context-shapes-test] [frontend-tests.tokens.import-export-test] [frontend-tests.tokens.logic.token-actions-test] @@ -35,6 +36,7 @@ 'frontend-tests.logic.copying-and-duplicating-test 'frontend-tests.logic.frame-guides-test 'frontend-tests.logic.groups-test + 'frontend-tests.logic.pasting-in-containers-test 'frontend-tests.plugins.context-shapes-test 'frontend-tests.util-range-tree-test 'frontend-tests.util-snap-data-test