mirror of
https://github.com/penpot/penpot.git
synced 2025-08-04 05:48:27 +02:00
Merge pull request #2201 from penpot/hiru-undelete-components
🎉 Allow to restore deleted components
This commit is contained in:
commit
11018581ed
26 changed files with 469 additions and 133 deletions
|
@ -9,12 +9,16 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages.changes :as ch]
|
||||
[app.common.pages.changes-spec :as pcs]
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]))
|
||||
|
@ -516,10 +520,17 @@
|
|||
[file data]
|
||||
|
||||
(let [selrect cts/empty-selrect
|
||||
name (:name data)
|
||||
path (:path data)
|
||||
name (:name data)
|
||||
path (:path data)
|
||||
main-instance-id (:main-instance-id data)
|
||||
main-instance-page (:main-instance-page data)
|
||||
obj (-> (cts/make-minimal-group nil selrect name)
|
||||
(merge data)
|
||||
(dissoc :path
|
||||
:main-instance-id
|
||||
:main-instance-page
|
||||
:main-instance-x
|
||||
:main-instance-y)
|
||||
(check-name file :group)
|
||||
(d/without-nils))]
|
||||
(-> file
|
||||
|
@ -528,6 +539,8 @@
|
|||
:id (:id obj)
|
||||
:name name
|
||||
:path path
|
||||
:main-instance-id main-instance-id
|
||||
:main-instance-page main-instance-page
|
||||
:shapes [obj]})
|
||||
|
||||
(assoc :last-id (:id obj))
|
||||
|
@ -546,7 +559,8 @@
|
|||
(commit-change
|
||||
file
|
||||
{:type :del-component
|
||||
:id component-id})
|
||||
:id component-id
|
||||
:skip-undelete? true})
|
||||
|
||||
(:masked-group? component)
|
||||
(let [mask (first children)]
|
||||
|
@ -586,6 +600,42 @@
|
|||
(dissoc :current-component-id)
|
||||
(update :parent-stack pop))))
|
||||
|
||||
(defn finish-deleted-component
|
||||
[component-id page-id main-instance-x main-instance-y file]
|
||||
(let [file (assoc file :current-component-id component-id)
|
||||
page (ctpl/get-page (:data file) page-id)
|
||||
component (ctkl/get-component (:data file) component-id)
|
||||
main-instance-id (:main-instance-id component)
|
||||
|
||||
; To obtain a deleted component, we first create the component
|
||||
; and the main instance in the workspace, and then delete them.
|
||||
[_ shapes]
|
||||
(ctn/make-component-instance page
|
||||
component
|
||||
(:id file)
|
||||
(gpt/point main-instance-x
|
||||
main-instance-y)
|
||||
{:main-instance? true
|
||||
:force-id main-instance-id})]
|
||||
(as-> file $
|
||||
(reduce #(commit-change %1
|
||||
{:type :add-obj
|
||||
:id (:id %2)
|
||||
:page-id (:id page)
|
||||
:parent-id (:parent-id %2)
|
||||
:frame-id (:frame-id %2)
|
||||
:obj %2})
|
||||
$
|
||||
shapes)
|
||||
(commit-change $ {:type :del-component
|
||||
:id component-id})
|
||||
(reduce #(commit-change %1 {:type :del-obj
|
||||
:page-id page-id
|
||||
:id (:id %2)})
|
||||
$
|
||||
shapes)
|
||||
(dissoc $ :current-component-id))))
|
||||
|
||||
(defn delete-object
|
||||
[file id]
|
||||
(let [page-id (:current-page-id file)]
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.colors-list :as ctcl]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape :as cts]
|
||||
|
@ -302,9 +303,7 @@
|
|||
|
||||
(defmethod process-change :del-page
|
||||
[data {:keys [id]}]
|
||||
(-> data
|
||||
(update :pages (fn [pages] (filterv #(not= % id) pages)))
|
||||
(update :pages-index dissoc id)))
|
||||
(ctpl/delete-page data id))
|
||||
|
||||
(defmethod process-change :mov-page
|
||||
[data {:keys [id index]}]
|
||||
|
@ -320,7 +319,7 @@
|
|||
|
||||
(defmethod process-change :del-color
|
||||
[data {:keys [id]}]
|
||||
(update data :colors dissoc id))
|
||||
(ctcl/delete-color data id))
|
||||
|
||||
(defmethod process-change :add-recent-color
|
||||
[data {:keys [color]}]
|
||||
|
@ -371,8 +370,16 @@
|
|||
(assoc :objects objects))))
|
||||
|
||||
(defmethod process-change :del-component
|
||||
[data {:keys [id skip-undelete?]}]
|
||||
(ctf/delete-component data id skip-undelete?))
|
||||
|
||||
(defmethod process-change :restore-component
|
||||
[data {:keys [id]}]
|
||||
(d/dissoc-in data [:components id]))
|
||||
(ctf/restore-component data id))
|
||||
|
||||
(defmethod process-change :purge-component
|
||||
[data {:keys [id]}]
|
||||
(ctf/purge-component data id))
|
||||
|
||||
;; -- Typography
|
||||
|
||||
|
@ -386,7 +393,7 @@
|
|||
|
||||
(defmethod process-change :del-typography
|
||||
[data {:keys [id]}]
|
||||
(update data :typographies dissoc id))
|
||||
(ctyl/delete-typography data id))
|
||||
|
||||
;; === Operations
|
||||
|
||||
|
|
|
@ -617,18 +617,34 @@
|
|||
changes)))
|
||||
|
||||
(defn delete-component
|
||||
[changes id]
|
||||
[changes id components-v2]
|
||||
(assert-library changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-component (get-in library-data [:components id])]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :del-component
|
||||
:id id})
|
||||
(update :undo-changes d/preconj {:type :add-component
|
||||
:id id
|
||||
:name (:name prev-component)
|
||||
:path (:path prev-component)
|
||||
:main-instance-id (:main-instance-id prev-component)
|
||||
:main-instance-page (:main-instance-page prev-component)
|
||||
:shapes (vals (:objects prev-component))}))))
|
||||
(update :undo-changes
|
||||
(fn [undo-changes]
|
||||
(cond-> undo-changes
|
||||
components-v2
|
||||
(d/preconj {:type :purge-component
|
||||
:id id})
|
||||
|
||||
:always
|
||||
(d/preconj {:type :add-component
|
||||
:id id
|
||||
:name (:name prev-component)
|
||||
:path (:path prev-component)
|
||||
:main-instance-id (:main-instance-id prev-component)
|
||||
:main-instance-page (:main-instance-page prev-component)
|
||||
:shapes (vals (:objects prev-component))})))))))
|
||||
|
||||
(defn restore-component
|
||||
[changes id]
|
||||
(assert-library changes)
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :restore-component
|
||||
:id id})
|
||||
(update :undo-changes d/preconj {:type :del-component
|
||||
:id id})))
|
||||
|
|
|
@ -159,7 +159,16 @@
|
|||
(s/keys :req-un [::id]
|
||||
:opt-un [::name :internal.changes.add-component/shapes]))
|
||||
|
||||
(s/def :internal.changes.del-component/skip-undelete? boolean?)
|
||||
|
||||
(defmethod change-spec :del-component [_]
|
||||
(s/keys :req-un [::id]
|
||||
:opt-un [:internal.changes.del-component/skip-undelete?]))
|
||||
|
||||
(defmethod change-spec :restore-component [_]
|
||||
(s/keys :req-un [::id]))
|
||||
|
||||
(defmethod change-spec :purge-component [_]
|
||||
(s/keys :req-un [::id]))
|
||||
|
||||
(defmethod change-spec :add-typography [_]
|
||||
|
|
|
@ -22,3 +22,7 @@
|
|||
[file-data color-id f]
|
||||
(update-in file-data [:colors color-id] f))
|
||||
|
||||
(defn delete-color
|
||||
[file-data color-id]
|
||||
(update file-data :colors dissoc color-id))
|
||||
|
||||
|
|
|
@ -35,3 +35,7 @@
|
|||
[file-data component-id f]
|
||||
(update-in file-data [:components component-id] f))
|
||||
|
||||
(defn delete-component
|
||||
[file-data component-id]
|
||||
(update file-data :components dissoc component-id))
|
||||
|
||||
|
|
|
@ -108,53 +108,59 @@
|
|||
"Clone the shapes of the component, generating new names and ids, and linking
|
||||
each new shape to the corresponding one of the component. Place the new instance
|
||||
coordinates in the given position."
|
||||
[container component component-file-id position main-instance?]
|
||||
(let [component-shape (get-shape component (:id component))
|
||||
([container component component-file-id position]
|
||||
(make-component-instance container component component-file-id position {}))
|
||||
|
||||
orig-pos (gpt/point (:x component-shape) (:y component-shape))
|
||||
delta (gpt/subtract position orig-pos)
|
||||
([container component component-file-id position
|
||||
{:keys [main-instance? force-id] :or {main-instance? false force-id nil}}]
|
||||
(let [component-shape (get-shape component (:id component))
|
||||
|
||||
objects (:objects container)
|
||||
unames (volatile! (ctst/retrieve-used-names objects))
|
||||
orig-pos (gpt/point (:x component-shape) (:y component-shape))
|
||||
delta (gpt/subtract position orig-pos)
|
||||
|
||||
frame-id (ctst/frame-id-by-position objects (gpt/add orig-pos delta))
|
||||
objects (:objects container)
|
||||
unames (volatile! (ctst/retrieve-used-names objects))
|
||||
|
||||
update-new-shape
|
||||
(fn [new-shape original-shape]
|
||||
(let [new-name (ctst/generate-unique-name @unames (:name new-shape))]
|
||||
frame-id (ctst/frame-id-by-position objects (gpt/add orig-pos delta))
|
||||
|
||||
(when (nil? (:parent-id original-shape))
|
||||
(vswap! unames conj new-name))
|
||||
update-new-shape
|
||||
(fn [new-shape original-shape]
|
||||
(let [new-name (ctst/generate-unique-name @unames (:name new-shape))]
|
||||
|
||||
(cond-> new-shape
|
||||
true
|
||||
(as-> $
|
||||
(gsh/move $ delta)
|
||||
(assoc $ :frame-id frame-id)
|
||||
(assoc $ :parent-id
|
||||
(or (:parent-id $) (:frame-id $)))
|
||||
(dissoc $ :touched))
|
||||
(when (nil? (:parent-id original-shape))
|
||||
(vswap! unames conj new-name))
|
||||
|
||||
(nil? (:shape-ref original-shape))
|
||||
(assoc :shape-ref (:id original-shape))
|
||||
(cond-> new-shape
|
||||
true
|
||||
(as-> $
|
||||
(gsh/move $ delta)
|
||||
(assoc $ :frame-id frame-id)
|
||||
(assoc $ :parent-id
|
||||
(or (:parent-id $) (:frame-id $)))
|
||||
(dissoc $ :touched))
|
||||
|
||||
(nil? (:parent-id original-shape))
|
||||
(assoc :component-id (:id original-shape)
|
||||
:component-file component-file-id
|
||||
:component-root? true
|
||||
:name new-name)
|
||||
(nil? (:shape-ref original-shape))
|
||||
(assoc :shape-ref (:id original-shape))
|
||||
|
||||
(and (nil? (:parent-id original-shape)) main-instance?)
|
||||
(assoc :main-instance? true)
|
||||
(nil? (:parent-id original-shape))
|
||||
(assoc :component-id (:id original-shape)
|
||||
:component-file component-file-id
|
||||
:component-root? true
|
||||
:name new-name)
|
||||
|
||||
(some? (:parent-id original-shape))
|
||||
(dissoc :component-root?))))
|
||||
(and (nil? (:parent-id original-shape)) main-instance?)
|
||||
(assoc :main-instance? true)
|
||||
|
||||
[new-shape new-shapes _]
|
||||
(ctst/clone-object component-shape
|
||||
nil
|
||||
(get component :objects)
|
||||
update-new-shape)]
|
||||
(some? (:parent-id original-shape))
|
||||
(dissoc :component-root?))))
|
||||
|
||||
[new-shape new-shapes _]
|
||||
(ctst/clone-object component-shape
|
||||
nil
|
||||
(get component :objects)
|
||||
update-new-shape
|
||||
(fn [object _] object)
|
||||
force-id)]
|
||||
|
||||
[new-shape new-shapes])))
|
||||
|
||||
[new-shape new-shapes]))
|
||||
|
||||
|
|
|
@ -120,6 +120,57 @@
|
|||
|
||||
;; Asset helpers
|
||||
|
||||
(defn delete-component
|
||||
"Delete a component and store it to be able to be recovered later.
|
||||
|
||||
Remember also the position of the main instance."
|
||||
([file-data component-id]
|
||||
(delete-component file-data component-id false))
|
||||
|
||||
([file-data component-id skip-undelete?]
|
||||
(let [components-v2 (get-in file-data [:options :components-v2])
|
||||
|
||||
add-to-deleted-components
|
||||
(fn [file-data]
|
||||
(let [component (ctkl/get-component file-data component-id)]
|
||||
(if (some? component)
|
||||
(let [page (ctpl/get-page file-data (:main-instance-page component))
|
||||
main-instance (ctn/get-shape page (:main-instance-id component))
|
||||
component (assoc component
|
||||
:main-instance-x (:x main-instance) ; An instance root is always a group,
|
||||
:main-instance-y (:y main-instance))] ; so it will have :x and :y
|
||||
(when (nil? main-instance)
|
||||
(throw (ex-info "Cannot delete the main instance before the component" {:component-id component-id})))
|
||||
(assoc-in file-data [:deleted-components component-id] component))
|
||||
file-data)))]
|
||||
|
||||
(cond-> file-data
|
||||
(and components-v2 (not skip-undelete?))
|
||||
(add-to-deleted-components)
|
||||
|
||||
:always
|
||||
(ctkl/delete-component component-id)))))
|
||||
|
||||
(defn get-deleted-component
|
||||
"Retrieve a component that has been deleted but still is in the safe store."
|
||||
[file-data component-id]
|
||||
(get-in file-data [:deleted-components component-id]))
|
||||
|
||||
(defn restore-component
|
||||
"Recover a deleted component and put it again in place."
|
||||
[file-data component-id]
|
||||
(let [component (-> (get-in file-data [:deleted-components component-id])
|
||||
(dissoc :main-instance-x :main-instance-y))]
|
||||
(cond-> file-data
|
||||
(some? component)
|
||||
(-> (assoc-in [:components component-id] component)
|
||||
(d/dissoc-in [:deleted-components component-id])))))
|
||||
|
||||
(defn purge-component
|
||||
"Remove permanently a component."
|
||||
[file-data component-id]
|
||||
(d/dissoc-in file-data [:deleted-components component-id]))
|
||||
|
||||
(defmulti uses-asset?
|
||||
"Checks if a shape uses the given asset."
|
||||
(fn [asset-type _ _ _] asset-type))
|
||||
|
@ -185,7 +236,7 @@
|
|||
|
||||
(defn migrate-to-components-v2
|
||||
"If there is any component in the file library, add a new 'Library backup' and generate
|
||||
main instances for all components there. Mark the file with the :comonents-v2 option."
|
||||
main instances for all components there. Mark the file with the :components-v2 option."
|
||||
[file-data]
|
||||
(let [components (ctkl/components-seq file-data)]
|
||||
(if (or (empty? components)
|
||||
|
@ -205,7 +256,7 @@
|
|||
component
|
||||
(:id file-data)
|
||||
position
|
||||
true)
|
||||
{:main-instance? true})
|
||||
|
||||
add-shapes
|
||||
(fn [page]
|
||||
|
@ -269,7 +320,7 @@
|
|||
component
|
||||
(:id file-data)
|
||||
position
|
||||
true)
|
||||
{:main-instance? true})
|
||||
|
||||
; Add all shapes of the main instance to the library page
|
||||
add-main-instance-shapes
|
||||
|
|
|
@ -37,3 +37,9 @@
|
|||
[file-data page-id f]
|
||||
(update-in file-data [:pages-index page-id] f))
|
||||
|
||||
(defn delete-page
|
||||
[file-data page-id]
|
||||
(-> file-data
|
||||
(update :pages (fn [pages] (filterv #(not= % page-id) pages)))
|
||||
(update :pages-index dissoc page-id)))
|
||||
|
||||
|
|
|
@ -284,10 +284,13 @@
|
|||
the order of the children of each parent."
|
||||
|
||||
([object parent-id objects update-new-object]
|
||||
(clone-object object parent-id objects update-new-object (fn [object _] object)))
|
||||
(clone-object object parent-id objects update-new-object (fn [object _] object) nil))
|
||||
|
||||
([object parent-id objects update-new-object update-original-object]
|
||||
(let [new-id (uuid/next)]
|
||||
(clone-object object parent-id objects update-new-object update-original-object nil))
|
||||
|
||||
([object parent-id objects update-new-object update-original-object force-id]
|
||||
(let [new-id (or force-id (uuid/next))]
|
||||
(loop [child-ids (seq (:shapes object))
|
||||
new-direct-children []
|
||||
new-children []
|
||||
|
|
|
@ -22,3 +22,7 @@
|
|||
[file-data typography-id f]
|
||||
(update-in file-data [:typographies typography-id] f))
|
||||
|
||||
(defn delete-typography
|
||||
[file-data typography-id]
|
||||
(update file-data :typographies dissoc typography-id))
|
||||
|
||||
|
|
|
@ -97,8 +97,7 @@
|
|||
(ctn/make-component-instance (ctpl/get-page file-data page-id)
|
||||
(ctkl/get-component (:data library) component-id)
|
||||
(:id library)
|
||||
(gpt/point 0 0)
|
||||
false)]
|
||||
(gpt/point 0 0))]
|
||||
|
||||
(swap! idmap assoc label (:id instance-shape))
|
||||
(-> file-data
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue