0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-18 10:41:29 -05:00

🎉 Import & export new components

This commit is contained in:
Andrés Moya 2022-08-31 17:48:02 +02:00
parent 251e7eada2
commit 46053b6bbf
15 changed files with 283 additions and 122 deletions

View file

@ -750,6 +750,10 @@
(uuid? (:typography-ref-file form))
(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
;; the :file-id prop)
(uuid? (:file-id form))

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

@ -370,8 +370,8 @@
(assoc :objects objects))))
(defmethod process-change :del-component
[data {:keys [id]}]
(ctf/delete-component data id))
[data {:keys [id skip-undelete?]}]
(ctf/delete-component data id skip-undelete?))
(defmethod process-change :restore-component
[data {:keys [id]}]

View file

@ -159,8 +159,11 @@
(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]))
(s/keys :req-un [::id]
:opt-un [:internal.changes.del-component/skip-undelete?]))
(defmethod change-spec :restore-component [_]
(s/keys :req-un [::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

@ -124,29 +124,32 @@
"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]
(let [components-v2 (get-in file-data [:options :components-v2])
([file-data component-id]
(delete-component file-data component-id false))
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)))]
([file-data component-id skip-undelete?]
(let [components-v2 (get-in file-data [:options :components-v2])
(cond-> file-data
components-v2
(add-to-deleted-components)
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)))]
:always
(ctkl/delete-component component-id))))
(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."
@ -253,7 +256,7 @@
component
(:id file-data)
position
true)
{:main-instance? true})
add-shapes
(fn [page]
@ -317,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

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

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

View file

@ -118,8 +118,7 @@
:name (:name new-component-shape)
:objects (d/index-by :id new-component-shapes)}
(:component-file main-instance-shape)
position
false))]
position))]
[new-component-shape new-component-shapes
new-instance-shape new-instance-shapes]))
@ -130,7 +129,7 @@
(let [component (cph/get-component libraries file-id component-id)
[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})
(pcb/empty-changes it (:id page))

View file

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

View file

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

View file

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

View file

@ -400,7 +400,8 @@
component-id (get-meta node :component-id uuid/uuid)
component-file (get-meta node :component-file 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
(some? stroke-color-ref-id)
@ -414,6 +415,9 @@
component-root?
(assoc :component-root? component-root?)
main-instance?
(assoc :main-instance? main-instance?)
(some? shape-ref)
(assoc :shape-ref shape-ref))))

View file

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

View file

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