0
Fork 0
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:
Pablo Alba 2025-04-01 11:07:22 +02:00 committed by GitHub
parent 076d64df8f
commit f04229d8cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 191 additions and 103 deletions

View file

@ -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]}]

View file

@ -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})))

View file

@ -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

View file

@ -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

View file

@ -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."

View file

@ -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))))