Merge pull request #2201 from penpot/hiru-undelete-components

🎉 Allow to restore deleted components
This commit is contained in:
Andrey Antukh 2022-09-05 14:26:14 +02:00 committed by GitHub
commit 11018581ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 469 additions and 133 deletions

View file

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

View 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

View file

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

View file

@ -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 [_]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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