Merge pull request #1053 from penpot/feat/export-import

Feat/export import
This commit is contained in:
Andrés Moya 2021-06-22 12:02:04 +02:00 committed by GitHub
commit be387ad892
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 503 additions and 238 deletions

View file

@ -17,14 +17,7 @@
{:exclude-files {:exclude-files
["data_readers.clj" ["data_readers.clj"
"app/util/perf.cljs" "app/util/perf.cljs"
"app/common/exceptions.cljc" "app/common/exceptions.cljc"]}
"app/util/import/.*"
"app/worker/export.cljs"
"app/worker/import.cljs"
"app/libs/.*"
"app/main/data/workspace/path/selection.cljs"
"app/main/data/workspace/transforms.cljs"
]}
:linters :linters
{:unsorted-required-namespaces {:unsorted-required-namespaces

View file

@ -556,3 +556,7 @@
:else :else
m))) m)))
(defn not-empty?
[coll]
(boolean (seq coll)))

View file

@ -291,6 +291,31 @@
(-> file (-> file
(update :parent-stack pop))) (update :parent-stack pop)))
(defn add-interaction
[file action-type event-type from-id destination-id]
(assert (some? (lookup-shape file from-id)) (str "Cannot locate shape with id " from-id))
(assert (some? (lookup-shape file destination-id)) (str "Cannot locate shape with id " destination-id))
(let [interactions (->> (lookup-shape file from-id)
:interactions
(filterv #(or (not= (:action-type %) action-type)
(not= (:event-type %) event-type))))
conj (fnil conj [])
interactions (-> interactions
(conj
{:action-type action-type
:event-type event-type
:destination destination-id}))]
(commit-change
file
{:type :mod-obj
:page-id (:current-page-id file)
:id from-id
:operations
[{:type :set :attr :interactions :val interactions}]})))
(defn generate-changes (defn generate-changes
[file] [file]
(:changes file)) (:changes file))

View file

@ -25,59 +25,59 @@
(deftype File [^:mutable file] (deftype File [^:mutable file]
Object Object
(addPage [self name] (addPage [_ name]
(set! file (fb/add-page file {:name name})) (set! file (fb/add-page file {:name name}))
(str (:current-page-id file))) (str (:current-page-id file)))
(addPage [self name options] (addPage [_ name options]
(set! file (fb/add-page file {:name name :options options})) (set! file (fb/add-page file {:name name :options options}))
(str (:current-page-id file))) (str (:current-page-id file)))
(closePage [self] (closePage [_]
(set! file (fb/close-page file))) (set! file (fb/close-page file)))
(addArtboard [self data] (addArtboard [_ data]
(set! file (fb/add-artboard file (parse-data data))) (set! file (fb/add-artboard file (parse-data data)))
(str (:last-id file))) (str (:last-id file)))
(closeArtboard [self data] (closeArtboard [_]
(set! file (fb/close-artboard file))) (set! file (fb/close-artboard file)))
(addGroup [self data] (addGroup [_ data]
(set! file (fb/add-group file (parse-data data))) (set! file (fb/add-group file (parse-data data)))
(str (:last-id file))) (str (:last-id file)))
(closeGroup [self] (closeGroup [_]
(set! file (fb/close-group file))) (set! file (fb/close-group file)))
(createRect [self data] (createRect [_ data]
(set! file (fb/create-rect file (parse-data data))) (set! file (fb/create-rect file (parse-data data)))
(str (:last-id file))) (str (:last-id file)))
(createCircle [self data] (createCircle [_ data]
(set! file (fb/create-circle file (parse-data data))) (set! file (fb/create-circle file (parse-data data)))
(str (:last-id file))) (str (:last-id file)))
(createPath [self data] (createPath [_ data]
(set! file (fb/create-path file (parse-data data))) (set! file (fb/create-path file (parse-data data)))
(str (:last-id file))) (str (:last-id file)))
(createText [self data] (createText [_ data]
(set! file (fb/create-text file (parse-data data))) (set! file (fb/create-text file (parse-data data)))
(str (:last-id file))) (str (:last-id file)))
(createImage [self data] (createImage [_ data]
(set! file (fb/create-image file (parse-data data))) (set! file (fb/create-image file (parse-data data)))
(str (:last-id file))) (str (:last-id file)))
(createSVG [self data] (createSVG [_ data]
(set! file (fb/create-svg-raw file (parse-data data))) (set! file (fb/create-svg-raw file (parse-data data)))
(str (:last-id file))) (str (:last-id file)))
(closeSVG [self] (closeSVG [_]
(set! file (fb/close-svg-raw file))) (set! file (fb/close-svg-raw file)))
(asMap [self] (asMap [_]
(clj->js file))) (clj->js file)))
(defn create-file-export [^string name] (defn create-file-export [^string name]

View file

@ -141,9 +141,9 @@
selected? (contains? selected-points position)] selected? (contains? selected-points position)]
(streams/drag-stream (streams/drag-stream
(rx/of (rx/of
(when-not selected? (selection/select-node position shift? "drag")) (when-not selected? (selection/select-node position shift?))
(drag-selected-points @ms/mouse-position)) (drag-selected-points @ms/mouse-position))
(rx/of (selection/select-node position shift? "click"))))))) (rx/of (selection/select-node position shift?)))))))
(defn drag-selected-points (defn drag-selected-points
[start-position] [start-position]

View file

@ -60,7 +60,7 @@
(some? id) (some? id)
(assoc-in [:workspace-local :edit-path id :selected-points] positions)))))) (assoc-in [:workspace-local :edit-path id :selected-points] positions))))))
(defn select-node [position shift? kk] (defn select-node [position shift?]
(ptk/reify ::select-node (ptk/reify ::select-node
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
@ -79,38 +79,6 @@
(some? id) (some? id)
(assoc-in [:workspace-local :edit-path id :selected-points] selected-points)))))) (assoc-in [:workspace-local :edit-path id :selected-points] selected-points))))))
(defn deselect-node [position shift?]
(ptk/reify ::deselect-node
ptk/UpdateEvent
(update [_ state]
(let [id (get-in state [:workspace-local :edition])]
(-> state
(update-in [:workspace-local :edit-path id :selected-points] (fnil disj #{}) position))))))
(defn add-to-selection-handler [index type]
(ptk/reify ::add-to-selection-handler
ptk/UpdateEvent
(update [_ state]
state)))
(defn add-to-selection-node [index]
(ptk/reify ::add-to-selection-node
ptk/UpdateEvent
(update [_ state]
state)))
(defn remove-from-selection-handler [index]
(ptk/reify ::remove-from-selection-handler
ptk/UpdateEvent
(update [_ state]
state)))
(defn remove-from-selection-node [index]
(ptk/reify ::remove-from-selection-handler
ptk/UpdateEvent
(update [_ state]
state)))
(defn deselect-all [] (defn deselect-all []
(ptk/reify ::deselect-all (ptk/reify ::deselect-all
ptk/UpdateEvent ptk/UpdateEvent
@ -140,7 +108,7 @@
(ptk/reify ::handle-selection (ptk/reify ::handle-selection
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ _ stream]
(let [stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event))) (let [stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event)))
stoper (->> stream (rx/filter stop?)) stoper (->> stream (rx/filter stop?))
from-p @ms/mouse-position] from-p @ms/mouse-position]

View file

@ -98,8 +98,8 @@
;; -- RESIZE ;; -- RESIZE
(defn start-resize (defn start-resize
[handler initial ids shape] [handler ids shape]
(letfn [(resize [shape initial resizing-shapes layout [point lock? point-snap]] (letfn [(resize [shape initial layout [point lock? point-snap]]
(let [{:keys [width height]} (:selrect shape) (let [{:keys [width height]} (:selrect shape)
{:keys [rotation]} shape {:keys [rotation]} shape
rotation (or rotation 0) rotation (or rotation 0)
@ -181,7 +181,7 @@
(rx/switch-map (fn [[point :as current]] (rx/switch-map (fn [[point :as current]]
(->> (snap/closest-snap-point page-id resizing-shapes layout zoom point) (->> (snap/closest-snap-point page-id resizing-shapes layout zoom point)
(rx/map #(conj current %))))) (rx/map #(conj current %)))))
(rx/mapcat (partial resize shape initial-position resizing-shapes layout)) (rx/mapcat (partial resize shape initial-position layout))
(rx/take-until stoper)) (rx/take-until stoper))
(rx/of (apply-modifiers ids) (rx/of (apply-modifiers ids)
(finish-transform)))))))) (finish-transform))))))))
@ -196,7 +196,7 @@
(assoc-in [:workspace-local :transform] :rotate))) (assoc-in [:workspace-local :transform] :rotate)))
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [_ _ stream]
(let [stoper (rx/filter ms/mouse-up? stream) (let [stoper (rx/filter ms/mouse-up? stream)
group (gsh/selection-rect shapes) group (gsh/selection-rect shapes)
group-center (gsh/center-selrect group) group-center (gsh/center-selrect group)
@ -235,7 +235,7 @@
[] []
(ptk/reify ::start-move-selected (ptk/reify ::start-move-selected
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [_ state stream]
(let [initial (deref ms/mouse-position) (let [initial (deref ms/mouse-position)
selected (wsh/lookup-selected state {:omit-blocked? true}) selected (wsh/lookup-selected state {:omit-blocked? true})
stopper (rx/filter ms/mouse-up? stream)] stopper (rx/filter ms/mouse-up? stream)]
@ -259,7 +259,7 @@
(defn start-move-duplicate [from-position] (defn start-move-duplicate [from-position]
(ptk/reify ::start-move-selected (ptk/reify ::start-move-selected
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [_ _ stream]
(->> stream (->> stream
(rx/filter (ptk/type? ::dws/duplicate-selected)) (rx/filter (ptk/type? ::dws/duplicate-selected))
(rx/first) (rx/first)
@ -268,7 +268,7 @@
(defn calculate-frame-for-move [ids] (defn calculate-frame-for-move [ids]
(ptk/reify ::calculate-frame-for-move (ptk/reify ::calculate-frame-for-move
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [it state _]
(let [position @ms/mouse-position (let [position @ms/mouse-position
page-id (:current-page-id state) page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id) objects (wsh/lookup-page-objects state page-id)
@ -313,7 +313,7 @@
(assoc-in [:workspace-local :transform] :move))) (assoc-in [:workspace-local :transform] :move)))
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [_ state stream]
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id) objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state {:omit-blocked? true}) selected (wsh/lookup-selected state {:omit-blocked? true})
@ -349,20 +349,6 @@
(calculate-frame-for-move ids) (calculate-frame-for-move ids)
(finish-transform))))))))) (finish-transform)))))))))
(defn- get-displacement-with-grid
"Retrieve the correct displacement delta point for the
provided direction speed and distances thresholds."
[shape direction options]
(let [grid-x (:grid-x options 10)
grid-y (:grid-y options 10)
x-mod (mod (:x shape) grid-x)
y-mod (mod (:y shape) grid-y)]
(case direction
:up (gpt/point 0 (- (if (zero? y-mod) grid-y y-mod)))
:down (gpt/point 0 (- grid-y y-mod))
:left (gpt/point (- (if (zero? x-mod) grid-x x-mod)) 0)
:right (gpt/point (- grid-x x-mod) 0))))
(defn- get-displacement (defn- get-displacement
"Retrieve the correct displacement delta point for the "Retrieve the correct displacement delta point for the
provided direction speed and distances thresholds." provided direction speed and distances thresholds."
@ -394,7 +380,7 @@
state)) state))
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [_ state stream]
(if (= same-event (get-in state [:workspace-local :current-move-selected])) (if (= same-event (get-in state [:workspace-local :current-move-selected]))
(let [selected (wsh/lookup-selected state {:omit-blocked? true}) (let [selected (wsh/lookup-selected state {:omit-blocked? true})
move-events (->> stream move-events (->> stream
@ -539,7 +525,7 @@
(defn increase-rotation [ids rotation] (defn increase-rotation [ids rotation]
(ptk/reify ::increase-rotation (ptk/reify ::increase-rotation
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [_ state _]
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id) objects (wsh/lookup-page-objects state page-id)
@ -559,7 +545,7 @@
(us/verify (s/coll-of uuid?) ids) (us/verify (s/coll-of uuid?) ids)
(ptk/reify ::apply-modifiers (ptk/reify ::apply-modifiers
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [_ state _]
(let [objects (wsh/lookup-page-objects state) (let [objects (wsh/lookup-page-objects state)
children-ids (->> ids (mapcat #(cp/get-children % objects))) children-ids (->> ids (mapcat #(cp/get-children % objects)))
ids-with-children (d/concat [] children-ids ids) ids-with-children (d/concat [] children-ids ids)
@ -610,11 +596,6 @@
(update [_ state] (update [_ state]
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
update-children
(fn [objects ids modifiers]
(reduce #(assoc-in %1 [%2 :modifiers] modifiers) objects ids))
;; For each shape updates the modifiers given as arguments ;; For each shape updates the modifiers given as arguments
update-shape update-shape
@ -629,7 +610,7 @@
#(reduce update-shape % ids)))) #(reduce update-shape % ids))))
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [_ state _]
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id) objects (wsh/lookup-page-objects state page-id)
ids (d/concat [] ids (mapcat #(cp/get-children % objects) ids))] ids (d/concat [] ids (mapcat #(cp/get-children % objects) ids))]
@ -638,7 +619,7 @@
(defn flip-horizontal-selected [] (defn flip-horizontal-selected []
(ptk/reify ::flip-horizontal-selected (ptk/reify ::flip-horizontal-selected
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [_ state _]
(let [objects (wsh/lookup-page-objects state) (let [objects (wsh/lookup-page-objects state)
selected (wsh/lookup-selected state {:omit-blocked? true}) selected (wsh/lookup-selected state {:omit-blocked? true})
shapes (map #(get objects %) selected) shapes (map #(get objects %) selected)
@ -654,7 +635,7 @@
(defn flip-vertical-selected [] (defn flip-vertical-selected []
(ptk/reify ::flip-vertical-selected (ptk/reify ::flip-vertical-selected
ptk/WatchEvent ptk/WatchEvent
(watch [it state stream] (watch [_ state _]
(let [objects (wsh/lookup-page-objects state) (let [objects (wsh/lookup-page-objects state)
selected (wsh/lookup-selected state {:omit-blocked? true}) selected (wsh/lookup-selected state {:omit-blocked? true})
shapes (map #(get objects %) selected) shapes (map #(get objects %) selected)
@ -687,6 +668,6 @@
[] []
(ptk/reify ::selected-to-path (ptk/reify ::selected-to-path
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state _]
(let [ids (wsh/lookup-selected state {:omit-blocked? true})] (let [ids (wsh/lookup-selected state {:omit-blocked? true})]
(rx/of (dch/update-shapes ids ups/convert-to-path)))))) (rx/of (dch/update-shapes ids ups/convert-to-path))))))

View file

@ -25,6 +25,7 @@
[app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.shapes.svg-raw :as svg-raw] [app.main.ui.shapes.svg-raw :as svg-raw]
[app.main.ui.shapes.text :as text] [app.main.ui.shapes.text :as text]
[app.util.object :as obj]
[app.util.timers :as ts] [app.util.timers :as ts]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
@ -73,10 +74,11 @@
(mf/fnc group-wrapper (mf/fnc group-wrapper
[{:keys [shape frame] :as props}] [{:keys [shape frame] :as props}]
(let [childs (mapv #(get objects %) (:shapes shape))] (let [childs (mapv #(get objects %) (:shapes shape))]
[:& group-shape {:frame frame [:& shape-container {:shape shape}
:shape shape [:& group-shape {:frame frame
:is-child-selected? true :shape shape
:childs childs}])))) :is-child-selected? true
:childs childs}]]))))
(defn svg-raw-wrapper-factory (defn svg-raw-wrapper-factory
[objects] [objects]
@ -207,7 +209,8 @@
:height height :height height
:version "1.1" :version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink" :xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"} :xmlns "http://www.w3.org/2000/svg"
:xmlns:penpot "https://penpot.app/xmlns"}
[:& wrapper {:shape frame :view-box vbox}]])) [:& wrapper {:shape frame :view-box vbox}]]))
(mf/defc component-svg (mf/defc component-svg
@ -238,5 +241,58 @@
:height height :height height
:version "1.1" :version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink" :xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"} :xmlns "http://www.w3.org/2000/svg"
:xmlns:penpot "https://penpot.app/xmlns"}
[:& wrapper {:shape group :view-box vbox}]])) [:& wrapper {:shape group :view-box vbox}]]))
(mf/defc component-symbol
[{:keys [id data] :as props}]
(let [{:keys [name objects]} data
root (get objects id)
{:keys [width height]} (:selrect root)
vbox (str "0 0 " width " " height)
modifier (-> (gpt/point (:x root) (:y root))
(gpt/negate)
(gmt/translate-matrix))
modifier-ids (concat [id] (cp/get-children id objects))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
objects (reduce update-fn objects modifier-ids)
root (assoc-in root [:modifiers :displacement] modifier)
group-wrapper
(mf/use-memo
(mf/deps objects)
#(group-wrapper-factory objects))]
[:symbol {:id (str id)
:viewBox vbox}
[:title name]
[:& group-wrapper {:shape root :view-box vbox}]]))
(mf/defc components-sprite-svg
{::mf/wrap-props false}
[props]
(let [data (obj/get props "data")
children (obj/get props "children")
embed? (obj/get props "embed?")]
[:& (mf/provider embed/context) {:value embed?}
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot "https://penpot.app/xmlns"
:style {:width "100vw"
:height "100vh"
:display (when-not (some? children) "none")}}
[:defs
(for [[component-id component-data] (:components data)]
[:& component-symbol {:id component-id
:key (str component-id)
:data component-data}])]
children]]))

View file

@ -66,5 +66,16 @@
(let [elem (mf/element exports/page-svg #js {:data data :embed? true})] (let [elem (mf/element exports/page-svg #js {:data data :embed? true})]
(rds/renderToStaticMarkup elem))))))) (rds/renderToStaticMarkup elem)))))))
(defn render-components
[data]
(rx/concat
(->> (rx/merge
(populate-images-cache data)
(populate-fonts-cache data))
(rx/ignore))
(->> (rx/of data)
(rx/map
(fn [data]
(let [elem (mf/element exports/components-sprite-svg #js {:data data :embed? true})]
(rds/renderToStaticMarkup elem)))))))

View file

@ -82,6 +82,7 @@
;; Used for export ;; Used for export
["/render-object/:file-id/:page-id/:object-id" :render-object] ["/render-object/:file-id/:page-id/:object-id" :render-object]
["/render-sprite/:file-id" :render-sprite]
["/dashboard/team/:team-id" ["/dashboard/team/:team-id"
["/members" :dashboard-team-members] ["/members" :dashboard-team-members]
@ -172,6 +173,14 @@
:page-id page-id :page-id page-id
:object-id object-id}])) :object-id object-id}]))
:render-sprite
(do
(let [file-id (uuid (get-in route [:path-params :file-id]))
component-id (get-in route [:query-params :component-id])
component-id (when (some? component-id) (uuid component-id))]
[:& render/render-sprite {:file-id file-id
:component-id component-id}]))
:workspace :workspace
(let [project-id (some-> params :path :project-id uuid) (let [project-id (some-> params :path :project-id uuid)
file-id (some-> params :path :file-id uuid) file-id (some-> params :path :file-id uuid)

View file

@ -123,3 +123,30 @@
[:& object-svg {:objects @objects [:& object-svg {:objects @objects
:object-id object-id :object-id object-id
:zoom 1}]))) :zoom 1}])))
(mf/defc render-sprite
[{:keys [file-id component-id] :as props}]
(let [file (mf/use-state nil)]
(mf/use-effect
(mf/deps file-id)
(fn []
(->> (repo/query! :file {:id file-id})
(rx/subs
(fn [result]
(reset! file result))))
(constantly nil)))
(when @file
[:*
[:& exports/components-sprite-svg {:data (:data @file) :embed true}
(when (some? component-id)
[:use {:x 0 :y 0
:xlinkHref (str "#" component-id)}])]
(when-not (some? component-id)
[:ul
(for [[id data] (get-in @file [:data :components])]
(let [url (str "#/render-sprite/" (:id @file) "?component-id=" id)]
[:li [:a {:href url} (:name data)]]))])])))

View file

@ -91,20 +91,22 @@
[(str "penpot:" (d/name k)) v])] [(str "penpot:" (d/name k)) v])]
(into {} (map prefix-entry) m))) (into {} (map prefix-entry) m)))
(mf/defc export-grid-data (mf/defc export-grid-data
[{:keys [grids]}] [{:keys [grids]}]
[:> "penpot:grids" #js {} (when-not (empty? grids)
(for [{:keys [type display params]} grids] [:> "penpot:grids" #js {}
(let [props (->> (d/without-keys params [:color]) (for [{:keys [type display params]} grids]
(prefix-keys) (let [props (->> (d/without-keys params [:color])
(clj->js))] (prefix-keys)
[:> "penpot:grid" (clj->js))]
(-> props [:> "penpot:grid"
(obj/set! "penpot:color" (get-in params [:color :color])) (-> props
(obj/set! "penpot:opacity" (get-in params [:color :opacity])) (obj/set! "penpot:color" (get-in params [:color :color]))
(obj/set! "penpot:type" (d/name type)) (obj/set! "penpot:opacity" (get-in params [:color :opacity]))
(cond-> (some? display) (obj/set! "penpot:type" (d/name type))
(obj/set! "penpot:display" (str display))))]))]) (cond-> (some? display)
(obj/set! "penpot:display" (str display))))]))]))
(mf/defc export-page (mf/defc export-page
[{:keys [options]}] [{:keys [options]}]
@ -117,62 +119,84 @@
[:> "penpot:page" #js {} [:> "penpot:page" #js {}
[:& export-grid-data {:grids grids}]])))) [:& export-grid-data {:grids grids}]]))))
(mf/defc export-shadow-data
[{:keys [shadow]}]
(for [{:keys [style hidden color offset-x offset-y blur spread]} shadow]
[:> "penpot:shadow"
#js {:penpot:shadow-type (d/name style)
:penpot:hidden (str hidden)
:penpot:color (str (:color color))
:penpot:opacity (str (:opacity color))
:penpot:offset-x (str offset-x)
:penpot:offset-y (str offset-y)
:penpot:blur (str blur)
:penpot:spread (str spread)}]))
(mf/defc export-blur-data [{:keys [blur]}]
(when (some? blur)
(let [{:keys [type hidden value]} blur]
[:> "penpot:blur"
#js {:penpot:blur-type (d/name type)
:penpot:hidden (str hidden)
:penpot:value (str value)}])))
(mf/defc export-exports-data [{:keys [exports]}]
(for [{:keys [scale suffix type]} exports]
[:> "penpot:export"
#js {:penpot:type (d/name type)
:penpot:suffix suffix
:penpot:scale (str scale)}]))
(mf/defc export-svg-data [shape]
[:*
(when (contains? shape :svg-attrs)
(let [svg-transform (get shape :svg-transform)
svg-attrs (->> shape :svg-attrs keys (mapv d/name) (str/join ",") )
svg-defs (->> shape :svg-defs keys (mapv d/name) (str/join ","))]
[:> "penpot:svg-import"
#js {:penpot:svg-attrs (when-not (empty? svg-attrs) svg-attrs)
:penpot:svg-defs (when-not (empty? svg-defs) svg-defs)
:penpot:svg-transform (when svg-transform (str svg-transform))
:penpot:svg-viewbox-x (get-in shape [:svg-viewbox :x])
:penpot:svg-viewbox-y (get-in shape [:svg-viewbox :y])
:penpot:svg-viewbox-width (get-in shape [:svg-viewbox :width])
:penpot:svg-viewbox-height (get-in shape [:svg-viewbox :height])}
(for [[def-id def-xml] (:svg-defs shape)]
[:> "penpot:svg-def" #js {:def-id def-id}
[:& render-xml {:xml def-xml}]])]))
(when (= (:type shape) :svg-raw)
(let [props
(-> (obj/new)
(obj/set! "penpot:x" (:x shape))
(obj/set! "penpot:y" (:y shape))
(obj/set! "penpot:width" (:width shape))
(obj/set! "penpot:height" (:height shape))
(obj/set! "penpot:tag" (-> (get-in shape [:content :tag]) d/name))
(obj/merge! (-> (get-in shape [:content :attrs])
(clj->js))))]
[:> "penpot:svg-content" props
(for [leaf (->> shape :content :content (filter string?))]
[:> "penpot:svg-child" {} leaf])]))])
(mf/defc export-interactions-data
[{:keys [interactions]}]
(when-not (empty? interactions)
[:> "penpot:interactions" #js {}
(for [{:keys [action-type destination event-type]} interactions]
[:> "penpot:interaction"
#js {:penpot:action-type (d/name action-type)
:penpot:destination (str destination)
:penpot:event-type (d/name event-type)}])]))
(mf/defc export-data (mf/defc export-data
[{:keys [shape]}] [{:keys [shape]}]
(let [props (-> (obj/new) (let [props (-> (obj/new) (add-data shape))]
(add-data shape))]
[:> "penpot:shape" props [:> "penpot:shape" props
(for [{:keys [style hidden color offset-x offset-y blur spread]} (:shadow shape)] [:& export-shadow-data shape]
[:> "penpot:shadow" #js {:penpot:shadow-type (d/name style) [:& export-blur-data shape]
:penpot:hidden (str hidden) [:& export-exports-data shape]
:penpot:color (str (:color color)) [:& export-svg-data shape]
:penpot:opacity (str (:opacity color)) [:& export-interactions-data shape]
:penpot:offset-x (str offset-x) [:& export-grid-data shape]]))
:penpot:offset-y (str offset-y)
:penpot:blur (str blur)
:penpot:spread (str spread)}])
(when (some? (:blur shape))
(let [{:keys [type hidden value]} (:blur shape)]
[:> "penpot:blur" #js {:penpot:blur-type (d/name type)
:penpot:hidden (str hidden)
:penpot:value (str value)}]))
(for [{:keys [scale suffix type]} (:exports shape)]
[:> "penpot:export" #js {:penpot:type (d/name type)
:penpot:suffix suffix
:penpot:scale (str scale)}])
(when (contains? shape :svg-attrs)
(let [svg-transform (get shape :svg-transform)
svg-attrs (->> shape :svg-attrs keys (mapv d/name) (str/join ",") )
svg-defs (->> shape :svg-defs keys (mapv d/name) (str/join ","))]
[:> "penpot:svg-import" #js {:penpot:svg-attrs (when-not (empty? svg-attrs) svg-attrs)
:penpot:svg-defs (when-not (empty? svg-defs) svg-defs)
:penpot:svg-transform (when svg-transform (str svg-transform))
:penpot:svg-viewbox-x (get-in shape [:svg-viewbox :x])
:penpot:svg-viewbox-y (get-in shape [:svg-viewbox :y])
:penpot:svg-viewbox-width (get-in shape [:svg-viewbox :width])
:penpot:svg-viewbox-height (get-in shape [:svg-viewbox :height])}
(for [[def-id def-xml] (:svg-defs shape)]
[:> "penpot:svg-def" #js {:def-id def-id}
[:& render-xml {:xml def-xml}]])]))
(when (= (:type shape) :svg-raw)
(let [props (-> (obj/new)
(obj/set! "penpot:x" (:x shape))
(obj/set! "penpot:y" (:y shape))
(obj/set! "penpot:width" (:width shape))
(obj/set! "penpot:height" (:height shape))
(obj/set! "penpot:tag" (-> (get-in shape [:content :tag]) d/name))
(obj/merge! (-> (get-in shape [:content :attrs])
(clj->js))))]
[:> "penpot:svg-content" props
(for [leaf (->> shape :content :content (filter string?))]
[:> "penpot:svg-child" {} leaf])]))
(when (and (= (:type shape) :frame)
(seq (:grids shape)))
[:& export-grid-data {:grids (:grids shape)}])]))

View file

@ -286,9 +286,9 @@
shape-center (geom/center-shape shape) shape-center (geom/center-shape shape)
on-resize (fn [current-position initial-position event] on-resize (fn [current-position _initial-position event]
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (dw/start-resize current-position initial-position selected shape))) (st/emit! (dw/start-resize current-position selected shape)))
on-rotate #(do (dom/stop-propagation %) on-rotate #(do (dom/stop-propagation %)
(st/emit! (dw/start-rotate shapes)))] (st/emit! (dw/start-rotate shapes)))]
@ -311,9 +311,9 @@
shape (geom/transform-shape shape {:round-coords? false}) shape (geom/transform-shape shape {:round-coords? false})
shape' (if (debug? :simple-selection) (geom/setup {:type :rect} (geom/selection-rect [shape])) shape) shape' (if (debug? :simple-selection) (geom/setup {:type :rect} (geom/selection-rect [shape])) shape)
on-resize (fn [current-position initial-position event] on-resize (fn [current-position _initial-position event]
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (dw/start-resize current-position initial-position #{shape-id} shape'))) (st/emit! (dw/start-resize current-position #{shape-id} shape')))
on-rotate on-rotate
#(do (dom/stop-propagation %) #(do (dom/stop-propagation %)

View file

@ -15,6 +15,12 @@
[app.util.path.parser :as upp] [app.util.path.parser :as upp]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(def url-regex
#"url\(#([^\)]*)\)")
(def uuid-regex
#"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}")
(defn valid? (defn valid?
[root] [root]
(contains? (:attrs root) :xmlns:penpot)) (contains? (:attrs root) :xmlns:penpot))
@ -41,7 +47,7 @@
(defn find-all-nodes (defn find-all-nodes
[node tag] [node tag]
(when (some? node) (when (some? node)
(->> node :content (filterv #(= (:tag %) :defs))))) (->> node :content (filterv #(= (:tag %) tag)))))
(defn get-data (defn get-data
([node] ([node]
@ -65,6 +71,11 @@
(or (close? node) (or (close? node)
(some? (get-data node)))) (some? (get-data node))))
(defn get-id
[node]
(when-let [id (re-find uuid-regex (get-in node [:attrs :id]))]
(uuid/uuid id)))
(defn str->bool (defn str->bool
[val] [val]
(when (some? val) (= val "true"))) (when (some? val) (= val "true")))
@ -115,7 +126,7 @@
(defn without-penpot-prefix (defn without-penpot-prefix
[m] [m]
(let [no-penpot-prefix? (let [no-penpot-prefix?
(fn [[k v]] (fn [[k _]]
(not (str/starts-with? (d/name k) "penpot:")))] (not (str/starts-with? (d/name k) "penpot:")))]
(into {} (filter no-penpot-prefix?) m))) (into {} (filter no-penpot-prefix?) m)))
@ -180,11 +191,11 @@
[props svg-data] [props svg-data]
(let [values (->> (select-keys svg-data [:cx :cy :rx :ry]) (let [values (->> (select-keys svg-data [:cx :cy :rx :ry])
(d/mapm (fn [_ val] (d/parse-double val))))] (d/mapm (fn [_ val] (d/parse-double val))))]
(-> props
{:x (- (:cx values) (:rx values)) (assoc :x (- (:cx values) (:rx values))
:y (- (:cy values) (:ry values)) :y (- (:cy values) (:ry values))
:width (* (:rx values) 2) :width (* (:rx values) 2)
:height (* (:ry values) 2)})) :height (* (:ry values) 2)))))
(defn parse-path (defn parse-path
[props center svg-data] [props center svg-data]
@ -193,9 +204,6 @@
(assoc :content content) (assoc :content content)
(assoc :center center)))) (assoc :center center))))
(def url-regex #"url\(#([^\)]*)\)")
(defn parse-stops (defn parse-stops
[gradient-node] [gradient-node]
(->> gradient-node (->> gradient-node
@ -451,7 +459,7 @@
[props node] [props node]
(let [shadows (extract-from-data node :penpot:shadow parse-shadow)] (let [shadows (extract-from-data node :penpot:shadow parse-shadow)]
(cond-> props (cond-> props
(not (empty? shadows)) (d/not-empty? shadows)
(assoc :shadow shadows)))) (assoc :shadow shadows))))
(defn add-blur (defn add-blur
@ -465,7 +473,7 @@
[props node] [props node]
(let [exports (extract-from-data node :penpot:export parse-export)] (let [exports (extract-from-data node :penpot:export parse-export)]
(cond-> props (cond-> props
(not (empty? exports)) (d/not-empty? exports)
(assoc :exports exports)))) (assoc :exports exports))))
(defn add-layer-options (defn add-layer-options
@ -483,7 +491,7 @@
(defn remove-prefix [s] (defn remove-prefix [s]
(cond-> s (cond-> s
(string? s) (string? s)
(str/replace #"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}-" ""))) (str/replace (re-pattern (str uuid-regex "-")) "")))
(defn get-svg-attrs (defn get-svg-attrs
[svg-data svg-attrs] [svg-data svg-attrs]
@ -499,7 +507,7 @@
(reduce assoc-key {})))) (reduce assoc-key {}))))
(defn get-svg-defs (defn get-svg-defs
[node svg-defs] [node]
(let [svg-import (get-data node :penpot:svg-import)] (let [svg-import (get-data node :penpot:svg-import)]
(->> svg-import (->> svg-import
@ -537,7 +545,7 @@
(some? svg-defs) (some? svg-defs)
(assoc :svg-defs (get-svg-defs node svg-defs)))) (assoc :svg-defs (get-svg-defs node))))
props))) props)))
@ -563,7 +571,7 @@
(defn add-frame-data [props node] (defn add-frame-data [props node]
(let [grids (parse-grids node)] (let [grids (parse-grids node)]
(cond-> props (cond-> props
(not (empty? grids)) (d/not-empty? grids)
(assoc :grids grids)))) (assoc :grids grids))))
(defn has-image? (defn has-image?
@ -633,10 +641,19 @@
background (:background style) background (:background style)
grids (->> (parse-grids node) grids (->> (parse-grids node)
(group-by :type) (group-by :type)
(d/mapm (fn [k v] (-> v first :params))))] (d/mapm (fn [_ v] (-> v first :params))))]
(cond-> {} (cond-> {}
(some? background) (some? background)
(assoc-in [:options :background] background) (assoc-in [:options :background] background)
(not (empty? grids)) (d/not-empty? grids)
(assoc-in [:options :saved-grids] grids)))) (assoc-in [:options :saved-grids] grids))))
(defn parse-interactions
[node]
(let [interactions-node (get-data node :penpot:interactions)]
(->> (find-all-nodes interactions-node :penpot:interaction)
(mapv (fn [node]
{:destination (get-meta node :destination uuid/uuid)
:action-type (get-meta node :action-type keyword)
:event-type (get-meta node :event-type keyword)})))))

View file

@ -6,36 +6,57 @@
(ns app.worker.export (ns app.worker.export
(:require (:require
[app.common.data :as d]
[app.config :as cfg]
[app.main.render :as r] [app.main.render :as r]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.zip :as uz] [app.util.http :as http]
[app.util.json :as json] [app.util.json :as json]
[app.util.zip :as uz]
[app.worker.impl :as impl] [app.worker.impl :as impl]
[beicon.core :as rx])) [beicon.core :as rx]
[cuerdas.core :as str]))
(defn create-manifest (defn create-manifest
"Creates a manifest entry for the given files" "Creates a manifest entry for the given files"
[team-id files] [team-id file-id files]
(letfn [(format-page [manifest page] (letfn [(format-page [manifest page]
(-> manifest (-> manifest
(assoc (str (:id page)) (assoc (str (:id page))
{:name (:name page)}))) {:name (:name page)})))
(format-file [manifest file] (format-file [manifest file]
(let [name (:name file) (let [name (:name file)
pages (->> (get-in file [:data :pages]) (mapv str)) is-shared (:is-shared file)
index (->> (get-in file [:data :pages-index]) (vals) pages (->> (get-in file [:data :pages])
(reduce format-page {}))] (mapv str))
index (->> (get-in file [:data :pages-index])
(vals)
(reduce format-page {}))]
(-> manifest (-> manifest
(assoc (str (:id file)) (assoc (str (:id file))
{:name name {:name name
:pages pages :shared is-shared
:pagesIndex index}))))] :pages pages
:pagesIndex index
:hasComponents (d/not-empty? (get-in file [:data :components]))
:hasImages (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) (let [manifest {:teamId (str team-id)
:fileId (str file-id)
:files (->> (vals files) (reduce format-file {}))}] :files (->> (vals files) (reduce format-file {}))}]
(json/encode manifest)))) (json/encode manifest))))
(defn process-pages [file]
(let [pages (get-in file [:data :pages])
pages-index (get-in file [:data :pages-index])]
(->> pages
(map #(hash-map
:file-id (:id file)
:data (get pages-index %))))))
(defn get-page-data (defn get-page-data
[{file-id :file-id {:keys [id name] :as data} :data}] [{file-id :file-id {:keys [id name] :as data} :data}]
(->> (r/render-page data) (->> (r/render-page data)
@ -45,30 +66,96 @@
:file-id file-id :file-id file-id
:markup markup})))) :markup markup}))))
(defn process-pages [file]
(let [pages (get-in file [:data :pages])
pages-index (get-in file [:data :pages-index])]
(->> pages
(map #(hash-map
:file-id (:id file)
:data (get pages-index %))))))
(defn collect-page (defn collect-page
[{:keys [id file-id markup] :as page}] [{:keys [id file-id markup] :as page}]
[(str file-id "/" id ".svg") markup]) [(str file-id "/" id ".svg") markup])
(defn collect-entries [result data keys]
(-> result
(assoc (str (:id data))
(->> (select-keys data keys)
(d/deep-mapm
(fn [[k v]]
[(-> k str/camel) v]))))))
(def ^:const color-keys
[:name :color :opacity :gradient])
(def ^:const typography-keys
[:name :font-family :font-id :font-size :font-style :font-variant-id :font-weight
:letter-spacing :line-height :text-transform])
(def ^:const media-keys
[:name :mtype :width :height])
(defn collect-color
[result color]
(collect-entries result color color-keys))
(defn collect-typography
[result typography]
(collect-entries result typography typography-keys))
(defn collect-media
[result media]
(collect-entries result media media-keys))
(defn parse-library-color
[[file-id colors]]
(let [markup
(->> (vals colors)
(reduce collect-color {})
(json/encode))]
[(str file-id "/colors.json") markup]))
(defn parse-library-typographies
[[file-id typographies]]
(let [markup
(->> (vals typographies)
(reduce collect-typography {})
(json/encode))]
[(str file-id "/typographies.json") markup]))
(defn parse-library-media
[[file-id media]]
(rx/merge
(let [markup
(->> (vals media)
(reduce collect-media {})
(json/encode))]
(rx/of (vector (str file-id "/images.json") markup)))
(->> (rx/from (vals media))
(rx/map #(assoc % :file-id file-id))
(rx/flat-map
(fn [media]
(let [file-path (str file-id "/images/" (:id media) "." (dom/mtype->extension (:mtype media)))]
(->> (http/send!
{:uri (cfg/resolve-file-media media)
:response-type :blob
:method :get})
(rx/map :body)
(rx/map #(vector file-path %)))))))))
(defn parse-library-components
[file]
(->> (r/render-components (:data file))
(rx/map #(vector (str (:id file) "/components.svg") %))))
(defn export-file (defn export-file
[team-id file-id] [team-id file-id]
(let [files-stream (let [files-stream
(->> (rp/query :file {:id file-id}) (->> (rx/merge (rp/query :file {:id file-id})
(->> (rp/query :file-libraries {:file-id file-id})
(rx/flat-map identity)
(rx/map #(assoc % :is-shared true))))
(rx/reduce #(assoc %1 (:id %2) %2) {}) (rx/reduce #(assoc %1 (:id %2) %2) {})
(rx/share)) (rx/share))
manifest-stream manifest-stream
(->> files-stream (->> files-stream
(rx/map #(create-manifest team-id %)) (rx/map #(create-manifest team-id file-id %))
(rx/map #(vector "manifest.json" %))) (rx/map #(vector "manifest.json" %)))
render-stream render-stream
@ -79,6 +166,33 @@
(rx/flat-map get-page-data) (rx/flat-map get-page-data)
(rx/share)) (rx/share))
colors-stream
(->> files-stream
(rx/flat-map vals)
(rx/map #(vector (:id %) (get-in % [:data :colors])))
(rx/filter #(d/not-empty? (second %)))
(rx/map parse-library-color))
typographies-stream
(->> files-stream
(rx/flat-map vals)
(rx/map #(vector (:id %) (get-in % [:data :typographies])))
(rx/filter #(d/not-empty? (second %)))
(rx/map parse-library-typographies))
media-stream
(->> files-stream
(rx/flat-map vals)
(rx/map #(vector (:id %) (get-in % [:data :media])))
(rx/filter #(d/not-empty? (second %)))
(rx/flat-map parse-library-media))
components-stream
(->> files-stream
(rx/flat-map vals)
(rx/filter #(d/not-empty? (get-in % [:data :components])))
(rx/flat-map parse-library-components))
pages-stream pages-stream
(->> render-stream (->> render-stream
(rx/map collect-page))] (rx/map collect-page))]
@ -90,7 +204,13 @@
:file file-id :file file-id
:data (str "Render " (:file-name %) " - " (:name %))))) :data (str "Render " (:file-name %) " - " (:name %)))))
(->> (rx/merge manifest-stream pages-stream) (->> (rx/merge
manifest-stream
pages-stream
components-stream
media-stream
colors-stream
typographies-stream)
(rx/reduce conj []) (rx/reduce conj [])
(rx/with-latest-from files-stream) (rx/with-latest-from files-stream)
(rx/flat-map (fn [[data files]] (rx/flat-map (fn [[data files]]
@ -98,7 +218,7 @@
(rx/map #(vector (get files file-id) %))))))))) (rx/map #(vector (get files file-id) %)))))))))
(defmethod impl/handler :export-file (defmethod impl/handler :export-file
[{:keys [team-id project-id files] :as message}] [{:keys [team-id files] :as message}]
(->> (rx/from files) (->> (rx/from files)
(rx/mapcat #(export-file team-id %)) (rx/mapcat #(export-file team-id %))

View file

@ -17,7 +17,6 @@
[app.util.zip :as uz] [app.util.zip :as uz]
[app.worker.impl :as impl] [app.worker.impl :as impl]
[beicon.core :as rx] [beicon.core :as rx]
[cuerdas.core :as str]
[tubax.core :as tubax])) [tubax.core :as tubax]))
;; Upload changes batches size ;; Upload changes batches size
@ -25,12 +24,13 @@
(defn create-file (defn create-file
"Create a new file on the back-end" "Create a new file on the back-end"
[project-id name] [project-id file-desc]
(let [file-id (uuid/next)] (let [file-id (uuid/next)]
(rp/mutation (rp/mutation
:create-temp-file :create-temp-file
{:id file-id {:id file-id
:name name :name (:name file-desc)
:is-shared (:shared file-desc)
:project-id project-id :project-id project-id
:data (-> cp/empty-file-data (assoc :id file-id))}))) :data (-> cp/empty-file-data (assoc :id file-id))})))
@ -78,36 +78,65 @@
(defn add-shape-file (defn add-shape-file
[file node] [file node]
(let [type (cip/get-type node) (let [type (cip/get-type node)
close? (cip/close? node) close? (cip/close? node)]
data (cip/parse-data type node)]
(if close? (if close?
(case type (case type
:frame :frame (fb/close-artboard file)
(fb/close-artboard file) :group (fb/close-group file)
:svg-raw (fb/close-svg-raw file)
#_default file)
:group (let [data (cip/parse-data type node)
(fb/close-group file) old-id (cip/get-id node)
interactions (cip/parse-interactions node)
:svg-raw file (case type
(fb/close-svg-raw file) :frame (fb/add-artboard file data)
:group (fb/add-group file data)
:rect (fb/create-rect file data)
:circle (fb/create-circle file data)
:path (fb/create-path file data)
:text (fb/create-text file data)
:image (fb/create-image file data)
:svg-raw (fb/create-svg-raw file data)
#_default file)]
;; default (assert (some? old-id) "ID not found")
file)
(case type ;; We store this data for post-processing after every shape has been
:frame (fb/add-artboard file data) ;; added
:group (fb/add-group file data) (cond-> file
:rect (fb/create-rect file data) (some? (:last-id file))
:circle (fb/create-circle file data) (assoc-in [:id-mapping old-id] (:last-id file))
:path (fb/create-path file data)
:text (fb/create-text file data)
:image (fb/create-image file data)
:svg-raw (fb/create-svg-raw file data)
;; default (d/not-empty? interactions)
file)))) (assoc-in [:interactions old-id] interactions))))))
(defn post-process-file
[file]
(letfn [(add-interaction
[id file {:keys [action-type event-type destination] :as interaction}]
(fb/add-interaction file action-type event-type id destination))
(add-interactions
[file [old-id interactions]]
(let [id (get-in file [:id-mapping old-id])]
(->> interactions
(mapv (fn [interaction]
(let [id (get-in file [:id-mapping (:destination interaction)])]
(assoc interaction :destination id))))
(reduce
(partial add-interaction id) file))))
(process-interactions
[file]
(reduce add-interactions file (:interactions file)))]
(-> file
(process-interactions)
(dissoc :id-mapping :interactions))))
(defn merge-reduce [f seed ob] (defn merge-reduce [f seed ob]
(->> (rx/concat (->> (rx/concat
@ -145,6 +174,7 @@
(rx/filter cip/shape?) (rx/filter cip/shape?)
(rx/mapcat (partial resolve-images file-id)) (rx/mapcat (partial resolve-images file-id))
(rx/reduce add-shape-file (fb/add-page file page-data)) (rx/reduce add-shape-file (fb/add-page file page-data))
(rx/map post-process-file)
(rx/map fb/close-page))) (rx/map fb/close-page)))
(rx/empty))) (rx/empty)))
@ -173,7 +203,7 @@
(rx/flat-map (comp :files json/decode :content)) (rx/flat-map (comp :files json/decode :content))
(rx/flat-map (rx/flat-map
(fn [[file-id file-desc]] (fn [[file-id file-desc]]
(->> (create-file project-id (:name file-desc)) (->> (create-file project-id file-desc)
(rx/flat-map #(process-file % file-id file-desc zip-file))))))) (rx/flat-map #(process-file % file-id file-desc zip-file)))))))
(defmethod impl/handler :import-file (defmethod impl/handler :import-file