mirror of
https://github.com/penpot/penpot.git
synced 2025-03-15 17:21:17 -05:00
✨ Enhance synchronization of a component with subcomponents
This commit is contained in:
parent
7c75b75f5b
commit
68ca44188c
8 changed files with 346 additions and 239 deletions
|
@ -236,12 +236,8 @@
|
||||||
:width :size-group
|
:width :size-group
|
||||||
:height :size-group
|
:height :size-group
|
||||||
:proportion :size-group
|
:proportion :size-group
|
||||||
:x :position-group
|
|
||||||
:y :position-group
|
|
||||||
:rx :radius-group
|
:rx :radius-group
|
||||||
:ry :radius-group
|
:ry :radius-group})
|
||||||
:points :points-group
|
|
||||||
:transform :transform-group})
|
|
||||||
|
|
||||||
(def color-sync-attrs [:fill-color
|
(def color-sync-attrs [:fill-color
|
||||||
:stroke-color])
|
:stroke-color])
|
||||||
|
@ -899,14 +895,14 @@
|
||||||
group (get component-sync-attrs attr)]
|
group (get component-sync-attrs attr)]
|
||||||
|
|
||||||
(cond-> shape
|
(cond-> shape
|
||||||
|
(and shape-ref group (not ignore) (not= val (get shape attr)))
|
||||||
|
(update :touched #(conj (or % #{}) group))
|
||||||
|
|
||||||
(nil? val)
|
(nil? val)
|
||||||
(dissoc attr)
|
(dissoc attr)
|
||||||
|
|
||||||
(some? val)
|
(some? val)
|
||||||
(assoc attr val)
|
(assoc attr val))))
|
||||||
|
|
||||||
(and shape-ref group (not ignore))
|
|
||||||
(update :touched #(conj (or % #{}) group)))))
|
|
||||||
|
|
||||||
(defmethod process-operation :set-touched
|
(defmethod process-operation :set-touched
|
||||||
[shape op]
|
[shape op]
|
||||||
|
|
|
@ -31,15 +31,15 @@
|
||||||
(update page :objects
|
(update page :objects
|
||||||
#(into % (d/index-by :id objects-list))))
|
#(into % (d/index-by :id objects-list))))
|
||||||
|
|
||||||
(defn get-root-component
|
(defn get-root-shape
|
||||||
"Get the root shape linked to the component for this shape, if any"
|
"Get the root shape linked to a component for this shape, if any"
|
||||||
[id objects]
|
[shape objects]
|
||||||
(let [obj (get objects id)]
|
(if (:component-root? shape)
|
||||||
(if-let [component-id (:component-root? obj)]
|
shape
|
||||||
id
|
(if-let [parent-id (:parent-id shape)]
|
||||||
(if-let [parent-id (:parent-id obj)]
|
(get-root-shape (get objects (:parent-id shape))
|
||||||
(get-root-component parent-id objects)
|
objects)
|
||||||
nil))))
|
nil)))
|
||||||
|
|
||||||
(defn get-children
|
(defn get-children
|
||||||
"Retrieve all children ids recursively for a given object"
|
"Retrieve all children ids recursively for a given object"
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
(defn get-object-with-children
|
(defn get-object-with-children
|
||||||
"Retrieve a list with an object and all of its children"
|
"Retrieve a list with an object and all of its children"
|
||||||
[id objects]
|
[id objects]
|
||||||
(map #(get objects %) (concat [id] (get-children id objects))))
|
(map #(get objects %) (d/concat [id] (get-children id objects))))
|
||||||
|
|
||||||
(defn is-shape-grouped
|
(defn is-shape-grouped
|
||||||
"Checks if a shape is inside a group"
|
"Checks if a shape is inside a group"
|
||||||
|
|
|
@ -1148,7 +1148,6 @@
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [page-id (:current-page-id state)
|
(let [page-id (:current-page-id state)
|
||||||
objects (dwc/lookup-page-objects state page-id)
|
objects (dwc/lookup-page-objects state page-id)
|
||||||
root-id (cph/get-root-component (:id shape) objects)
|
|
||||||
|
|
||||||
mdata {:position position
|
mdata {:position position
|
||||||
:shape shape
|
:shape shape
|
||||||
|
|
|
@ -160,7 +160,10 @@
|
||||||
:val (:component-root? updated-shape)}
|
:val (:component-root? updated-shape)}
|
||||||
{:type :set
|
{:type :set
|
||||||
:attr :shape-ref
|
:attr :shape-ref
|
||||||
:val (:shape-ref updated-shape)}]})
|
:val (:shape-ref updated-shape)}
|
||||||
|
{:type :set
|
||||||
|
:attr :touched
|
||||||
|
:val (:touched updated-shape)}]})
|
||||||
updated-shapes))
|
updated-shapes))
|
||||||
|
|
||||||
uchanges (conj uchanges
|
uchanges (conj uchanges
|
||||||
|
@ -184,9 +187,13 @@
|
||||||
:val (:component-root? original-shape)}
|
:val (:component-root? original-shape)}
|
||||||
{:type :set
|
{:type :set
|
||||||
:attr :shape-ref
|
:attr :shape-ref
|
||||||
:val (:shape-ref original-shape)}]}))
|
:val (:shape-ref original-shape)}
|
||||||
|
{:type :set
|
||||||
|
:attr :touched
|
||||||
|
:val (:touched original-shape)}]}))
|
||||||
updated-shapes))]
|
updated-shapes))]
|
||||||
|
|
||||||
|
|
||||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
|
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
|
||||||
(dws/select-shapes (d/ordered-set (:id group))))))))))
|
(dws/select-shapes (d/ordered-set (:id group))))))))))
|
||||||
|
|
||||||
|
@ -245,7 +252,8 @@
|
||||||
(dwc/calculate-frame-overlap all-frames $))
|
(dwc/calculate-frame-overlap all-frames $))
|
||||||
(assoc $ :parent-id
|
(assoc $ :parent-id
|
||||||
(or (:parent-id $) (:frame-id $)))
|
(or (:parent-id $) (:frame-id $)))
|
||||||
(assoc $ :shape-ref (:id original-shape)))
|
(assoc $ :shape-ref (:id original-shape))
|
||||||
|
(dissoc $ :touched))
|
||||||
|
|
||||||
(nil? (:parent-id original-shape))
|
(nil? (:parent-id original-shape))
|
||||||
(assoc :component-id (:id original-shape)
|
(assoc :component-id (:id original-shape)
|
||||||
|
@ -356,25 +364,35 @@
|
||||||
(ptk/reify ::reset-component
|
(ptk/reify ::reset-component
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
|
(js/console.info "##### RESET-COMPONENT of shape" (str id))
|
||||||
(let [page-id (:current-page-id state)
|
(let [page-id (:current-page-id state)
|
||||||
page (get-in state [:workspace-data :pages-index page-id])
|
page (get-in state [:workspace-data :pages-index page-id])
|
||||||
objects (dwc/lookup-page-objects state page-id)
|
objects (dwc/lookup-page-objects state page-id)
|
||||||
root-shape (get objects id)
|
shape (get objects id)
|
||||||
file-id (get root-shape :component-file)
|
file-id (get shape :component-file)
|
||||||
|
|
||||||
components
|
[all-shapes component root-component]
|
||||||
(if (nil? file-id)
|
(dwlh/resolve-shapes-and-components shape
|
||||||
(get-in state [:workspace-data :components])
|
objects
|
||||||
(get-in state [:workspace-libraries file-id :data :components]))
|
state
|
||||||
|
true)
|
||||||
|
|
||||||
|
_ (js/console.info "shape" (:name shape) "<- component" (:name component))
|
||||||
|
_ (js/console.debug "all-shapes" (clj->js all-shapes))
|
||||||
|
_ (js/console.debug "component" (clj->js component))
|
||||||
|
_ (js/console.debug "root-component" (clj->js root-component))
|
||||||
|
|
||||||
[rchanges uchanges]
|
[rchanges uchanges]
|
||||||
(dwlh/generate-sync-shape-and-children-components root-shape
|
(dwlh/generate-sync-shape-and-children-components shape
|
||||||
objects
|
all-shapes
|
||||||
components
|
component
|
||||||
|
root-component
|
||||||
(:id page)
|
(:id page)
|
||||||
nil
|
nil
|
||||||
true)]
|
true)]
|
||||||
|
|
||||||
|
(js/console.debug "rchanges" (clj->js rchanges))
|
||||||
|
|
||||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
|
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
|
||||||
|
|
||||||
(defn update-component
|
(defn update-component
|
||||||
|
@ -383,22 +401,32 @@
|
||||||
(ptk/reify ::update-component
|
(ptk/reify ::update-component
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
|
(js/console.info "##### UPDATE-COMPONENT of shape" (str id))
|
||||||
(let [page-id (:current-page-id state)
|
(let [page-id (:current-page-id state)
|
||||||
objects (dwc/lookup-page-objects state page-id)
|
objects (dwc/lookup-page-objects state page-id)
|
||||||
root-shape (get objects id)
|
shape (get objects id)
|
||||||
file-id (get root-shape :component-file)
|
file-id (get shape :component-file)
|
||||||
|
|
||||||
components
|
[all-shapes component root-component]
|
||||||
(if (nil? file-id)
|
(dwlh/resolve-shapes-and-components shape
|
||||||
(get-in state [:workspace-data :components])
|
objects
|
||||||
(get-in state [:workspace-libraries file-id :data :components]))
|
state
|
||||||
|
true)
|
||||||
|
|
||||||
|
_ (js/console.info "shape" (:name shape) "-> component" (:name component))
|
||||||
|
_ (js/console.debug "all-shapes" (clj->js all-shapes))
|
||||||
|
_ (js/console.debug "component" (clj->js component))
|
||||||
|
_ (js/console.debug "root-component" (clj->js root-component))
|
||||||
|
|
||||||
[rchanges uchanges]
|
[rchanges uchanges]
|
||||||
(dwlh/generate-sync-shape-inverse root-shape
|
(dwlh/generate-sync-shape-inverse shape
|
||||||
objects
|
all-shapes
|
||||||
components
|
component
|
||||||
|
root-component
|
||||||
page-id)]
|
page-id)]
|
||||||
|
|
||||||
|
(js/console.debug "rchanges" (clj->js rchanges))
|
||||||
|
|
||||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
|
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
|
||||||
|
|
||||||
(declare sync-file-2nd-stage)
|
(declare sync-file-2nd-stage)
|
||||||
|
@ -415,6 +443,7 @@
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
|
(js/console.info "##### SYNC-FILE" (str (or file-id "local")))
|
||||||
(let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components file-id state)
|
(let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components file-id state)
|
||||||
[rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state)
|
[rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state)
|
||||||
[rchanges3 uchanges3] (dwlh/generate-sync-file :colors file-id state)
|
[rchanges3 uchanges3] (dwlh/generate-sync-file :colors file-id state)
|
||||||
|
@ -423,6 +452,7 @@
|
||||||
[rchanges6 uchanges6] (dwlh/generate-sync-library :typographies file-id state)
|
[rchanges6 uchanges6] (dwlh/generate-sync-library :typographies file-id state)
|
||||||
rchanges (d/concat rchanges1 rchanges2 rchanges3 rchanges4 rchanges5 rchanges6)
|
rchanges (d/concat rchanges1 rchanges2 rchanges3 rchanges4 rchanges5 rchanges6)
|
||||||
uchanges (d/concat uchanges1 uchanges2 uchanges3 uchanges4 uchanges5 uchanges6)]
|
uchanges (d/concat uchanges1 uchanges2 uchanges3 uchanges4 uchanges5 uchanges6)]
|
||||||
|
(js/console.debug "rchanges" (clj->js rchanges))
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(rx/of (dm/hide-tag :sync-dialog))
|
(rx/of (dm/hide-tag :sync-dialog))
|
||||||
(when rchanges
|
(when rchanges
|
||||||
|
@ -448,11 +478,13 @@
|
||||||
(ptk/reify ::sync-file-2nd-stage
|
(ptk/reify ::sync-file-2nd-stage
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
|
(js/console.info "##### SYNC-FILE" (str (or file-id "local")) "(2nd stage)")
|
||||||
(let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components nil state)
|
(let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components nil state)
|
||||||
[rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state)
|
[rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state)
|
||||||
rchanges (d/concat rchanges1 rchanges2)
|
rchanges (d/concat rchanges1 rchanges2)
|
||||||
uchanges (d/concat uchanges1 uchanges2)]
|
uchanges (d/concat uchanges1 uchanges2)]
|
||||||
(when rchanges
|
(when rchanges
|
||||||
|
(js/console.debug "rchanges" (clj->js rchanges))
|
||||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))))
|
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))))
|
||||||
|
|
||||||
(def ignore-sync
|
(def ignore-sync
|
||||||
|
|
|
@ -21,8 +21,10 @@
|
||||||
|
|
||||||
(declare generate-sync-container)
|
(declare generate-sync-container)
|
||||||
(declare generate-sync-shape)
|
(declare generate-sync-shape)
|
||||||
|
(declare has-asset-reference-fn)
|
||||||
|
|
||||||
(declare generate-sync-component-components)
|
(declare get-assets)
|
||||||
|
(declare resolve-shapes-and-components)
|
||||||
(declare generate-sync-shape-and-children-components)
|
(declare generate-sync-shape-and-children-components)
|
||||||
(declare generate-sync-shape-inverse)
|
(declare generate-sync-shape-inverse)
|
||||||
(declare generate-sync-shape<-component)
|
(declare generate-sync-shape<-component)
|
||||||
|
@ -50,7 +52,10 @@
|
||||||
(assoc :frame-id nil)
|
(assoc :frame-id nil)
|
||||||
|
|
||||||
(nil? (:parent-id new-shape))
|
(nil? (:parent-id new-shape))
|
||||||
(assoc :component-root? true)))
|
(dissoc :component-id
|
||||||
|
:component-file
|
||||||
|
:component-root?
|
||||||
|
:shape-ref)))
|
||||||
|
|
||||||
;; Make the original shape an instance of the new component.
|
;; Make the original shape an instance of the new component.
|
||||||
;; If one of the original shape children already was a component
|
;; If one of the original shape children already was a component
|
||||||
|
@ -58,7 +63,8 @@
|
||||||
update-original-shape (fn [original-shape new-shape]
|
update-original-shape (fn [original-shape new-shape]
|
||||||
(cond-> original-shape
|
(cond-> original-shape
|
||||||
true
|
true
|
||||||
(assoc :shape-ref (:id new-shape))
|
(-> (assoc :shape-ref (:id new-shape))
|
||||||
|
(dissoc :touched))
|
||||||
|
|
||||||
(nil? (:parent-id new-shape))
|
(nil? (:parent-id new-shape))
|
||||||
(assoc :component-id (:id new-shape)
|
(assoc :component-id (:id new-shape)
|
||||||
|
@ -96,7 +102,7 @@
|
||||||
(let [[page-rchanges page-uchanges]
|
(let [[page-rchanges page-uchanges]
|
||||||
(generate-sync-container asset-type
|
(generate-sync-container asset-type
|
||||||
library-id
|
library-id
|
||||||
library-items
|
state
|
||||||
page
|
page
|
||||||
(:id page)
|
(:id page)
|
||||||
nil)]
|
nil)]
|
||||||
|
@ -123,7 +129,7 @@
|
||||||
(let [[comp-rchanges comp-uchanges]
|
(let [[comp-rchanges comp-uchanges]
|
||||||
(generate-sync-container asset-type
|
(generate-sync-container asset-type
|
||||||
library-id
|
library-id
|
||||||
library-items
|
state
|
||||||
local-component
|
local-component
|
||||||
nil
|
nil
|
||||||
(:id local-component))]
|
(:id local-component))]
|
||||||
|
@ -132,7 +138,30 @@
|
||||||
(d/concat uchanges comp-uchanges)))
|
(d/concat uchanges comp-uchanges)))
|
||||||
[rchanges uchanges])))))
|
[rchanges uchanges])))))
|
||||||
|
|
||||||
(defn has-asset-reference-fn
|
(defn- generate-sync-container
|
||||||
|
"Generate changes to synchronize all shapes in a particular container
|
||||||
|
(a page or a component) that are linked to the given library."
|
||||||
|
[asset-type library-id state container page-id component-id]
|
||||||
|
(let [has-asset-reference? (has-asset-reference-fn asset-type library-id)
|
||||||
|
linked-shapes (cph/select-objects has-asset-reference? container)]
|
||||||
|
(loop [shapes (seq linked-shapes)
|
||||||
|
rchanges []
|
||||||
|
uchanges []]
|
||||||
|
(if-let [shape (first shapes)]
|
||||||
|
(let [[shape-rchanges shape-uchanges]
|
||||||
|
(generate-sync-shape asset-type
|
||||||
|
library-id
|
||||||
|
state
|
||||||
|
(get container :objects)
|
||||||
|
page-id
|
||||||
|
component-id
|
||||||
|
shape)]
|
||||||
|
(recur (next shapes)
|
||||||
|
(d/concat rchanges shape-rchanges)
|
||||||
|
(d/concat uchanges shape-uchanges)))
|
||||||
|
[rchanges uchanges]))))
|
||||||
|
|
||||||
|
(defn- has-asset-reference-fn
|
||||||
"Gets a function that checks if a shape uses some asset of the given type
|
"Gets a function that checks if a shape uses some asset of the given type
|
||||||
in the given library."
|
in the given library."
|
||||||
[asset-type library-id]
|
[asset-type library-id]
|
||||||
|
@ -167,50 +196,28 @@
|
||||||
#(and (some? (:typography-ref-id %))
|
#(and (some? (:typography-ref-id %))
|
||||||
(= library-id (:typography-ref-file %)))))))))
|
(= library-id (:typography-ref-file %)))))))))
|
||||||
|
|
||||||
(defn generate-sync-container
|
(defmulti generate-sync-shape
|
||||||
"Generate changes to synchronize all shapes in a particular container
|
"Generate changes to synchronize one shape, that use the given type
|
||||||
(a page or a component)."
|
of asset of the given library."
|
||||||
[asset-type library-id library-items container page-id component-id]
|
(fn [type _ _ _ _ _ _ _] type))
|
||||||
(let [has-asset-reference? (has-asset-reference-fn asset-type library-id)
|
|
||||||
linked-shapes (cph/select-objects has-asset-reference? container)]
|
|
||||||
(loop [shapes (seq linked-shapes)
|
|
||||||
rchanges []
|
|
||||||
uchanges []]
|
|
||||||
(if-let [shape (first shapes)]
|
|
||||||
(let [[shape-rchanges shape-uchanges]
|
|
||||||
(generate-sync-shape asset-type
|
|
||||||
library-id
|
|
||||||
library-items
|
|
||||||
(get container :objects)
|
|
||||||
page-id
|
|
||||||
component-id
|
|
||||||
shape)]
|
|
||||||
(recur (next shapes)
|
|
||||||
(d/concat rchanges shape-rchanges)
|
|
||||||
(d/concat uchanges shape-uchanges)))
|
|
||||||
[rchanges uchanges]))))
|
|
||||||
|
|
||||||
(defmulti generate-sync-shape (fn [type _ _ _ _ _ _ _] type))
|
|
||||||
|
|
||||||
(defmethod generate-sync-shape :components
|
(defmethod generate-sync-shape :components
|
||||||
[_ library-id library-items objects page-id component-id shape]
|
[_ library-id state objects page-id component-id shape]
|
||||||
|
(let [[all-shapes component root-component]
|
||||||
;; Synchronize a shape that is the root instance of a component, and all of its
|
(resolve-shapes-and-components shape
|
||||||
;; children. All attributes of the component shape that have changed, and whose
|
|
||||||
;; group have not been touched in the linked shape, will be copied to the shape.
|
|
||||||
;; Any shape that is linked to a no-longer existent component shape will be
|
|
||||||
;; detached.
|
|
||||||
(let [root-shape shape
|
|
||||||
components library-items
|
|
||||||
reset-touched? false]
|
|
||||||
(generate-sync-shape-and-children-components root-shape
|
|
||||||
objects
|
objects
|
||||||
components
|
state
|
||||||
|
false)]
|
||||||
|
|
||||||
|
(generate-sync-shape-and-children-components shape
|
||||||
|
all-shapes
|
||||||
|
component
|
||||||
|
root-component
|
||||||
page-id
|
page-id
|
||||||
component-id
|
component-id
|
||||||
reset-touched?)))
|
false)))
|
||||||
|
|
||||||
(defn generate-sync-text-shape [shape page-id component-id update-node]
|
(defn- generate-sync-text-shape [shape page-id component-id update-node]
|
||||||
(let [old-content (:content shape)
|
(let [old-content (:content shape)
|
||||||
new-content (ut/map-node update-node old-content)
|
new-content (ut/map-node update-node old-content)
|
||||||
rchanges [(d/without-nils {:type :mod-obj
|
rchanges [(d/without-nils {:type :mod-obj
|
||||||
|
@ -232,13 +239,14 @@
|
||||||
[rchanges lchanges])))
|
[rchanges lchanges])))
|
||||||
|
|
||||||
(defmethod generate-sync-shape :colors
|
(defmethod generate-sync-shape :colors
|
||||||
[_ library-id library-items _ page-id component-id shape]
|
[_ library-id state _ page-id component-id shape]
|
||||||
|
|
||||||
;; Synchronize a shape that uses some colors of the library. The value of the
|
;; Synchronize a shape that uses some colors of the library. The value of the
|
||||||
;; color in the library is copied to the shape.
|
;; color in the library is copied to the shape.
|
||||||
|
(let [colors (get-assets library-id :colors state)]
|
||||||
(if (= :text (:type shape))
|
(if (= :text (:type shape))
|
||||||
(let [update-node (fn [node]
|
(let [update-node (fn [node]
|
||||||
(if-let [color (get library-items (:fill-color-ref-id node))]
|
(if-let [color (get colors (:fill-color-ref-id node))]
|
||||||
(assoc node :fill-color (:value color))
|
(assoc node :fill-color (:value color))
|
||||||
node))]
|
node))]
|
||||||
(generate-sync-text-shape shape page-id component-id update-node))
|
(generate-sync-text-shape shape page-id component-id update-node))
|
||||||
|
@ -265,7 +273,7 @@
|
||||||
(recur (next attrs)
|
(recur (next attrs)
|
||||||
roperations
|
roperations
|
||||||
uoperations)
|
uoperations)
|
||||||
(let [color (get library-items (get shape attr-ref-id))
|
(let [color (get colors (get shape attr-ref-id))
|
||||||
roperation {:type :set
|
roperation {:type :set
|
||||||
:attr attr
|
:attr attr
|
||||||
:val (:value color)
|
:val (:value color)
|
||||||
|
@ -276,15 +284,16 @@
|
||||||
:ignore-touched true}]
|
:ignore-touched true}]
|
||||||
(recur (next attrs)
|
(recur (next attrs)
|
||||||
(conj roperations roperation)
|
(conj roperations roperation)
|
||||||
(conj uoperations uoperation))))))))))
|
(conj uoperations uoperation)))))))))))
|
||||||
|
|
||||||
(defmethod generate-sync-shape :typographies
|
(defmethod generate-sync-shape :typographies
|
||||||
[_ library-id library-items _ page-id component-id shape]
|
[_ library-id state _ page-id component-id shape]
|
||||||
|
|
||||||
;; Synchronize a shape that uses some typographies of the library. The attributes
|
;; Synchronize a shape that uses some typographies of the library. The attributes
|
||||||
;; of the typography are copied to the shape."
|
;; of the typography are copied to the shape."
|
||||||
(let [update-node (fn [node]
|
(let [typographies (get-assets library-id :typographies state)
|
||||||
(if-let [typography (get library-items (:typography-ref-id node))]
|
update-node (fn [node]
|
||||||
|
(if-let [typography (get typographies (:typography-ref-id node))]
|
||||||
(merge node (d/without-keys typography [:name :id]))
|
(merge node (d/without-keys typography [:name :id]))
|
||||||
node))]
|
node))]
|
||||||
(generate-sync-text-shape shape page-id component-id update-node)))
|
(generate-sync-text-shape shape page-id component-id update-node)))
|
||||||
|
@ -292,16 +301,59 @@
|
||||||
|
|
||||||
;; ---- Component synchronization helpers ----
|
;; ---- Component synchronization helpers ----
|
||||||
|
|
||||||
|
(defn- get-assets
|
||||||
|
[library-id asset-type state]
|
||||||
|
(if (nil? library-id)
|
||||||
|
(get-in state [:workspace-data asset-type])
|
||||||
|
(get-in state [:workspace-libraries library-id :data asset-type])))
|
||||||
|
|
||||||
|
(defn- get-component
|
||||||
|
[state file-id component-id]
|
||||||
|
(let [components (if (nil? file-id)
|
||||||
|
(get-in state [:workspace-data :components])
|
||||||
|
(get-in state [:workspace-libraries file-id :data :components]))]
|
||||||
|
(get components component-id)))
|
||||||
|
|
||||||
|
(defn resolve-shapes-and-components
|
||||||
|
"Get all shapes inside a component instance, and the component they are
|
||||||
|
linked with. If follow-indirection? is true, and the shape corresponding
|
||||||
|
to the root shape is also a component instance, follow the link and get
|
||||||
|
the final component."
|
||||||
|
[shape objects state follow-indirection?]
|
||||||
|
(loop [all-shapes (cph/get-object-with-children (:id shape) objects)
|
||||||
|
local-objects objects
|
||||||
|
local-shape shape]
|
||||||
|
|
||||||
|
(let [root-shape (cph/get-root-shape local-shape local-objects)
|
||||||
|
component (get-component state
|
||||||
|
(get root-shape :component-file)
|
||||||
|
(get root-shape :component-id))
|
||||||
|
component-shape (get-in component [:objects (:shape-ref local-shape)])]
|
||||||
|
|
||||||
|
(if (or (nil? (:component-id component-shape))
|
||||||
|
(not follow-indirection?))
|
||||||
|
[all-shapes component component-shape]
|
||||||
|
(let [resolve-indirection
|
||||||
|
(fn [shape]
|
||||||
|
(let [component-shape (get-in component [:objects (:shape-ref shape)])]
|
||||||
|
(-> shape
|
||||||
|
(assoc :shape-ref (:shape-ref component-shape))
|
||||||
|
(d/assoc-when :component-id (:component-id component-shape))
|
||||||
|
(d/assoc-when :component-file (:component-file component-shape)))))
|
||||||
|
new-shapes (map resolve-indirection all-shapes)]
|
||||||
|
(recur new-shapes
|
||||||
|
(:objects component)
|
||||||
|
component-shape))))))
|
||||||
|
|
||||||
(defn generate-sync-shape-and-children-components
|
(defn generate-sync-shape-and-children-components
|
||||||
"Generate changes to synchronize one shape that is linked to a component,
|
"Generate changes to synchronize one shape that the root of a component
|
||||||
and all its children. If reset-touched? is false, same considerations as
|
instance, and all its children, from the given component.
|
||||||
in generate-sync-shape :components. If it's true, all attributes of the
|
If reset? is false, all atributes of each component shape that have
|
||||||
component that have changed will be copied, and the 'touched' flags in
|
changed, and whose group has not been touched in the instance shape will
|
||||||
the shapes will be cleared."
|
be copied to this one.
|
||||||
[root-shape objects components page-id component-id reset-touched?]
|
If reset? is true, all changed attributes will be copied and the 'touched'
|
||||||
(let [all-shapes (cph/get-object-with-children (:id root-shape) objects)
|
flags in the instance shape will be cleared."
|
||||||
component (get components (:component-id root-shape))
|
[root-shape all-shapes component root-component page-id component-id reset?]
|
||||||
root-component (get-in component [:objects (:shape-ref root-shape)])]
|
|
||||||
(loop [shapes (seq all-shapes)
|
(loop [shapes (seq all-shapes)
|
||||||
rchanges []
|
rchanges []
|
||||||
uchanges []]
|
uchanges []]
|
||||||
|
@ -316,20 +368,20 @@
|
||||||
component
|
component
|
||||||
page-id
|
page-id
|
||||||
component-id
|
component-id
|
||||||
reset-touched?)]
|
reset?)]
|
||||||
(recur (next shapes)
|
(recur (next shapes)
|
||||||
(d/concat rchanges shape-rchanges)
|
(d/concat rchanges shape-rchanges)
|
||||||
(d/concat uchanges shape-uchanges))))))))
|
(d/concat uchanges shape-uchanges)))))))
|
||||||
|
|
||||||
(defn generate-sync-shape-inverse
|
(defn- generate-sync-shape-inverse
|
||||||
"Generate changes to update the component a shape is linked to, from
|
"Generate changes to update the component a shape is linked to, from
|
||||||
the values in the shape and all its children. It acts like the above
|
the values in the shape and all its children.
|
||||||
function with reset-touched? as true. Also clears the 'touched' flags
|
All atributes of each instance shape that have changed, will be copied
|
||||||
in the source shapes."
|
to the component shape. Also clears the 'touched' flags in the source
|
||||||
[root-shape objects components page-id]
|
shapes.
|
||||||
(let [all-shapes (cph/get-object-with-children (:id root-shape) objects)
|
And if the component shapes are, in turn, instances of a second component,
|
||||||
component (get components (:component-id root-shape))
|
their 'touched' flags will be set accordingly."
|
||||||
root-component (get-in component [:objects (:shape-ref root-shape)])]
|
[root-shape all-shapes component root-component page-id]
|
||||||
(loop [shapes (seq all-shapes)
|
(loop [shapes (seq all-shapes)
|
||||||
rchanges []
|
rchanges []
|
||||||
uchanges []]
|
uchanges []]
|
||||||
|
@ -345,12 +397,12 @@
|
||||||
page-id)]
|
page-id)]
|
||||||
(recur (next shapes)
|
(recur (next shapes)
|
||||||
(d/concat rchanges shape-rchanges)
|
(d/concat rchanges shape-rchanges)
|
||||||
(d/concat uchanges shape-uchanges))))))))
|
(d/concat uchanges shape-uchanges)))))))
|
||||||
|
|
||||||
(defn generate-sync-shape<-component
|
(defn- generate-sync-shape<-component
|
||||||
"Generate changes to synchronize one shape that is linked to other shape
|
"Generate changes to synchronize one shape that is linked to other shape
|
||||||
inside a component. Same considerations as above about reset-touched?"
|
inside a component. Same considerations as above about reset-touched?"
|
||||||
[shape root-shape root-component component page-id component-id reset-touched?]
|
[shape root-shape root-component component page-id component-id reset?]
|
||||||
(if (nil? component)
|
(if (nil? component)
|
||||||
(remove-component-and-ref shape page-id component-id)
|
(remove-component-and-ref shape page-id component-id)
|
||||||
(let [component-shape (get (:objects component) (:shape-ref shape))]
|
(let [component-shape (get (:objects component) (:shape-ref shape))]
|
||||||
|
@ -362,25 +414,32 @@
|
||||||
root-component
|
root-component
|
||||||
page-id
|
page-id
|
||||||
component-id
|
component-id
|
||||||
reset-touched?)))))
|
{:omit-touched? (not reset?)
|
||||||
|
:reset-touched? reset?
|
||||||
|
:set-touched? false})))))
|
||||||
|
|
||||||
(defn generate-sync-shape->component
|
(defn- generate-sync-shape->component
|
||||||
"Generate changes to synchronize one shape inside a component, with other
|
"Generate changes to synchronize one shape inside a component, with other
|
||||||
shape that is linked to it."
|
shape that is linked to it."
|
||||||
[shape root-shape root-component component page-id]
|
[shape root-shape root-component component page-id]
|
||||||
|
(js/console.log "component" (clj->js component))
|
||||||
(if (nil? component)
|
(if (nil? component)
|
||||||
empty-changes
|
empty-changes
|
||||||
(let [component-shape (get (:objects component) (:shape-ref shape))]
|
(let [component-shape (get (:objects component) (:shape-ref shape))]
|
||||||
|
(js/console.log "component-shape" (clj->js component-shape))
|
||||||
(if (nil? component-shape)
|
(if (nil? component-shape)
|
||||||
empty-changes
|
empty-changes
|
||||||
(let [[rchanges1 uchanges1]
|
(let [_(js/console.info "update" (:name shape) "->" (:name component-shape))
|
||||||
|
[rchanges1 uchanges1]
|
||||||
(update-attrs component-shape
|
(update-attrs component-shape
|
||||||
shape
|
shape
|
||||||
root-component
|
root-component
|
||||||
root-shape
|
root-shape
|
||||||
nil
|
nil
|
||||||
(:id root-component)
|
(:id root-component)
|
||||||
true)
|
{:omit-touched? false
|
||||||
|
:reset-touched? false
|
||||||
|
:set-touched? true})
|
||||||
[rchanges2 uchanges2]
|
[rchanges2 uchanges2]
|
||||||
(reset-touched shape
|
(reset-touched shape
|
||||||
page-id
|
page-id
|
||||||
|
@ -391,13 +450,16 @@
|
||||||
|
|
||||||
; ---- Operation generation helpers ----
|
; ---- Operation generation helpers ----
|
||||||
|
|
||||||
(defn remove-component-and-ref
|
(defn- remove-component-and-ref
|
||||||
[shape page-id component-id]
|
[shape page-id component-id]
|
||||||
[[(d/without-nils {:type :mod-obj
|
[[(d/without-nils {:type :mod-obj
|
||||||
:id (:id shape)
|
:id (:id shape)
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:component-id component-id
|
:component-id component-id
|
||||||
:operations [{:type :set
|
:operations [{:type :set
|
||||||
|
:attr :component-root?
|
||||||
|
:val nil}
|
||||||
|
{:type :set
|
||||||
:attr :component-id
|
:attr :component-id
|
||||||
:val nil}
|
:val nil}
|
||||||
{:type :set
|
{:type :set
|
||||||
|
@ -413,6 +475,9 @@
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:component-id component-id
|
:component-id component-id
|
||||||
:operations [{:type :set
|
:operations [{:type :set
|
||||||
|
:attr :component-root?
|
||||||
|
:val (:component-root? shape)}
|
||||||
|
{:type :set
|
||||||
:attr :component-id
|
:attr :component-id
|
||||||
:val (:component-id shape)}
|
:val (:component-id shape)}
|
||||||
{:type :set
|
{:type :set
|
||||||
|
@ -424,7 +489,7 @@
|
||||||
{:type :set-touched
|
{:type :set-touched
|
||||||
:touched (:touched shape)}]})]])
|
:touched (:touched shape)}]})]])
|
||||||
|
|
||||||
(defn remove-ref
|
(defn- -remove-ref
|
||||||
[shape page-id component-id]
|
[shape page-id component-id]
|
||||||
[[(d/without-nils {:type :mod-obj
|
[[(d/without-nils {:type :mod-obj
|
||||||
:id (:id shape)
|
:id (:id shape)
|
||||||
|
@ -445,7 +510,7 @@
|
||||||
{:type :set-touched
|
{:type :set-touched
|
||||||
:touched (:touched shape)}]})]])
|
:touched (:touched shape)}]})]])
|
||||||
|
|
||||||
(defn reset-touched
|
(defn- reset-touched
|
||||||
[shape page-id component-id]
|
[shape page-id component-id]
|
||||||
[[(d/without-nils {:type :mod-obj
|
[[(d/without-nils {:type :mod-obj
|
||||||
:id (:id shape)
|
:id (:id shape)
|
||||||
|
@ -460,32 +525,42 @@
|
||||||
:operations [{:type :set-touched
|
:operations [{:type :set-touched
|
||||||
:touched (:touched shape)}]})]])
|
:touched (:touched shape)}]})]])
|
||||||
|
|
||||||
(defn update-attrs
|
(defn- update-attrs
|
||||||
"The main function that implements the sync algorithm."
|
"The main function that implements the sync algorithm. Copy
|
||||||
[shape component-shape root-shape root-component page-id component-id reset-touched?]
|
attributes that have changed in the origin shape to the dest shape.
|
||||||
|
If omit-touched? is true, attributes whose group has been touched
|
||||||
|
in the destination shape will be ignored.
|
||||||
|
If reset-touched? is true, the 'touched' flags will be cleared in
|
||||||
|
the dest shape.
|
||||||
|
If set-touched? is true, the corresponding 'touched' flags will be
|
||||||
|
set in dest shape if they are different than their current values."
|
||||||
|
[dest-shape origin-shape dest-root origin-root page-id component-id
|
||||||
|
{:keys [omit-touched? reset-touched? set-touched?] :as options}]
|
||||||
|
|
||||||
;; === Uncomment this to debug synchronization ===
|
;; === Uncomment this to debug synchronization ===
|
||||||
;; (println "SYNC"
|
;; (println "SYNC"
|
||||||
;; "[C]" (:name component-shape)
|
;; "[C]" (:name origin-shape)
|
||||||
;; "->"
|
;; "->"
|
||||||
;; (if page-id "[W]" ["C"])
|
;; (if page-id "[W]" ["C"])
|
||||||
;; (:name shape))
|
;; (:name dest-shape)
|
||||||
|
;; (str options))
|
||||||
|
|
||||||
(let [; The position attributes need a special sync algorith, because we do
|
(let [; The position attributes need a special sync algorith, because we do
|
||||||
; not synchronize the absolute position, but the position relative of
|
; not synchronize the absolute position, but the position relative of
|
||||||
; the container shape of the component.
|
; the container shape of the component.
|
||||||
new-pos (calc-new-pos shape component-shape root-shape root-component)
|
new-pos (calc-new-pos dest-shape origin-shape dest-root origin-root)
|
||||||
pos-group (get cp/component-sync-attrs :x)
|
touched (get dest-shape :touched #{})]
|
||||||
touched (get shape :touched #{})]
|
|
||||||
|
|
||||||
(loop [attrs (seq (keys (dissoc cp/component-sync-attrs :x :y)))
|
(loop [attrs (seq (keys (dissoc cp/component-sync-attrs :x :y)))
|
||||||
roperations (if (or (not (touched pos-group)) reset-touched? true)
|
roperations (if (or (not= (:x new-pos) (:x dest-shape))
|
||||||
[{:type :set :attr :x :val (:x new-pos)} ; ^ TODO: the position-group is being set
|
(not= (:y new-pos) (:y dest-shape)))
|
||||||
{:type :set :attr :y :val (:y new-pos)}] ; | as touched somewhere. Investigate why.
|
[{:type :set :attr :x :val (:x new-pos)}
|
||||||
|
{:type :set :attr :y :val (:y new-pos)}]
|
||||||
[])
|
[])
|
||||||
uoperations (if (or (not (touched pos-group)) reset-touched? true)
|
uoperations (if (or (not= (:x new-pos) (:x dest-shape))
|
||||||
[{:type :set :attr :x :val (:x shape)}
|
(not= (:y new-pos) (:y dest-shape)))
|
||||||
{:type :set :attr :y :val (:y shape)}]
|
[{:type :set :attr :x :val (:x dest-shape)}
|
||||||
|
{:type :set :attr :y :val (:y dest-shape)}]
|
||||||
[])]
|
[])]
|
||||||
|
|
||||||
(let [attr (first attrs)]
|
(let [attr (first attrs)]
|
||||||
|
@ -499,51 +574,50 @@
|
||||||
uoperations (if reset-touched?
|
uoperations (if reset-touched?
|
||||||
(conj uoperations
|
(conj uoperations
|
||||||
{:type :set-touched
|
{:type :set-touched
|
||||||
:touched (:touched shape)})
|
:touched (:touched dest-shape)})
|
||||||
uoperations)
|
uoperations)
|
||||||
|
|
||||||
rchanges [(d/without-nils {:type :mod-obj
|
rchanges [(d/without-nils {:type :mod-obj
|
||||||
:id (:id shape)
|
:id (:id dest-shape)
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:component-id component-id
|
:component-id component-id
|
||||||
:operations roperations})]
|
:operations roperations})]
|
||||||
uchanges [(d/without-nils {:type :mod-obj
|
uchanges [(d/without-nils {:type :mod-obj
|
||||||
:id (:id shape)
|
:id (:id dest-shape)
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:component-id component-id
|
:component-id component-id
|
||||||
:operations uoperations})]]
|
:operations uoperations})]]
|
||||||
[rchanges uchanges])
|
[rchanges uchanges])
|
||||||
|
|
||||||
(if-not (contains? shape attr)
|
(if-not (contains? dest-shape attr)
|
||||||
(recur (next attrs)
|
(recur (next attrs)
|
||||||
roperations
|
roperations
|
||||||
uoperations)
|
uoperations)
|
||||||
(let [roperation {:type :set
|
(let [roperation {:type :set
|
||||||
:attr attr
|
:attr attr
|
||||||
:val (get component-shape attr)
|
:val (get origin-shape attr)
|
||||||
:ignore-touched true}
|
:ignore-touched (not set-touched?)}
|
||||||
uoperation {:type :set
|
uoperation {:type :set
|
||||||
:attr attr
|
:attr attr
|
||||||
:val (get shape attr)
|
:val (get dest-shape attr)
|
||||||
:ignore-touched true}
|
:ignore-touched (not set-touched?)}
|
||||||
|
|
||||||
attr-group (get cp/component-sync-attrs attr)]
|
attr-group (get cp/component-sync-attrs attr)]
|
||||||
(if (or (not (touched attr-group)) reset-touched?)
|
(if (and (touched attr-group) omit-touched?)
|
||||||
(recur (next attrs)
|
|
||||||
(conj roperations roperation)
|
|
||||||
(conj uoperations uoperation))
|
|
||||||
(recur (next attrs)
|
(recur (next attrs)
|
||||||
roperations
|
roperations
|
||||||
uoperations)))))))))
|
uoperations)
|
||||||
|
(recur (next attrs)
|
||||||
|
(conj roperations roperation)
|
||||||
|
(conj uoperations uoperation))))))))))
|
||||||
|
|
||||||
(defn calc-new-pos
|
(defn- calc-new-pos
|
||||||
[shape component-shape root-shape root-component]
|
[dest-shape origin-shape dest-root origin-root]
|
||||||
(let [root-pos (gpt/point (:x root-shape) (:y root-shape))
|
(let [root-pos (gpt/point (:x dest-root) (:y dest-root))
|
||||||
root-component-pos (gpt/point (:x root-component) (:y root-component))
|
origin-root-pos (gpt/point (:x origin-root) (:y origin-root))
|
||||||
component-pos (gpt/point (:x component-shape) (:y component-shape))
|
origin-pos (gpt/point (:x origin-shape) (:y origin-shape))
|
||||||
delta (gpt/subtract component-pos root-component-pos)
|
delta (gpt/subtract origin-pos origin-root-pos)
|
||||||
shape-pos (gpt/point (:x shape) (:y shape))
|
shape-pos (gpt/point (:x dest-shape) (:y dest-shape))
|
||||||
new-pos (gpt/add root-pos delta)]
|
new-pos (gpt/add root-pos delta)]
|
||||||
new-pos))
|
new-pos))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -106,8 +106,7 @@
|
||||||
(show-component [shape objects]
|
(show-component [shape objects]
|
||||||
(if (nil? (:shape-ref shape))
|
(if (nil? (:shape-ref shape))
|
||||||
""
|
""
|
||||||
(let [root-id (cph/get-root-component (:id shape) objects)
|
(let [root-shape (cph/get-root-shape shape objects)
|
||||||
root-shape (when root-id (get objects root-id))
|
|
||||||
component-id (when root-shape (:component-id root-shape))
|
component-id (when root-shape (:component-id root-shape))
|
||||||
component-file-id (when root-shape (:component-file root-shape))
|
component-file-id (when root-shape (:component-file root-shape))
|
||||||
component-file (when component-file-id (get libraries component-file-id))
|
component-file (when component-file-id (get libraries component-file-id))
|
||||||
|
@ -118,7 +117,9 @@
|
||||||
component-shape (when (and component (:shape-ref shape))
|
component-shape (when (and component (:shape-ref shape))
|
||||||
(get-in component [:objects (:shape-ref shape)]))]
|
(get-in component [:objects (:shape-ref shape)]))]
|
||||||
(str/format " %s--> %s%s%s"
|
(str/format " %s--> %s%s%s"
|
||||||
(if (:component-root? shape) "#" "-")
|
(cond (:component-root? shape) "#"
|
||||||
|
(:component-id shape) "@"
|
||||||
|
:else "-")
|
||||||
(when component-file (str/format "<%s> " (:name component-file)))
|
(when component-file (str/format "<%s> " (:name component-file)))
|
||||||
(:name component-shape)
|
(:name component-shape)
|
||||||
(if (or (:component-root? shape)
|
(if (or (:component-root? shape)
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
|
[app.main.data.workspace.common :as dwc]
|
||||||
[app.main.data.workspace.libraries :as dwl]
|
[app.main.data.workspace.libraries :as dwl]
|
||||||
[app.main.ui.hooks :refer [use-rxsub]]
|
[app.main.ui.hooks :refer [use-rxsub]]
|
||||||
[app.main.ui.components.dropdown :refer [dropdown]]))
|
[app.main.ui.components.dropdown :refer [dropdown]]))
|
||||||
|
@ -65,8 +66,10 @@
|
||||||
do-detach-component #(st/emit! (dwl/detach-component id))
|
do-detach-component #(st/emit! (dwl/detach-component id))
|
||||||
do-reset-component #(st/emit! (dwl/reset-component id))
|
do-reset-component #(st/emit! (dwl/reset-component id))
|
||||||
do-update-component #(do
|
do-update-component #(do
|
||||||
|
(st/emit! dwc/start-undo-transaction)
|
||||||
(st/emit! (dwl/update-component id))
|
(st/emit! (dwl/update-component id))
|
||||||
(st/emit! (dwl/sync-file nil)))
|
(st/emit! (dwl/sync-file nil))
|
||||||
|
(st/emit! dwc/commit-undo-transaction))
|
||||||
do-navigate-component-file #(st/emit! (dwl/nav-to-component-file
|
do-navigate-component-file #(st/emit! (dwl/nav-to-component-file
|
||||||
(:component-file shape)))]
|
(:component-file shape)))]
|
||||||
[:*
|
[:*
|
||||||
|
|
|
@ -89,7 +89,8 @@
|
||||||
:default-value (:name shape "")}]
|
:default-value (:name shape "")}]
|
||||||
[:span.element-name
|
[:span.element-name
|
||||||
{:on-double-click on-click}
|
{:on-double-click on-click}
|
||||||
(:name shape "")])))
|
(:name shape "")
|
||||||
|
(when (seq (:touched shape)) " *")])))
|
||||||
|
|
||||||
(defn- make-collapsed-iref
|
(defn- make-collapsed-iref
|
||||||
[id]
|
[id]
|
||||||
|
@ -305,6 +306,7 @@
|
||||||
:component-id
|
:component-id
|
||||||
:component-file
|
:component-file
|
||||||
:shape-ref
|
:shape-ref
|
||||||
|
:touched
|
||||||
:metadata])]
|
:metadata])]
|
||||||
(persistent!
|
(persistent!
|
||||||
(reduce-kv (fn [res id obj]
|
(reduce-kv (fn [res id obj]
|
||||||
|
|
Loading…
Add table
Reference in a new issue