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

@ -750,6 +750,10 @@
(uuid? (:typography-ref-file form)) (uuid? (:typography-ref-file form))
(update :typography-ref-file lookup-index) (update :typography-ref-file lookup-index)
;; This covers the component instance links
(uuid? (:component-file form))
(update :component-file lookup-index)
;; This covers the shadows and grids (they have directly ;; This covers the shadows and grids (they have directly
;; the :file-id prop) ;; the :file-id prop)
(uuid? (:file-id form)) (uuid? (:file-id form))

View file

@ -9,12 +9,16 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.pages.changes :as ch] [app.common.pages.changes :as ch]
[app.common.pages.changes-spec :as pcs] [app.common.pages.changes-spec :as pcs]
[app.common.spec :as us] [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.file :as ctf]
[app.common.types.page :as ctp] [app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -516,10 +520,17 @@
[file data] [file data]
(let [selrect cts/empty-selrect (let [selrect cts/empty-selrect
name (:name data) name (:name data)
path (:path 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) obj (-> (cts/make-minimal-group nil selrect name)
(merge data) (merge data)
(dissoc :path
:main-instance-id
:main-instance-page
:main-instance-x
:main-instance-y)
(check-name file :group) (check-name file :group)
(d/without-nils))] (d/without-nils))]
(-> file (-> file
@ -528,6 +539,8 @@
:id (:id obj) :id (:id obj)
:name name :name name
:path path :path path
:main-instance-id main-instance-id
:main-instance-page main-instance-page
:shapes [obj]}) :shapes [obj]})
(assoc :last-id (:id obj)) (assoc :last-id (:id obj))
@ -546,7 +559,8 @@
(commit-change (commit-change
file file
{:type :del-component {:type :del-component
:id component-id}) :id component-id
:skip-undelete? true})
(:masked-group? component) (:masked-group? component)
(let [mask (first children)] (let [mask (first children)]
@ -586,6 +600,42 @@
(dissoc :current-component-id) (dissoc :current-component-id)
(update :parent-stack pop)))) (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 (defn delete-object
[file id] [file id]
(let [page-id (:current-page-id file)] (let [page-id (:current-page-id file)]

View file

@ -19,6 +19,7 @@
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.colors-list :as ctcl] [app.common.types.colors-list :as ctcl]
[app.common.types.file :as ctf]
[app.common.types.page :as ctp] [app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl] [app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
@ -302,9 +303,7 @@
(defmethod process-change :del-page (defmethod process-change :del-page
[data {:keys [id]}] [data {:keys [id]}]
(-> data (ctpl/delete-page data id))
(update :pages (fn [pages] (filterv #(not= % id) pages)))
(update :pages-index dissoc id)))
(defmethod process-change :mov-page (defmethod process-change :mov-page
[data {:keys [id index]}] [data {:keys [id index]}]
@ -320,7 +319,7 @@
(defmethod process-change :del-color (defmethod process-change :del-color
[data {:keys [id]}] [data {:keys [id]}]
(update data :colors dissoc id)) (ctcl/delete-color data id))
(defmethod process-change :add-recent-color (defmethod process-change :add-recent-color
[data {:keys [color]}] [data {:keys [color]}]
@ -371,8 +370,16 @@
(assoc :objects objects)))) (assoc :objects objects))))
(defmethod process-change :del-component (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]}] [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 ;; -- Typography
@ -386,7 +393,7 @@
(defmethod process-change :del-typography (defmethod process-change :del-typography
[data {:keys [id]}] [data {:keys [id]}]
(update data :typographies dissoc id)) (ctyl/delete-typography data id))
;; === Operations ;; === Operations

View file

@ -617,18 +617,34 @@
changes))) changes)))
(defn delete-component (defn delete-component
[changes id] [changes id components-v2]
(assert-library changes) (assert-library changes)
(let [library-data (::library-data (meta changes)) (let [library-data (::library-data (meta changes))
prev-component (get-in library-data [:components id])] prev-component (get-in library-data [:components id])]
(-> changes (-> changes
(update :redo-changes conj {:type :del-component (update :redo-changes conj {:type :del-component
:id id}) :id id})
(update :undo-changes d/preconj {:type :add-component (update :undo-changes
:id id (fn [undo-changes]
:name (:name prev-component) (cond-> undo-changes
:path (:path prev-component) components-v2
:main-instance-id (:main-instance-id prev-component) (d/preconj {:type :purge-component
:main-instance-page (:main-instance-page prev-component) :id id})
:shapes (vals (:objects prev-component))}))))
: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] (s/keys :req-un [::id]
:opt-un [::name :internal.changes.add-component/shapes])) :opt-un [::name :internal.changes.add-component/shapes]))
(s/def :internal.changes.del-component/skip-undelete? boolean?)
(defmethod change-spec :del-component [_] (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])) (s/keys :req-un [::id]))
(defmethod change-spec :add-typography [_] (defmethod change-spec :add-typography [_]

View file

@ -22,3 +22,7 @@
[file-data color-id f] [file-data color-id f]
(update-in file-data [:colors 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] [file-data component-id f]
(update-in file-data [:components 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 "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 each new shape to the corresponding one of the component. Place the new instance
coordinates in the given position." coordinates in the given position."
[container component component-file-id position main-instance?] ([container component component-file-id position]
(let [component-shape (get-shape component (:id component)) (make-component-instance container component component-file-id position {}))
orig-pos (gpt/point (:x component-shape) (:y component-shape)) ([container component component-file-id position
delta (gpt/subtract position orig-pos) {:keys [main-instance? force-id] :or {main-instance? false force-id nil}}]
(let [component-shape (get-shape component (:id component))
objects (:objects container) orig-pos (gpt/point (:x component-shape) (:y component-shape))
unames (volatile! (ctst/retrieve-used-names objects)) 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 frame-id (ctst/frame-id-by-position objects (gpt/add orig-pos delta))
(fn [new-shape original-shape]
(let [new-name (ctst/generate-unique-name @unames (:name new-shape))]
(when (nil? (:parent-id original-shape)) update-new-shape
(vswap! unames conj new-name)) (fn [new-shape original-shape]
(let [new-name (ctst/generate-unique-name @unames (:name new-shape))]
(cond-> new-shape (when (nil? (:parent-id original-shape))
true (vswap! unames conj new-name))
(as-> $
(gsh/move $ delta)
(assoc $ :frame-id frame-id)
(assoc $ :parent-id
(or (:parent-id $) (:frame-id $)))
(dissoc $ :touched))
(nil? (:shape-ref original-shape)) (cond-> new-shape
(assoc :shape-ref (:id original-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)) (nil? (:shape-ref original-shape))
(assoc :component-id (:id original-shape) (assoc :shape-ref (:id original-shape))
:component-file component-file-id
:component-root? true
:name new-name)
(and (nil? (:parent-id original-shape)) main-instance?) (nil? (:parent-id original-shape))
(assoc :main-instance? true) (assoc :component-id (:id original-shape)
:component-file component-file-id
:component-root? true
:name new-name)
(some? (:parent-id original-shape)) (and (nil? (:parent-id original-shape)) main-instance?)
(dissoc :component-root?)))) (assoc :main-instance? true)
[new-shape new-shapes _] (some? (:parent-id original-shape))
(ctst/clone-object component-shape (dissoc :component-root?))))
nil
(get component :objects) [new-shape new-shapes _]
update-new-shape)] (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 ;; 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? (defmulti uses-asset?
"Checks if a shape uses the given asset." "Checks if a shape uses the given asset."
(fn [asset-type _ _ _] asset-type)) (fn [asset-type _ _ _] asset-type))
@ -185,7 +236,7 @@
(defn migrate-to-components-v2 (defn migrate-to-components-v2
"If there is any component in the file library, add a new 'Library backup' and generate "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] [file-data]
(let [components (ctkl/components-seq file-data)] (let [components (ctkl/components-seq file-data)]
(if (or (empty? components) (if (or (empty? components)
@ -205,7 +256,7 @@
component component
(:id file-data) (:id file-data)
position position
true) {:main-instance? true})
add-shapes add-shapes
(fn [page] (fn [page]
@ -269,7 +320,7 @@
component component
(:id file-data) (:id file-data)
position position
true) {:main-instance? true})
; Add all shapes of the main instance to the library page ; Add all shapes of the main instance to the library page
add-main-instance-shapes add-main-instance-shapes

View file

@ -37,3 +37,9 @@
[file-data page-id f] [file-data page-id f]
(update-in file-data [:pages-index 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." the order of the children of each parent."
([object parent-id objects update-new-object] ([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] ([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)) (loop [child-ids (seq (:shapes object))
new-direct-children [] new-direct-children []
new-children [] new-children []

View file

@ -22,3 +22,7 @@
[file-data typography-id f] [file-data typography-id f]
(update-in file-data [:typographies 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) (ctn/make-component-instance (ctpl/get-page file-data page-id)
(ctkl/get-component (:data library) component-id) (ctkl/get-component (:data library) component-id)
(:id library) (:id library)
(gpt/point 0 0) (gpt/point 0 0))]
false)]
(swap! idmap assoc label (:id instance-shape)) (swap! idmap assoc label (:id instance-shape))
(-> file-data (-> file-data

View file

@ -18,6 +18,7 @@
[app.common.types.color :as ctc] [app.common.types.color :as ctc]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape-tree :as ctst] [app.common.types.shape-tree :as ctst]
[app.common.types.typography :as ctt] [app.common.types.typography :as ctt]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -393,13 +394,50 @@
(ptk/reify ::delete-component (ptk/reify ::delete-component
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [data (get state :workspace-data) (let [data (get state :workspace-data)
components-v2 (features/active-feature? state :components-v2)
changes (-> (pcb/empty-changes it) changes (-> (pcb/empty-changes it)
(pcb/with-library-data data) (pcb/with-library-data data)
(pcb/delete-component id))] (pcb/delete-component id components-v2))]
(rx/of (dch/commit-changes changes)))))) (rx/of (dch/commit-changes changes))))))
(defn restore-component
"Restore a deleted component, with the given id, on the current file library."
[id]
(us/assert ::us/uuid id)
(ptk/reify ::restore-component
ptk/WatchEvent
(watch [it state _]
(let [data (get state :workspace-data)
component (ctf/get-deleted-component data id)
page (ctpl/get-page data (:main-instance-page component))
; Make a new main instance, with the same id of the original
[_main-instance shapes]
(ctn/make-component-instance page
component
(:id data)
(gpt/point (:main-instance-x component)
(:main-instance-y component))
{:main-instance? true
:force-id (:main-instance-id component)})
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/with-page page))
changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true})
changes
shapes)
; restore-component change needs to be done after add main instance
; because when undo changes, the orden is inverse
changes (pcb/restore-component changes id)]
(rx/of (dch/commit-changes changes))))))
(defn instantiate-component (defn instantiate-component
"Create a new shape in the current page, from the component with the given id "Create a new shape in the current page, from the component with the given id
in the given file library. Then selects the newly created instance." in the given file library. Then selects the newly created instance."

View file

@ -118,8 +118,7 @@
:name (:name new-component-shape) :name (:name new-component-shape)
:objects (d/index-by :id new-component-shapes)} :objects (d/index-by :id new-component-shapes)}
(:component-file main-instance-shape) (:component-file main-instance-shape)
position position))]
false))]
[new-component-shape new-component-shapes [new-component-shape new-component-shapes
new-instance-shape new-instance-shapes])) new-instance-shape new-instance-shapes]))
@ -130,7 +129,7 @@
(let [component (cph/get-component libraries file-id component-id) (let [component (cph/get-component libraries file-id component-id)
[new-shape new-shapes] [new-shape new-shapes]
(ctn/make-component-instance page component file-id position false) (ctn/make-component-instance page component file-id position)
changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true})
(pcb/empty-changes it (:id page)) (pcb/empty-changes it (:id page))

View file

@ -233,7 +233,16 @@
(pcb/with-page page) (pcb/with-page page)
(pcb/with-objects objects) (pcb/with-objects objects)
(pcb/with-library-data file) (pcb/with-library-data file)
(pcb/set-page-option :guides guides) (pcb/set-page-option :guides guides))
changes (reduce (fn [changes component-id]
;; It's important to delete the component before the main instance, because we
;; need to store the instance position if we want to restore it later.
(pcb/delete-component changes component-id components-v2))
changes
components-to-delete)
changes (-> changes
(pcb/remove-objects all-children) (pcb/remove-objects all-children)
(pcb/remove-objects ids) (pcb/remove-objects ids)
(pcb/remove-objects empty-parents) (pcb/remove-objects empty-parents)
@ -252,12 +261,7 @@
(cond-> (seq starting-flows) (cond-> (seq starting-flows)
(pcb/update-page-option :flows (fn [flows] (pcb/update-page-option :flows (fn [flows]
(->> (map :id starting-flows) (->> (map :id starting-flows)
(reduce ctp/remove-flow flows)))))) (reduce ctp/remove-flow flows))))))]
changes (reduce (fn [changes component-id]
(pcb/delete-component changes component-id))
changes
components-to-delete)]
(rx/of (dc/detach-comment-thread ids) (rx/of (dc/detach-comment-thread ids)
(dwsl/update-layout-positions all-parents) (dwsl/update-layout-positions all-parents)

View file

@ -220,7 +220,7 @@
:fill "none"} :fill "none"}
(when include-metadata? (when include-metadata?
[:& export/export-page {:options (:options data)}]) [:& export/export-page {:id (:id data) :options (:options data)}])
(let [shapes (->> shapes (let [shapes (->> shapes
(remove cph/frame-shape?) (remove cph/frame-shape?)
@ -393,6 +393,11 @@
object (get objects id) object (get objects id)
selrect (:selrect object) selrect (:selrect object)
main-instance-id (:main-instance-id data)
main-instance-page (:main-instance-page data)
main-instance-x (:main-instance-x data)
main-instance-y (:main-instance-y data)
vbox vbox
(format-viewbox (format-viewbox
{:width (:width selrect) {:width (:width selrect)
@ -403,7 +408,13 @@
(mf/deps objects) (mf/deps objects)
(fn [] (group-wrapper-factory objects)))] (fn [] (group-wrapper-factory objects)))]
[:> "symbol" #js {:id (str id) :viewBox vbox "penpot:path" path} [:> "symbol" #js {:id (str id)
:viewBox vbox
"penpot:path" path
"penpot:main-instance-id" main-instance-id
"penpot:main-instance-page" main-instance-page
"penpot:main-instance-x" main-instance-x
"penpot:main-instance-y" main-instance-y}
[:title name] [:title name]
[:> shape-container {:shape object} [:> shape-container {:shape object}
[:& group-wrapper {:shape object :view-box vbox}]]])) [:& group-wrapper {:shape object :view-box vbox}]]]))
@ -414,7 +425,8 @@
(let [data (obj/get props "data") (let [data (obj/get props "data")
children (obj/get props "children") children (obj/get props "children")
render-embed? (obj/get props "render-embed?") render-embed? (obj/get props "render-embed?")
include-metadata? (obj/get props "include-metadata?")] include-metadata? (obj/get props "include-metadata?")
source (keyword (obj/get props "source" "components"))]
[:& (mf/provider embed/context) {:value render-embed?} [:& (mf/provider embed/context) {:value render-embed?}
[:& (mf/provider export/include-metadata-ctx) {:value include-metadata?} [:& (mf/provider export/include-metadata-ctx) {:value include-metadata?}
[:svg {:version "1.1" [:svg {:version "1.1"
@ -424,7 +436,7 @@
:style {:display (when-not (some? children) "none")} :style {:display (when-not (some? children) "none")}
:fill "none"} :fill "none"}
[:defs [:defs
(for [[id data] (:components data)] (for [[id data] (source data)]
[:& component-symbol {:id id :key (dm/str id) :data data}])] [:& component-symbol {:id id :key (dm/str id) :data data}])]
children]]])) children]]]))
@ -482,9 +494,9 @@
(rds/renderToStaticMarkup elem))))))) (rds/renderToStaticMarkup elem)))))))
(defn render-components (defn render-components
[data] [data source]
(let [;; Join all components objects into a single map (let [;; Join all components objects into a single map
objects (->> (:components data) objects (->> (source data)
(vals) (vals)
(map :objects) (map :objects)
(reduce conj))] (reduce conj))]
@ -498,5 +510,6 @@
(rx/map (rx/map
(fn [data] (fn [data]
(let [elem (mf/element components-sprite-svg (let [elem (mf/element components-sprite-svg
#js {:data data :render-embed? true :include-metadata? true})] #js {:data data :render-embed? true :include-metadata? true
:source (name source)})]
(rds/renderToStaticMarkup elem)))))))) (rds/renderToStaticMarkup elem))))))))

View file

@ -13,6 +13,7 @@
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.messages :as msg] [app.main.data.messages :as msg]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.features :as features]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
@ -247,6 +248,8 @@
:files (->> files :files (->> files
(mapv #(assoc % :status :analyzing)))}) (mapv #(assoc % :status :analyzing)))})
components-v2 (features/use-feature :components-v2)
analyze-import analyze-import
(mf/use-callback (mf/use-callback
(fn [files] (fn [files]
@ -268,6 +271,7 @@
:num-files (count files)})) :num-files (count files)}))
(->> (uw/ask-many! (->> (uw/ask-many!
{:cmd :import-files {:cmd :import-files
:components-v2 components-v2
:project-id project-id :project-id project-id
:files files}) :files files})
(rx/subs (rx/subs

View file

@ -54,7 +54,8 @@
([props attr trfn] ([props attr trfn]
(let [val (get shape attr) (let [val (get shape attr)
val (if (keyword? val) (d/name val) val) val (if (keyword? val) (d/name val) val)
ns-attr (str "penpot:" (-> attr d/name))] ns-attr (-> (str "penpot:" (-> attr d/name))
(str/strip-suffix "?"))]
(cond-> props (cond-> props
(some? val) (some? val)
(obj/set! ns-attr (trfn val))))))) (obj/set! ns-attr (trfn val)))))))
@ -136,7 +137,8 @@
(add! :typography-ref-file) (add! :typography-ref-file)
(add! :component-file) (add! :component-file)
(add! :component-id) (add! :component-id)
(add! :component-root) (add! :component-root?)
(add! :main-instance?)
(add! :shape-ref)))) (add! :shape-ref))))
(defn prefix-keys [m] (defn prefix-keys [m]
@ -177,11 +179,11 @@
:axis (d/name axis)}])]) :axis (d/name axis)}])])
(mf/defc export-page (mf/defc export-page
[{:keys [options]}] [{:keys [id options]}]
(let [saved-grids (get options :saved-grids) (let [saved-grids (get options :saved-grids)
flows (get options :flows) flows (get options :flows)
guides (get options :guides)] guides (get options :guides)]
[:> "penpot:page" #js {} [:> "penpot:page" #js {:id id}
(when (d/not-empty? saved-grids) (when (d/not-empty? saved-grids)
(let [parse-grid (fn [[type params]] {:type type :params params}) (let [parse-grid (fn [[type params]] {:type type :params params})
grids (->> saved-grids (mapv parse-grid))] grids (->> saved-grids (mapv parse-grid))]

View file

@ -10,6 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.types.components-list :as ctkl]
[app.common.types.page :as ctp] [app.common.types.page :as ctp]
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@ -18,6 +19,7 @@
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.shortcuts :as sc]
[app.main.features :as features]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
@ -373,9 +375,19 @@
component-file (-> shapes first :component-file) component-file (-> shapes first :component-file)
component-shapes (filter #(contains? % :component-id) shapes) component-shapes (filter #(contains? % :component-id) shapes)
components-v2 (features/use-feature :components-v2)
current-file-id (mf/use-ctx ctx/current-file-id) current-file-id (mf/use-ctx ctx/current-file-id)
local-component? (= component-file current-file-id) local-component? (= component-file current-file-id)
local-library (when local-component?
;; Not needed to subscribe to changes because it's not expected
;; to change while context menu is open
(deref refs/workspace-local-library))
main-component (when local-component?
(ctkl/get-component local-library (:component-id (first shapes))))
do-add-component #(st/emit! (dwl/add-component)) do-add-component #(st/emit! (dwl/add-component))
do-detach-component #(st/emit! (dwl/detach-component shape-id)) do-detach-component #(st/emit! (dwl/detach-component shape-id))
do-detach-component-in-bulk #(st/emit! dwl/detach-selected-components) do-detach-component-in-bulk #(st/emit! dwl/detach-selected-components)
@ -384,6 +396,7 @@
do-navigate-component-file #(st/emit! (dwl/nav-to-component-file component-file)) do-navigate-component-file #(st/emit! (dwl/nav-to-component-file component-file))
do-update-component #(st/emit! (dwl/update-component-sync shape-id component-file)) do-update-component #(st/emit! (dwl/update-component-sync shape-id component-file))
do-update-component-in-bulk #(st/emit! (dwl/update-component-in-bulk component-shapes component-file)) do-update-component-in-bulk #(st/emit! (dwl/update-component-in-bulk component-shapes component-file))
do-restore-component #(st/emit! (dwl/restore-component component-id))
do-update-remote-component do-update-remote-component
#(st/emit! (modal/show #(st/emit! (modal/show
@ -436,11 +449,14 @@
(if local-component? (if local-component?
[:* (if (and (nil? main-component) components-v2)
[:& menu-entry {:title (tr "workspace.shape.menu.update-main") [:& menu-entry {:title (tr "workspace.shape.menu.restore-main")
:on-click do-update-component}] :on-click do-restore-component}]
[:& menu-entry {:title (tr "workspace.shape.menu.show-main") [:*
:on-click do-show-component}]] [:& menu-entry {:title (tr "workspace.shape.menu.update-main")
:on-click do-update-component}]
[:& menu-entry {:title (tr "workspace.shape.menu.show-main")
:on-click do-show-component}]])
[:* [:*
[:& menu-entry {:title (tr "workspace.shape.menu.go-main") [:& menu-entry {:title (tr "workspace.shape.menu.go-main")

View file

@ -6,9 +6,11 @@
(ns app.main.ui.workspace.sidebar.options.menus.component (ns app.main.ui.workspace.sidebar.options.menus.component
(:require (:require
[app.common.types.components-list :as ctkl]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
@ -34,6 +36,15 @@
(:main-instance? values) (:main-instance? values)
true) true)
local-component? (= library-id current-file-id)
local-library (when local-component?
;; Not needed to subscribe to changes because it's not expected
;; to change while context menu is open
(deref refs/workspace-local-library))
main-component (when local-component?
(ctkl/get-component local-library component-id))
on-menu-click on-menu-click
(mf/use-callback (mf/use-callback
(fn [event] (fn [event]
@ -54,6 +65,9 @@
do-update-component do-update-component
#(st/emit! (dwl/update-component-sync id library-id)) #(st/emit! (dwl/update-component-sync id library-id))
do-restore-component
#(st/emit! (dwl/restore-component component-id))
do-update-remote-component do-update-remote-component
#(st/emit! (modal/show #(st/emit! (modal/show
{:type :confirm {:type :confirm
@ -85,11 +99,13 @@
;; app/main/ui/workspace/context_menu.cljs ;; app/main/ui/workspace/context_menu.cljs
[:& context-menu {:on-close on-menu-close [:& context-menu {:on-close on-menu-close
:show (:menu-open @local) :show (:menu-open @local)
:options (if (= library-id current-file-id) :options (if local-component?
[[(tr "workspace.shape.menu.detach-instance") do-detach-component] (if (and (nil? main-component) components-v2)
[(tr "workspace.shape.menu.reset-overrides") do-reset-component] [[(tr "workspace.shape.menu.restore-main") do-restore-component]]
[(tr "workspace.shape.menu.update-main") do-update-component] [[(tr "workspace.shape.menu.detach-instance") do-detach-component]
[(tr "workspace.shape.menu.show-main") do-show-component]] [(tr "workspace.shape.menu.reset-overrides") do-reset-component]
[(tr "workspace.shape.menu.update-main") do-update-component]
[(tr "workspace.shape.menu.show-main") do-show-component]])
[[(tr "workspace.shape.menu.detach-instance") do-detach-component] [[(tr "workspace.shape.menu.detach-instance") do-detach-component]
[(tr "workspace.shape.menu.reset-overrides") do-reset-component] [(tr "workspace.shape.menu.reset-overrides") do-reset-component]

View file

@ -400,7 +400,8 @@
component-id (get-meta node :component-id uuid/uuid) component-id (get-meta node :component-id uuid/uuid)
component-file (get-meta node :component-file uuid/uuid) component-file (get-meta node :component-file uuid/uuid)
shape-ref (get-meta node :shape-ref uuid/uuid) shape-ref (get-meta node :shape-ref uuid/uuid)
component-root? (get-meta node :component-root str->bool)] component-root? (get-meta node :component-root str->bool)
main-instance? (get-meta node :main-instance str->bool)]
(cond-> props (cond-> props
(some? stroke-color-ref-id) (some? stroke-color-ref-id)
@ -414,6 +415,9 @@
component-root? component-root?
(assoc :component-root? component-root?) (assoc :component-root? component-root?)
main-instance?
(assoc :main-instance? main-instance?)
(some? shape-ref) (some? shape-ref)
(assoc :shape-ref shape-ref)))) (assoc :shape-ref shape-ref))))

View file

@ -40,17 +40,18 @@
(reduce format-page {}))] (reduce format-page {}))]
(-> manifest (-> manifest
(assoc (str (:id file)) (assoc (str (:id file))
{:name name {:name name
:shared is-shared :shared is-shared
:pages pages :pages pages
:pagesIndex index :pagesIndex index
:version current-version :version current-version
:libraries (->> (:libraries file) (into #{}) (mapv str)) :libraries (->> (:libraries file) (into #{}) (mapv str))
:exportType (d/name export-type) :exportType (d/name export-type)
:hasComponents (d/not-empty? (get-in file [:data :components])) :hasComponents (d/not-empty? (get-in file [:data :components]))
:hasMedia (d/not-empty? (get-in file [:data :media])) :hasDeletedComponents (d/not-empty? (get-in file [:data :deleted-components]))
:hasColors (d/not-empty? (get-in file [:data :colors])) :hasMedia (d/not-empty? (get-in file [:data :media]))
:hasTypographies (d/not-empty? (get-in file [:data :typographies]))}))))] :hasColors (d/not-empty? (get-in file [:data :colors]))
:hasTypographies (d/not-empty? (get-in file [:data :typographies]))}))))]
(let [manifest {:teamId (str team-id) (let [manifest {:teamId (str team-id)
:fileId (str file-id) :fileId (str file-id)
:files (->> (vals files) (reduce format-file {}))}] :files (->> (vals files) (reduce format-file {}))}]
@ -146,9 +147,14 @@
(defn parse-library-components (defn parse-library-components
[file] [file]
(->> (r/render-components (:data file)) (->> (r/render-components (:data file) :components)
(rx/map #(vector (str (:id file) "/components.svg") %)))) (rx/map #(vector (str (:id file) "/components.svg") %))))
(defn parse-deleted-components
[file]
(->> (r/render-components (:data file) :deleted-components)
(rx/map #(vector (str (:id file) "/deleted-components.svg") %))))
(defn fetch-file-with-libraries [file-id components-v2] (defn fetch-file-with-libraries [file-id components-v2]
(->> (rx/zip (rp/query :file {:id file-id :components-v2 components-v2}) (->> (rx/zip (rp/query :file {:id file-id :components-v2 components-v2})
(rp/query :file-libraries {:file-id file-id})) (rp/query :file-libraries {:file-id file-id}))
@ -426,6 +432,12 @@
(rx/filter #(d/not-empty? (get-in % [:data :components]))) (rx/filter #(d/not-empty? (get-in % [:data :components])))
(rx/flat-map parse-library-components)) (rx/flat-map parse-library-components))
deleted-components-stream
(->> files-stream
(rx/flat-map vals)
(rx/filter #(d/not-empty? (get-in % [:data :deleted-components])))
(rx/flat-map parse-deleted-components))
pages-stream pages-stream
(->> render-stream (->> render-stream
(rx/map collect-page))] (rx/map collect-page))]
@ -441,6 +453,7 @@
manifest-stream manifest-stream
pages-stream pages-stream
components-stream components-stream
deleted-components-stream
media-stream media-stream
colors-stream colors-stream
typographies-stream) typographies-stream)

View file

@ -46,14 +46,15 @@
([context type id media] ([context type id media]
(let [file-id (:file-id context) (let [file-id (:file-id context)
path (case type path (case type
:manifest (str "manifest.json") :manifest (str "manifest.json")
:page (str file-id "/" id ".svg") :page (str file-id "/" id ".svg")
:colors (str file-id "/colors.json") :colors (str file-id "/colors.json")
:typographies (str file-id "/typographies.json") :typographies (str file-id "/typographies.json")
:media-list (str file-id "/media.json") :media-list (str file-id "/media.json")
:media (let [ext (cm/mtype->extension (:mtype media))] :media (let [ext (cm/mtype->extension (:mtype media))]
(str/concat file-id "/media/" id ext)) (str/concat file-id "/media/" id ext))
:components (str file-id "/components.svg")) :components (str file-id "/components.svg")
:deleted-components (str file-id "/deleted-components.svg"))
parse-svg? (and (not= type :media) (str/ends-with? path "svg")) parse-svg? (and (not= type :media) (str/ends-with? path "svg"))
parse-json? (and (not= type :media) (str/ends-with? path "json")) parse-json? (and (not= type :media) (str/ends-with? path "json"))
@ -125,7 +126,7 @@
(defn create-file (defn create-file
"Create a new file on the back-end" "Create a new file on the back-end"
[context] [context components-v2]
(let [resolve (:resolve context) (let [resolve (:resolve context)
file-id (resolve (:file-id context))] file-id (resolve (:file-id context))]
(rp/mutation :create-temp-file (rp/mutation :create-temp-file
@ -133,7 +134,9 @@
:name (:name context) :name (:name context)
:is-shared (:shared context) :is-shared (:shared context)
:project-id (:project-id context) :project-id (:project-id context)
:data (-> ctf/empty-file-data (assoc :id file-id))}))) :data (-> ctf/empty-file-data
(assoc :id file-id)
(assoc-in [:options :components-v2] components-v2))})))
(defn link-file-libraries (defn link-file-libraries
"Create a new file on the back-end" "Create a new file on the back-end"
@ -380,18 +383,22 @@
(rx/map (comp fb/close-page setup-interactions)))))))) (rx/map (comp fb/close-page setup-interactions))))))))
(defn import-component [context file node] (defn import-component [context file node]
(let [resolve (:resolve context) (let [resolve (:resolve context)
content (cip/find-node node :g) content (cip/find-node node :g)
file-id (:id file) file-id (:id file)
old-id (cip/get-id node) old-id (cip/get-id node)
id (resolve old-id) id (resolve old-id)
path (get-in node [:attrs :penpot:path] "") path (get-in node [:attrs :penpot:path] "")
data (-> (cip/parse-data :group content) main-instance-id (resolve (uuid (get-in node [:attrs :penpot:main-instance-id] "")))
(assoc :path path) main-instance-page (resolve (uuid (get-in node [:attrs :penpot:main-instance-page] "")))
(assoc :id id)) data (-> (cip/parse-data :group content)
(assoc :path path)
(assoc :id id)
(assoc :main-instance-id main-instance-id)
(assoc :main-instance-page main-instance-page))
file (-> file (fb/start-component data)) file (-> file (fb/start-component data))
children (cip/node-seq node)] children (cip/node-seq node)]
(->> (rx/from children) (->> (rx/from children)
(rx/filter cip/shape?) (rx/filter cip/shape?)
@ -401,6 +408,43 @@
(rx/reduce (partial process-import-node context) file) (rx/reduce (partial process-import-node context) file)
(rx/map fb/finish-component)))) (rx/map fb/finish-component))))
(defn import-deleted-component [context file node]
(let [resolve (:resolve context)
content (cip/find-node node :g)
file-id (:id file)
old-id (cip/get-id node)
id (resolve old-id)
path (get-in node [:attrs :penpot:path] "")
main-instance-id (resolve (uuid (get-in node [:attrs :penpot:main-instance-id] "")))
main-instance-page (resolve (uuid (get-in node [:attrs :penpot:main-instance-page] "")))
main-instance-x (get-in node [:attrs :penpot:main-instance-x] "")
main-instance-y (get-in node [:attrs :penpot:main-instance-y] "")
data (-> (cip/parse-data :group content)
(assoc :path path)
(assoc :id id)
(assoc :main-instance-id main-instance-id)
(assoc :main-instance-page main-instance-page)
(assoc :main-instance-x main-instance-x)
(assoc :main-instance-y main-instance-y))
file (-> file (fb/start-component data))
component-id (:current-component-id file)
children (cip/node-seq node)]
(->> (rx/from children)
(rx/filter cip/shape?)
(rx/skip 1)
(rx/skip-last 1)
(rx/mapcat (partial resolve-media context file-id))
(rx/reduce (partial process-import-node context) file)
(rx/map fb/finish-component)
(rx/map (partial fb/finish-deleted-component
component-id
main-instance-page
main-instance-x
main-instance-y)))))
(defn process-pages (defn process-pages
[context file] [context file]
(let [index (:pages-index context) (let [index (:pages-index context)
@ -486,6 +530,18 @@
(rx/concat-reduce (partial import-component context) file))) (rx/concat-reduce (partial import-component context) file)))
(rx/of file))) (rx/of file)))
(defn process-deleted-components
[context file]
(if (:has-deleted-components context)
(let [split-components
(fn [content] (->> (cip/node-seq content)
(filter #(= :symbol (:tag %)))))]
(->> (get-file context :deleted-components)
(rx/flat-map split-components)
(rx/concat-reduce (partial import-deleted-component context) file)))
(rx/of file)))
(defn process-file (defn process-file
[context file] [context file]
@ -502,18 +558,20 @@
(rx/flat-map (partial process-library-media context)) (rx/flat-map (partial process-library-media context))
(rx/tap #(progress! context :process-components)) (rx/tap #(progress! context :process-components))
(rx/flat-map (partial process-library-components context)) (rx/flat-map (partial process-library-components context))
(rx/tap #(progress! context :process-deleted-components))
(rx/flat-map (partial process-deleted-components context))
(rx/flat-map (partial send-changes context)) (rx/flat-map (partial send-changes context))
(rx/tap #(rx/end! progress-str)))])) (rx/tap #(rx/end! progress-str)))]))
(defn create-files (defn create-files
[context files] [context files components-v2]
(let [data (group-by :file-id files)] (let [data (group-by :file-id files)]
(rx/concat (rx/concat
(->> (rx/from files) (->> (rx/from files)
(rx/map #(merge context %)) (rx/map #(merge context %))
(rx/flat-map (fn [context] (rx/flat-map (fn [context]
(->> (create-file context) (->> (create-file context components-v2)
(rx/map #(vector % (first (get data (:file-id context))))))))) (rx/map #(vector % (first (get data (:file-id context)))))))))
(->> (rx/from files) (->> (rx/from files)
@ -564,7 +622,7 @@
(rx/catch #(rx/of {:uri (:uri file) :error (.-message %)})))))))) (rx/catch #(rx/of {:uri (:uri file) :error (.-message %)}))))))))
(defmethod impl/handler :import-files (defmethod impl/handler :import-files
[{:keys [project-id files]}] [{:keys [project-id files components-v2]}]
(let [context {:project-id project-id (let [context {:project-id project-id
:resolve (resolve-factory)} :resolve (resolve-factory)}
@ -572,7 +630,7 @@
binary-files (filter #(= "application/octet-stream" (:type %)) files)] binary-files (filter #(= "application/octet-stream" (:type %)) files)]
(->> (rx/merge (->> (rx/merge
(->> (create-files context zip-files) (->> (create-files context zip-files components-v2)
(rx/flat-map (rx/flat-map
(fn [[file data]] (fn [[file data]]
(->> (uz/load-from-url (:uri data)) (->> (uz/load-from-url (:uri data))

View file

@ -4303,6 +4303,9 @@ msgstr "Update main components"
msgid "workspace.shape.menu.update-main" msgid "workspace.shape.menu.update-main"
msgstr "Update main component" msgstr "Update main component"
msgid "workspace.shape.menu.restore-main"
msgstr "Restore main component"
#: src/app/main/ui/workspace/left_toolbar.cljs #: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.history" msgid "workspace.sidebar.history"
msgstr "History (%s)" msgstr "History (%s)"

View file

@ -4498,6 +4498,9 @@ msgstr "Actualizar componentes"
msgid "workspace.shape.menu.update-main" msgid "workspace.shape.menu.update-main"
msgstr "Actualizar componente principal" msgstr "Actualizar componente principal"
msgid "workspace.shape.menu.restore-main"
msgstr "Restaurar componente principal"
#: src/app/main/ui/workspace/left_toolbar.cljs #: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.history" msgid "workspace.sidebar.history"
msgstr "Historial (%s)" msgstr "Historial (%s)"