mirror of
https://github.com/penpot/penpot.git
synced 2025-04-09 21:41:23 -05:00
🎉 Allow duplicate/copy-paste/cut-paste variants
This commit is contained in:
parent
076d64df8f
commit
f04229d8cb
6 changed files with 191 additions and 103 deletions
|
@ -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]}]
|
||||
|
|
|
@ -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})))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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))))
|
||||
|
|
Loading…
Add table
Reference in a new issue