diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 7a0207c795..24da8365b8 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -352,7 +352,8 @@ [:map {:title "RestoreComponentChange"} [:type [:= :restore-component]] [:id ::sm/uuid] - [:page-id ::sm/uuid]]] + [:page-id ::sm/uuid] + [:parent-id {:optional true} [:maybe ::sm/uuid]]]] [:purge-component [:map {:title "PurgeComponentChange"} @@ -963,8 +964,8 @@ (ctf/delete-component data id skip-undelete? main-instance)) (defmethod process-change :restore-component - [data {:keys [id page-id]}] - (ctf/restore-component data id page-id)) + [data {:keys [id page-id parent-id]}] + (ctf/restore-component data id page-id parent-id)) (defmethod process-change :purge-component [data {:keys [id]}] diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 4e4836b6c2..41284fc781 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -1041,12 +1041,13 @@ :page-id page-id}))) (defn restore-component - [changes id page-id main-instance] + [changes id page-id main-instance parent-id] (assert-library! changes) (-> changes (update :redo-changes conj {:type :restore-component :id id - :page-id page-id}) + :page-id page-id + :parent-id parent-id}) (update :undo-changes conj {:type :del-component :id id :main-instance main-instance}))) diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index 982f27aaa5..729c61eb89 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -68,7 +68,8 @@ :variant-bad-name :variant-bad-variant-name :variant-component-bad-name - :variant-no-properties}) + :variant-no-properties + :variant-component-bad-id}) (def ^:private schema:error [:map {:title "ValidationError"} @@ -469,6 +470,10 @@ (when-not (= (:name parent) (cfh/merge-path-item (:path component) (:name component))) (report-error :variant-component-bad-name (str/ffmt "Component % has an invalid name" (:id shape)) + shape file page)) + (when-not (= (:variant-id component) (:variant-id shape)) + (report-error :variant-component-bad-id + (str/ffmt "Variant % has adifferent variant-id than its component" (:id shape)) shape file page)))) (defn- check-shape diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index ddce143e91..2d9e9634be 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -103,84 +103,76 @@ (defn- duplicate-component "Clone the root shape of the component and all children. Generate new ids from all of them." - [component new-component-id library-data force-id] - (let [components-v2 (dm/get-in library-data [:options :components-v2])] - (if components-v2 - (let [main-instance-page (ctf/get-component-page library-data component) - main-instance-shape (ctf/get-component-root library-data component) - delta (gpt/point (+ (:width main-instance-shape) 50) 0) + [component new-component-id library-data force-id delta variant-id] + (let [main-instance-page (ctf/get-component-page library-data component) + main-instance-shape (ctf/get-component-root library-data component) + delta (or delta (gpt/point (+ (:width main-instance-shape) 50) 0)) - ids-map (volatile! {}) - inverted-ids-map (volatile! {}) - nested-main-heads (volatile! #{}) + ids-map (volatile! {}) + inverted-ids-map (volatile! {}) + nested-main-heads (volatile! #{}) - update-original-shape - (fn [original-shape new-shape] + update-original-shape + (fn [original-shape new-shape] ; Save some ids for later - (vswap! ids-map assoc (:id original-shape) (:id new-shape)) - (vswap! inverted-ids-map assoc (:id new-shape) (:id original-shape)) - (when (and (ctk/main-instance? original-shape) - (not= (:component-id original-shape) (:id component))) - (vswap! nested-main-heads conj (:id original-shape))) - original-shape) + (vswap! ids-map assoc (:id original-shape) (:id new-shape)) + (vswap! inverted-ids-map assoc (:id new-shape) (:id original-shape)) + (when (and (ctk/main-instance? original-shape) + (not= (:component-id original-shape) (:id component))) + (vswap! nested-main-heads conj (:id original-shape))) + original-shape) - update-new-shape - (fn [new-shape _] - (cond-> new-shape + update-new-shape + (fn [new-shape _] + (cond-> new-shape ; Link the new main to the new component - (= (:component-id new-shape) (:id component)) - (assoc :component-id new-component-id) + (= (:component-id new-shape) (:id component)) + (assoc :component-id new-component-id) - :always - (gsh/move delta))) + (some? variant-id) + (assoc :variant-id variant-id) - [new-instance-shape new-instance-shapes _] - (ctst/clone-shape main-instance-shape - (:parent-id main-instance-shape) - (:objects main-instance-page) - :update-new-shape update-new-shape - :update-original-shape update-original-shape - :force-id force-id) + :always + (gsh/move delta))) - remap-frame - (fn [shape] + [new-instance-shape new-instance-shapes _] + (ctst/clone-shape main-instance-shape + (:parent-id main-instance-shape) + (:objects main-instance-page) + :update-new-shape update-new-shape + :update-original-shape update-original-shape + :force-id force-id) + + remap-frame + (fn [shape] ; Remap all frame-ids internal to the component to the new shapes - (update shape :frame-id - #(get @ids-map % (:frame-id shape)))) + (update shape :frame-id + #(get @ids-map % (:frame-id shape)))) - convert-nested-main - (fn [shape] + convert-nested-main + (fn [shape] ; If there is some nested main instance, convert it into a copy of ; main nested in the original component. - (let [origin-shape-id (get @inverted-ids-map (:id shape)) - objects (:objects main-instance-page) - parent-ids (cfh/get-parent-ids-seq-with-self objects origin-shape-id)] - (cond-> shape - (@nested-main-heads origin-shape-id) - (dissoc :main-instance) + (let [origin-shape-id (get @inverted-ids-map (:id shape)) + objects (:objects main-instance-page) + parent-ids (cfh/get-parent-ids-seq-with-self objects origin-shape-id)] + (cond-> shape + (@nested-main-heads origin-shape-id) + (dissoc :main-instance) - (some @nested-main-heads parent-ids) - (assoc :shape-ref origin-shape-id)))) + (some @nested-main-heads parent-ids) + (assoc :shape-ref origin-shape-id)))) - xf-shape (comp (map remap-frame) - (map convert-nested-main)) + xf-shape (comp (map remap-frame) + (map convert-nested-main)) - new-instance-shapes (into [] xf-shape new-instance-shapes)] + new-instance-shapes (into [] xf-shape new-instance-shapes)] - [nil nil new-instance-shape new-instance-shapes]) - - (let [component-root (d/seek #(nil? (:parent-id %)) (vals (:objects component))) - - [new-component-shape new-component-shapes _] - (ctst/clone-shape component-root - nil - (get component :objects))] - - [new-component-shape new-component-shapes nil nil])))) + [nil nil new-instance-shape new-instance-shapes])) (defn generate-duplicate-component "Create a new component copied from the one with the given id." - [changes library component-id new-component-id components-v2 & {:keys [new-shape-id apply-changes-local-library?]}] + [changes library component-id new-component-id components-v2 & {:keys [new-shape-id apply-changes-local-library? delta new-variant-id]}] (let [component (ctkl/get-component (:data library) component-id) new-name (:name component) @@ -192,7 +184,7 @@ [new-component-shape new-component-shapes ; <- null in components-v2 new-main-instance-shape new-main-instance-shapes] - (duplicate-component component new-component-id (:data library) new-shape-id)] + (duplicate-component component new-component-id (:data library) new-shape-id delta new-variant-id)] [new-main-instance-shape (-> changes @@ -209,7 +201,7 @@ (:id new-main-instance-shape) (:id main-instance-page) (:annotation component) - (:variant-id component) + (or new-variant-id (:variant-id component)) (:variant-properties component) {:apply-changes-local-library? apply-changes-local-library?}) ;; Update grid layout if the new main instance is inside @@ -376,6 +368,7 @@ inside-component? (some? (ctn/get-instance-root (:objects page) parent)) shapes (cfh/get-children-with-self (:objects component) (:main-instance-id component)) shapes (map #(gsh/move % delta) shapes) + is-variant? (ctk/is-variant? component) first-shape (cond-> (first shapes) (not (nil? parent-id)) @@ -389,7 +382,9 @@ inside-component? (dissoc :component-root) (not inside-component?) - (assoc :component-root true)) + (assoc :component-root true) + (and is-variant? (some? parent-id)) + (assoc :variant-id parent-id)) changes (-> changes (pcb/with-page page) @@ -400,7 +395,7 @@ changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) changes (rest shapes))] - {:changes (pcb/restore-component changes component-id (:id page) main-inst) + {:changes (pcb/restore-component changes component-id (:id page) main-inst parent-id) :shape (first shapes)}))) ;; ---- General library synchronization functions ---- @@ -2160,52 +2155,89 @@ (pcb/with-page changes page) frames))) -(defn generate-duplicate-component-change - [changes objects page component-root parent-id frame-id delta libraries library-data] - (let [component-id (:component-id component-root) - file-id (:component-file component-root) - main-component (ctf/get-component libraries file-id component-id) - moved-component (gsh/move component-root delta) - pos (gpt/point (:x moved-component) (:y moved-component)) - origin-frame (get-in page [:objects frame-id]) - delta (cond-> delta - (some? origin-frame) - (gpt/subtract (-> origin-frame :selrect gpt/point))) +(defn- duplicate-variant + [changes library component base-pos parent-id] + (let [component-page (ctpl/get-page (:data library) (:main-instance-page component)) + component-shape (dm/get-in component-page [: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) + [shape changes] (generate-duplicate-component changes + library + (:component-id component-shape) + new-component-id + true + {:apply-changes-local-library? true + :delta delta + :new-variant-id parent-id})] + [shape + (-> changes + (pcb/change-parent parent-id [shape]))])) + + +(defn generate-duplicate-component-change + [changes objects page main parent-id frame-id delta libraries library-data ids-map] + (let [main-id (:id main) + component-id (:component-id main) + file-id (:component-file main) + component (ctf/get-component libraries file-id component-id) + pos (as-> (gsh/move main delta) $ + (gpt/point (:x $) (:y $))) + + ;; 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)) - instantiate-component - #(generate-instantiate-component changes - objects - file-id - (:component-id component-root) - pos - page - libraries - (:id component-root) - parent-id - frame-id - {}) restore-component - #(let [restore (prepare-restore-component changes library-data (:component-id component-root) page delta (:id component-root) parent-id frame-id)] - [(:shape restore) (:changes restore)]) + #(let [origin-frame (get-in page [:objects frame-id]) + delta (cond-> delta + (some? origin-frame) + (gpt/subtract (-> origin-frame :selrect gpt/point))) + {:keys [shape changes]} (prepare-restore-component changes + library-data + component-id + page + delta + main-id + parent-id + frame-id)] + [shape changes]) [_shape changes] - (if (nil? main-component) + (if (nil? component) (restore-component) - (instantiate-component))] + (if (and (ctk/is-variant? main) in-variant-container?) + (duplicate-variant changes + (get libraries file-id) + component + pos + parent-id) + + (generate-instantiate-component changes + objects + file-id + component-id + pos + page + libraries + main-id + parent-id + frame-id + {})))] changes)) (defn generate-duplicate-shape-change - ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id] - (generate-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id (:frame-id obj) (:parent-id obj) false false true)) + ([changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id] + (generate-duplicate-shape-change changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id (:frame-id obj) (:parent-id obj) false false true)) - ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id frame-id parent-id duplicating-component? child? remove-swap-slot?] + ([changes objects page unames update-unames! ids ids-map obj delta level-delta libraries library-data file-id frame-id parent-id duplicating-component? child? remove-swap-slot?] (cond (nil? obj) changes (ctf/is-main-of-known-component? obj libraries) - (generate-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data) + (generate-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data ids-map) :else (let [frame? (cfh/frame-shape? obj) @@ -2307,6 +2339,7 @@ page unames update-unames! + ids ids-map child delta @@ -2349,6 +2382,7 @@ page unames update-unames! + ids ids-map %2 delta diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index c6f029ac57..722622076c 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -433,14 +433,19 @@ (defn restore-component "Recover a deleted component and all its shapes and put all this again in place." - [file-data component-id page-id] + [file-data component-id page-id parent-id] (let [components-v2 (dm/get-in file-data [:options :components-v2]) - update-page? (and components-v2 (not (nil? page-id)))] + update-page? (and components-v2 (not (nil? page-id))) + component (ctkl/get-component file-data component-id true) + update-variant? (and (some? parent-id) + (ctk/is-variant? component))] (-> file-data (ctkl/update-component component-id #(dissoc % :objects)) (ctkl/mark-component-undeleted component-id) (cond-> update-page? - (ctkl/update-component component-id #(assoc % :main-instance-page page-id)))))) + (ctkl/update-component component-id #(assoc % :main-instance-page page-id))) + (cond-> update-variant? + (ctkl/update-component component-id #(assoc % :variant-id parent-id)))))) (defn purge-component "Remove permanently a component." diff --git a/common/test/common_tests/logic/variants_test.cljc b/common/test/common_tests/logic/variants_test.cljc index 2c63a3e619..22971d3078 100644 --- a/common/test/common_tests/logic/variants_test.cljc +++ b/common/test/common_tests/logic/variants_test.cljc @@ -7,6 +7,8 @@ (ns common-tests.logic.variants-test (:require [app.common.files.changes-builder :as pcb] + [app.common.geom.point :as gpt] + [app.common.logic.libraries :as cll] [app.common.logic.variant-properties :as clvp] [app.common.test-helpers.components :as thc] [app.common.test-helpers.files :as thf] @@ -192,3 +194,43 @@ ;; ==== Check (t/is (= (-> comp01' :variant-properties first :value) "NewValue1")) (t/is (= (-> comp02' :variant-properties first :value) "NewValue2")))) + + +(t/deftest test-duplicate-variant-container + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + data (:data file) + page (thf/current-page file) + objects (:objects page) + + variant-container (ths/get-shape file :v01) + + + + + ;; ==== Action + changes (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-library-data (:data file)) + (pcb/with-objects (:objects page)) + (cll/generate-duplicate-changes objects ;; objects + page ;; page + #{(:id variant-container)} ;; ids + (gpt/point 0 0) ;; delta + {(:id file) file} ;; libraries + (:data file) ;; library-data + (:id file))) ;; file-id + + ;; ==== Get + file' (thf/apply-changes file changes) + data' (:data file') + page' (thf/current-page file') + objects' (:objects page')] + + ;; ==== Check + (thf/validate-file! file') + (t/is (= (count (:components data)) 2)) + (t/is (= (count (:components data')) 4)) + (t/is (= (count objects) 4)) + (t/is (= (count objects') 7))))