0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-11 06:21:30 -05:00

Rework nested components to avoid indirect references

This commit is contained in:
Andrés Moya 2020-10-22 16:14:46 +02:00 committed by Alonso Torres
parent 14d10af9b8
commit c38d0e3211
5 changed files with 674 additions and 517 deletions

View file

@ -35,13 +35,34 @@
(defn get-root-shape
"Get the root shape linked to a component for this shape, if any"
[shape objects]
(if (:component-root? shape)
(if (:component-id shape)
shape
(if-let [parent-id (:parent-id shape)]
(get-root-shape (get objects (:parent-id shape))
objects)
nil)))
(defn get-container
[page-id component-id local-file]
(if (some? page-id)
(get-in local-file [:pages-index page-id])
(get-in local-file [:components component-id])))
(defn get-shape
[container shape-id]
(get-in container [:objects shape-id]))
(defn get-component
[component-id file-id local-file libraries]
(let [file (if (nil? file-id)
local-file
(get-in libraries [file-id :data]))]
(get-in file [:components component-id])))
(defn get-component-root
[component]
(get-in component [:objects (:id component)]))
(defn get-children
"Retrieve all children ids recursively for a given object"
[id objects]

View file

@ -20,18 +20,20 @@
(defn show
([props]
(show (uuid/next) (:type props) props))
([type props] (show (uuid/next) type props))
([type props]
(show (uuid/next) type props))
([id type props]
(ptk/reify ::show-modal
ptk/UpdateEvent
(update [_ state]
(assoc state ::modal {:id id
:type type
:props props
:allow-click-outside false})))))
:type type
:props props
:allow-click-outside false})))))
(defn update-props
([type props]
(ptk/reify ::show-modal
(ptk/reify ::update-modal-props
ptk/UpdateEvent
(update [_ state]
(cond-> state

View file

@ -125,423 +125,6 @@
:object prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(def add-component
(ptk/reify ::add-component
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
shapes (dws/shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [;; If the selected shape is a group, we can use it. If not,
;; we need to create a group before creating the component.
[group rchanges uchanges]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(dws/prepare-create-group page-id shapes "Component-" true))
[new-shape new-shapes updated-shapes]
(dwlh/make-component-shape group objects)
rchanges (conj rchanges
{:type :add-component
:id (:id new-shape)
:name (:name new-shape)
:shapes new-shapes})
rchanges (into rchanges
(map (fn [updated-shape]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id updated-shape)}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :component-root?
:val (:component-root? updated-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref updated-shape)}
{:type :set
:attr :touched
:val (:touched updated-shape)}]})
updated-shapes))
uchanges (conj uchanges
{:type :del-component
:id (:id new-shape)})
uchanges (into uchanges
(map (fn [updated-shape]
(let [original-shape (get objects (:id updated-shape))]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id original-shape)}
{:type :set
:attr :component-file
:val (:component-file original-shape)}
{:type :set
:attr :component-root?
:val (:component-root? original-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref original-shape)}
{:type :set
:attr :touched
:val (:touched original-shape)}]}))
updated-shapes))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
(defn delete-component
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-component
ptk/WatchEvent
(watch [_ state stream]
(let [component (get-in state [:workspace-data :components id])
rchanges [{:type :del-component
:id id}]
uchanges [{:type :add-component
:id id
:name (:name component)
:shapes (vals (:objects component))}]]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn instantiate-component
[file-id component-id position]
(us/assert (s/nilable ::us/uuid) file-id)
(us/assert ::us/uuid component-id)
(ptk/reify ::instantiate-component
ptk/WatchEvent
(watch [_ state stream]
(let [component (if (nil? file-id)
(get-in state [:workspace-data :components component-id])
(get-in state [:workspace-libraries file-id :data :components component-id]))
component-shape (get-in component [:objects (:id component)])
orig-pos (gpt/point (:x component-shape) (:y component-shape))
delta (gpt/subtract position orig-pos)
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
unames (atom (dwc/retrieve-used-names objects))
frame-id (cph/frame-id-by-position objects (gpt/add orig-pos delta))
update-new-shape
(fn [new-shape original-shape]
(let [new-name
(dwc/generate-unique-name @unames (:name new-shape))]
(swap! unames conj new-name)
(cond-> new-shape
true
(as-> $
(assoc $ :name new-name)
(geom/move $ delta)
(assoc $ :frame-id frame-id)
(assoc $ :parent-id
(or (:parent-id $) (:frame-id $)))
(assoc $ :shape-ref (:id original-shape))
(dissoc $ :touched))
(nil? (:parent-id original-shape))
(assoc :component-id (:id original-shape)
:component-root? true)
(and (nil? (:parent-id original-shape)) (some? file-id))
(assoc :component-file file-id)
(and (nil? (:parent-id original-shape)) (nil? file-id))
(dissoc :component-file)
(some? (:parent-id original-shape))
(dissoc :component-root?))))
[new-shape new-shapes _]
(cph/clone-object component-shape
nil
(get component :objects)
update-new-shape)
rchanges (map (fn [obj]
{:type :add-obj
:id (:id obj)
:page-id page-id
:frame-id (:frame-id obj)
:parent-id (:parent-id obj)
:obj obj})
new-shapes)
uchanges (map (fn [obj]
{:type :del-obj
:id (:id obj)
:page-id page-id})
new-shapes)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id new-shape))))))))
(defn detach-component
[id]
(us/assert ::us/uuid id)
(ptk/reify ::detach-component
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
shapes (cph/get-object-with-children id objects)
rchanges (map (fn [obj]
{:type :mod-obj
:page-id page-id
:id (:id obj)
:operations [{:type :set
:attr :component-id
:val nil}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :shape-ref
:val nil}]})
shapes)
uchanges (map (fn [obj]
{:type :mod-obj
:page-id page-id
:id (:id obj)
:operations [{:type :set
:attr :component-id
:val (:component-id obj)}
{:type :set
:attr :component-file
:val (:component-file obj)}
{:type :set
:attr :shape-ref
:val (:shape-ref obj)}]})
shapes)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn nav-to-component-file
[file-id]
(us/assert ::us/uuid file-id)
(ptk/reify ::nav-to-component-file
ptk/WatchEvent
(watch [_ state stream]
(let [file (get-in state [:workspace-libraries file-id])
pparams {:project-id (:project-id file)
:file-id (:id file)}
qparams {:page-id (first (get-in file [:data :pages]))}]
(st/emit! (rt/nav-new-window :workspace pparams qparams))))))
(defn ext-library-changed
[file-id modified-at changes]
(us/assert ::us/uuid file-id)
(us/assert ::cp/changes changes)
(ptk/reify ::ext-library-changed
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-libraries file-id :modified-at] modified-at)
(d/update-in-when [:workspace-libraries file-id :data]
cp/process-changes changes)))))
(defn reset-component
[id]
(us/assert ::us/uuid id)
(ptk/reify ::reset-component
ptk/WatchEvent
(watch [_ state stream]
;; ===== Uncomment this to debug =====
;; (js/console.info "##### RESET-COMPONENT of shape" (str id))
(let [page-id (:current-page-id state)
page (get-in state [:workspace-data :pages-index page-id])
objects (dwc/lookup-page-objects state page-id)
shape (get objects id)
file-id (get shape :component-file)
[all-shapes component root-component]
(dwlh/resolve-shapes-and-components shape
objects
state
true)
;; ===== Uncomment this to debug =====
;; _ (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]
(dwlh/generate-sync-shape-and-children-components shape
all-shapes
component
root-component
(:id page)
nil
true)]
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn update-component
[id]
(us/assert ::us/uuid id)
(ptk/reify ::update-component
ptk/WatchEvent
(watch [_ state stream]
;; ===== Uncomment this to debug =====
;; (js/console.info "##### UPDATE-COMPONENT of shape" (str id))
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
shape (get objects id)
file-id (get shape :component-file)
[all-shapes component root-component]
(dwlh/resolve-shapes-and-components shape
objects
state
true)
;; ===== Uncomment this to debug =====
;; _ (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]
(dwlh/generate-sync-shape-inverse shape
all-shapes
component
root-component
page-id)]
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(declare sync-file-2nd-stage)
(defn sync-file
[file-id]
(us/assert (s/nilable ::us/uuid) file-id)
(ptk/reify ::sync-file
ptk/UpdateEvent
(update [_ state]
(if file-id
(assoc-in state [:workspace-libraries file-id :synced-at] (dt/now))
state))
ptk/WatchEvent
(watch [_ state stream]
;; ===== Uncomment this to debug =====
;; (js/console.info "##### SYNC-FILE" (str (or file-id "local")))
(let [library-changes [(dwlh/generate-sync-library :components file-id state)
(dwlh/generate-sync-library :colors file-id state)
(dwlh/generate-sync-library :typographies file-id state)]
file-changes [(dwlh/generate-sync-file :components file-id state)
(dwlh/generate-sync-file :colors file-id state)
(dwlh/generate-sync-file :typographies file-id state)]
rchanges (d/concat []
(->> library-changes (remove nil?) (map first) (flatten))
(->> file-changes (remove nil?) (map first) (flatten)))
uchanges (d/concat []
(->> library-changes (remove nil?) (map second) (flatten))
(->> file-changes (remove nil?) (map second) (flatten)))]
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/concat
(rx/of (dm/hide-tag :sync-dialog))
(when rchanges
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))
(when file-id
(rp/mutation :update-sync
{:file-id (get-in state [:workspace-file :id])
:library-id file-id}))
(when (some? library-changes)
(rx/of (sync-file-2nd-stage file-id))))))))
(defn sync-file-2nd-stage
"If some components have been modified, we need to launch another synchronization
to update the instances of the changed components."
;; TODO: this does not work if there are multiple nested components. Only the
;; first level will be updated.
;; To solve this properly, it would be better to launch another sync-file
;; recursively. But for this not to cause an infinite loop, we need to
;; implement updated-at at component level, to detect what components have
;; not changed, and then not to apply sync and terminate the loop.
[file-id]
(us/assert (s/nilable ::us/uuid) file-id)
(ptk/reify ::sync-file-2nd-stage
ptk/WatchEvent
(watch [_ state stream]
;; ===== Uncomment this to debug =====
;; (js/console.info "##### SYNC-FILE" (str (or file-id "local")) "(2nd stage)")
(let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components nil state)
[rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state)
rchanges (d/concat rchanges1 rchanges2)
uchanges (d/concat uchanges1 uchanges2)]
(when rchanges
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))))
(def ignore-sync
(ptk/reify ::sync-file
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-file :ignore-sync-until] (dt/now)))
ptk/WatchEvent
(watch [_ state stream]
(rp/mutation :ignore-sync
{:file-id (get-in state [:workspace-file :id])
:date (dt/now)}))))
(defn notify-sync-file
[file-id]
(us/assert ::us/uuid file-id)
(ptk/reify ::notify-sync-file
ptk/WatchEvent
(watch [_ state stream]
(let [libraries-need-sync (filter #(> (:modified-at %) (:synced-at %))
(vals (get state :workspace-libraries)))
do-update #(do (apply st/emit! (map (fn [library]
(sync-file (:id library)))
libraries-need-sync))
(st/emit! dm/hide))
do-dismiss #(do (st/emit! ignore-sync)
(st/emit! dm/hide))]
(rx/of (dm/info-dialog
(tr "workspace.updates.there-are-updates")
:inline-actions
[{:label (tr "workspace.updates.update")
:callback do-update}
{:label (tr "workspace.updates.dismiss")
:callback do-dismiss}]
:sync-dialog))))))
(defn add-typography
([typography] (add-typography typography true))
([typography edit?]
@ -586,3 +169,417 @@
uchg {:type :add-typography
:typography prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(def add-component
"Add a new component to current file library, from the currently selected shapes"
(ptk/reify ::add-component
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
shapes (dws/shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [;; If the selected shape is a group, we can use it. If not,
;; we need to create a group before creating the component.
[group rchanges uchanges]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(dws/prepare-create-group page-id shapes "Component-" true))
[new-shape new-shapes updated-shapes]
(dwlh/make-component-shape group objects)
rchanges (conj rchanges
{:type :add-component
:id (:id new-shape)
:name (:name new-shape)
:shapes new-shapes})
rchanges (into rchanges
(map (fn [updated-shape]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id updated-shape)}
{:type :set
:attr :component-file
:val (:component-file updated-shape)}
{:type :set
:attr :component-root?
:val (:component-root? updated-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref updated-shape)}
{:type :set
:attr :touched
:val (:touched updated-shape)}]})
updated-shapes))
uchanges (conj uchanges
{:type :del-component
:id (:id new-shape)})
uchanges (into uchanges
(map (fn [updated-shape]
(let [original-shape (get objects (:id updated-shape))]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id original-shape)}
{:type :set
:attr :component-file
:val (:component-file original-shape)}
{:type :set
:attr :component-root?
:val (:component-root? original-shape)}
{:type :set
:attr :shape-ref
:val (:shape-ref original-shape)}
{:type :set
:attr :touched
:val (:touched original-shape)}]}))
updated-shapes))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
(defn delete-component
"Delete the component with the given id, from the current file library."
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-component
ptk/WatchEvent
(watch [_ state stream]
(let [component (get-in state [:workspace-data :components id])
rchanges [{:type :del-component
:id id}]
uchanges [{:type :add-component
:id id
:name (:name component)
:shapes (vals (:objects component))}]]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn instantiate-component
"Create a new shape in the current page, from the component with the given id
in the given file library (if file-id is nil, take it from the current file library)."
[file-id component-id position]
(us/assert (s/nilable ::us/uuid) file-id)
(us/assert ::us/uuid component-id)
(us/assert ::us/point position)
(ptk/reify ::instantiate-component
ptk/WatchEvent
(watch [_ state stream]
(let [component (if (nil? file-id)
(get-in state [:workspace-data :components component-id])
(get-in state [:workspace-libraries file-id :data :components component-id]))
component-shape (get-in component [:objects (:id component)])
orig-pos (gpt/point (:x component-shape) (:y component-shape))
delta (gpt/subtract position orig-pos)
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
unames (atom (dwc/retrieve-used-names objects))
frame-id (cph/frame-id-by-position objects (gpt/add orig-pos delta))
update-new-shape
(fn [new-shape original-shape]
(let [new-name
(dwc/generate-unique-name @unames (:name new-shape))]
(swap! unames conj new-name)
(cond-> new-shape
true
(as-> $
(assoc $ :name new-name)
(geom/move $ delta)
(assoc $ :frame-id frame-id)
(assoc $ :parent-id
(or (:parent-id $) (:frame-id $))))
(nil? (:shape-ref original-shape))
(assoc :shape-ref (:id original-shape))
(nil? (:parent-id original-shape))
(assoc :component-id (:id original-shape)
:component-root? true)
(and (nil? (:parent-id original-shape)) (some? file-id))
(assoc :component-file file-id)
(and (nil? (:parent-id original-shape)) (nil? file-id))
(dissoc :component-file)
(and (some? (:component-id original-shape))
(nil? (:component-file original-shape))
(some? file-id))
(assoc :component-file file-id)
(some? (:parent-id original-shape))
(dissoc :component-root?))))
[new-shape new-shapes _]
(cph/clone-object component-shape
nil
(get component :objects)
update-new-shape)
rchanges (map (fn [obj]
{:type :add-obj
:id (:id obj)
:page-id page-id
:frame-id (:frame-id obj)
:parent-id (:parent-id obj)
:obj obj})
new-shapes)
uchanges (map (fn [obj]
{:type :del-obj
:id (:id obj)
:page-id page-id})
new-shapes)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id new-shape))))))))
(defn detach-component
"Remove all references to components in the shape with the given id,
and all its children, at the current page."
[id]
(us/assert ::us/uuid id)
(ptk/reify ::detach-component
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
shapes (cph/get-object-with-children id objects)
rchanges (map (fn [obj]
{:type :mod-obj
:page-id page-id
:id (:id obj)
:operations [{:type :set
:attr :component-id
:val nil}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :component-root?
:val nil}
{:type :set
:attr :shape-ref
:val nil}]})
shapes)
uchanges (map (fn [obj]
{:type :mod-obj
:page-id page-id
:id (:id obj)
:operations [{:type :set
:attr :component-id
:val (:component-id obj)}
{:type :set
:attr :component-file
:val (:component-file obj)}
{:type :set
:attr :component-root?
:val (:component-root? obj)}
{:type :set
:attr :shape-ref
:val (:shape-ref obj)}]})
shapes)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn nav-to-component-file
[file-id]
(us/assert ::us/uuid file-id)
(ptk/reify ::nav-to-component-file
ptk/WatchEvent
(watch [_ state stream]
(let [file (get-in state [:workspace-libraries file-id])
pparams {:project-id (:project-id file)
:file-id (:id file)}
qparams {:page-id (first (get-in file [:data :pages]))}]
(st/emit! (rt/nav-new-window :workspace pparams qparams))))))
(defn ext-library-changed
[file-id modified-at changes]
(us/assert ::us/uuid file-id)
(us/assert ::cp/changes changes)
(ptk/reify ::ext-library-changed
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-libraries file-id :modified-at] modified-at)
(d/update-in-when [:workspace-libraries file-id :data]
cp/process-changes changes)))))
(defn reset-component
"Cancels all modifications in the shape with the given id, and all its children, in
the current page. Set all attributes equal to the ones in the linked component,
and untouched."
[id]
(us/assert ::us/uuid id)
(ptk/reify ::reset-component
ptk/WatchEvent
(watch [_ state stream]
;; ===== Uncomment this to debug =====
;; (js/console.info "##### RESET-COMPONENT of shape" (str id))
(let [[rchanges uchanges]
(dwlh/generate-sync-shape-and-children-components (get state :current-page-id)
nil
id
(get state :workspace-data)
(get state :workspace-libraries)
true)]
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(defn update-component
"Modify the component linked to the shape with the given id, in the current page, so that
all attributes of its shapes are equal to the shape and its children. Also set all attributes
of the shape untouched."
[id]
(us/assert ::us/uuid id)
(ptk/reify ::update-component
ptk/WatchEvent
(watch [_ state stream]
;; ===== Uncomment this to debug =====
;; (js/console.info "##### UPDATE-COMPONENT of shape" (str id))
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
shape (get objects id)
file-id (get shape :component-file)
[rchanges uchanges]
(dwlh/generate-sync-shape-inverse (get state :current-page-id)
id
(get state :workspace-data)
(get state :workspace-libraries))]
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
(declare sync-file-2nd-stage)
(defn sync-file
"Syhchronize the library file with the given id, with the current file.
Walk through all shapes in all pages that use some color, typography or
component of the library file, and copy the new values to the shapes.
Do it also for shapes inside components of the local file library."
[file-id]
(us/assert (s/nilable ::us/uuid) file-id)
(ptk/reify ::sync-file
ptk/UpdateEvent
(update [_ state]
(if file-id
(assoc-in state [:workspace-libraries file-id :synced-at] (dt/now))
state))
ptk/WatchEvent
(watch [_ state stream]
;; ===== Uncomment this to debug =====
(js/console.info "##### SYNC-FILE" (str (or file-id "local")))
(let [library-changes [(dwlh/generate-sync-library :components file-id state)
(dwlh/generate-sync-library :colors file-id state)
(dwlh/generate-sync-library :typographies file-id state)]
file-changes [(dwlh/generate-sync-file :components file-id state)
(dwlh/generate-sync-file :colors file-id state)
(dwlh/generate-sync-file :typographies file-id state)]
rchanges (d/concat []
(->> library-changes (remove nil?) (map first) (flatten))
(->> file-changes (remove nil?) (map first) (flatten)))
uchanges (d/concat []
(->> library-changes (remove nil?) (map second) (flatten))
(->> file-changes (remove nil?) (map second) (flatten)))]
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/concat
(rx/of (dm/hide-tag :sync-dialog))
(when rchanges
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))
(when file-id
(rp/mutation :update-sync
{:file-id (get-in state [:workspace-file :id])
:library-id file-id}))
(when (some? library-changes)
(rx/of (sync-file-2nd-stage file-id))))))))
(defn sync-file-2nd-stage
"If some components have been modified, we need to launch another synchronization
to update the instances of the changed components."
;; TODO: this does not work if there are multiple nested components. Only the
;; first level will be updated.
;; To solve this properly, it would be better to launch another sync-file
;; recursively. But for this not to cause an infinite loop, we need to
;; implement updated-at at component level, to detect what components have
;; not changed, and then not to apply sync and terminate the loop.
[file-id]
(us/assert (s/nilable ::us/uuid) file-id)
(ptk/reify ::sync-file-2nd-stage
ptk/WatchEvent
(watch [_ state stream]
;; ===== Uncomment this to debug =====
(js/console.info "##### SYNC-FILE" (str (or file-id "local")) "(2nd stage)")
(let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components nil state)
[rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state)
rchanges (d/concat rchanges1 rchanges2)
uchanges (d/concat uchanges1 uchanges2)]
(when rchanges
;; ===== Uncomment this to debug =====
;; (js/console.debug "rchanges" (clj->js rchanges))
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))))
(def ignore-sync
(ptk/reify ::sync-file
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-file :ignore-sync-until] (dt/now)))
ptk/WatchEvent
(watch [_ state stream]
(rp/mutation :ignore-sync
{:file-id (get-in state [:workspace-file :id])
:date (dt/now)}))))
(defn notify-sync-file
[file-id]
(us/assert ::us/uuid file-id)
(ptk/reify ::notify-sync-file
ptk/WatchEvent
(watch [_ state stream]
(let [libraries-need-sync (filter #(> (:modified-at %) (:synced-at %))
(vals (get state :workspace-libraries)))
do-update #(do (apply st/emit! (map (fn [library]
(sync-file (:id library)))
libraries-need-sync))
(st/emit! dm/hide))
do-dismiss #(do (st/emit! ignore-sync)
(st/emit! dm/hide))]
(rx/of (dm/info-dialog
(tr "workspace.updates.there-are-updates")
:inline-actions
[{:label (tr "workspace.updates.update")
:callback do-update}
{:label (tr "workspace.updates.dismiss")
:callback do-dismiss}]
:sync-dialog))))))

View file

@ -33,9 +33,12 @@
(declare has-asset-reference-fn)
(declare get-assets)
(declare resolve-shapes-and-components)
(declare generate-sync-shape-and-children-components)
(declare generate-sync-shape-and-children-normal)
(declare generate-sync-shape-and-children-nested)
(declare generate-sync-shape-inverse)
(declare generate-sync-shape-inverse-normal)
(declare generate-sync-shape-inverse-nested)
(declare generate-sync-shape<-component)
(declare generate-sync-shape->component)
(declare remove-component-and-ref)
@ -55,23 +58,25 @@
(assert (nil? (:component-id shape)))
(assert (nil? (:component-file shape)))
(assert (nil? (:shape-ref shape)))
(let [update-new-shape (fn [new-shape original-shape]
(let [;; Ensure that the component root is not an instance and
;; it's no longer tied to a frame.
update-new-shape (fn [new-shape original-shape]
(cond-> new-shape
true
(assoc :frame-id nil)
(-> (assoc :frame-id nil)
(dissoc :component-root?))
(nil? (:parent-id new-shape))
(dissoc :component-id
:component-file
:component-root?
:shape-ref)))
;; Make the original shape an instance of the new component.
;; If one of the original shape children already was a component
;; instance, the 'instanceness' is copied into the new component.
;; instance, maintain this instanceness untouched.
update-original-shape (fn [original-shape new-shape]
(cond-> original-shape
true
(nil? (:shape-ref original-shape))
(-> (assoc :shape-ref (:id new-shape))
(dissoc :touched))
@ -124,6 +129,7 @@
"Generate changes to synchronize all shapes inside components of the current
file library, that use the given type of asset of the given library."
[asset-type library-id state]
;; (js/console.info "--- SYNC local library " (str asset-type) " from library " (str (or library-id "nil")))
(let [library-items
(if (nil? library-id)
(get-in state [:workspace-data asset-type])
@ -176,7 +182,7 @@
[asset-type library-id]
(case asset-type
:components
(fn [shape] (and (:component-root? shape)
(fn [shape] (and (:component-id shape)
(= (:component-file shape) library-id)))
:colors
@ -214,19 +220,12 @@
(defmethod generate-sync-shape :components
[_ library-id state objects page-id component-id shape]
(let [[all-shapes component root-component]
(resolve-shapes-and-components shape
objects
state
false)]
(generate-sync-shape-and-children-components shape
all-shapes
component
root-component
page-id
component-id
false)))
(generate-sync-shape-and-children-components page-id
component-id
(:id shape)
(get state :workspace-data)
(get state :workspace-libraries)
false))
(defn- generate-sync-text-shape [shape page-id component-id update-node]
(let [old-content (:content shape)
@ -328,37 +327,6 @@
(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
"Generate changes to synchronize one shape that the root of a component
instance, and all its children, from the given component.
@ -367,25 +335,108 @@
be copied to this one.
If reset? is true, all changed attributes will be copied and the 'touched'
flags in the instance shape will be cleared."
[root-shape all-shapes component root-component page-id component-id reset?]
(loop [shapes (seq all-shapes)
rchanges []
uchanges []]
(let [shape (first shapes)]
(if (nil? shape)
[page-id component-id shape-id local-file libraries reset?]
(let [container (cph/get-container page-id component-id local-file)
shape (cph/get-shape container shape-id)
component (cph/get-component (:component-id shape)
(:component-file shape)
local-file
libraries)
root-shape shape
root-component (cph/get-component-root component)]
(generate-sync-shape-and-children-normal page-id
component-id
container
shape
component
root-shape
root-component
reset?)))
(defn- generate-sync-shape-and-children-normal
[page-id component-id container shape component root-shape root-component reset?]
(let [[rchanges uchanges]
(generate-sync-shape<-component shape
root-shape
root-component
component
page-id
component-id
reset?)
children-ids (get shape :shapes [])]
(loop [children-ids (seq children-ids)
rchanges rchanges
uchanges uchanges]
(let [child-id (first children-ids)]
(if (nil? child-id)
[rchanges uchanges]
(let [child-shape (cph/get-shape container child-id)
[child-rchanges child-uchanges]
(if (nil? (:component-id child-shape))
(generate-sync-shape-and-children-normal page-id
component-id
container
child-shape
component
root-shape
root-component
reset?)
(generate-sync-shape-and-children-nested page-id
component-id
container
child-shape
component
root-shape
root-component
reset?))]
(recur (next children-ids)
(d/concat rchanges child-rchanges)
(d/concat uchanges child-uchanges))))))))
(defn- generate-sync-shape-and-children-nested
[page-id component-id container shape component root-shape root-component reset?]
(let [component-shape (d/seek #(= (:shape-ref %)
(:shape-ref shape))
(vals (:objects component)))
[rchanges uchanges]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape<-component
shape
root-shape
root-component
component
page-id
component-id
reset?)]
(recur (next shapes)
(d/concat rchanges shape-rchanges)
(d/concat uchanges shape-uchanges)))))))
(update-attrs shape
component-shape
root-shape
root-component
page-id
component-id
{:omit-touched? false
:reset-touched? false
:set-touched? false
:copy-touched? true})
children-ids (get shape :shapes [])]
(loop [children-ids (seq children-ids)
rchanges rchanges
uchanges uchanges]
(let [child-id (first children-ids)]
(if (nil? child-id)
[rchanges uchanges]
(let [child-shape (cph/get-shape container child-id)
[child-rchanges child-uchanges]
(generate-sync-shape-and-children-nested page-id
component-id
container
child-shape
component
root-shape
root-component
reset?)]
(recur (next children-ids)
(d/concat rchanges child-rchanges)
(d/concat uchanges child-uchanges))))))))
(defn- generate-sync-shape-inverse
"Generate changes to update the component a shape is linked to, from
@ -395,23 +446,94 @@
shapes.
And if the component shapes are, in turn, instances of a second component,
their 'touched' flags will be set accordingly."
[root-shape all-shapes component root-component page-id]
(loop [shapes (seq all-shapes)
rchanges []
uchanges []]
(let [shape (first shapes)]
(if (nil? shape)
[page-id shape-id local-file libraries]
(let [page (cph/get-container page-id nil local-file)
shape (cph/get-shape page shape-id)
component (cph/get-component (:component-id shape)
(:component-file shape)
local-file
libraries)
root-shape shape
root-component (cph/get-component-root component)]
(generate-sync-shape-inverse-normal page
shape
component
root-shape
root-component)))
(defn- generate-sync-shape-inverse-normal
[page shape component root-shape root-component]
(let [[rchanges uchanges]
(generate-sync-shape->component shape
root-shape
root-component
component
(:id page))
children-ids (get shape :shapes [])]
(loop [children-ids (seq children-ids)
rchanges rchanges
uchanges uchanges]
(let [child-id (first children-ids)]
(if (nil? child-id)
[rchanges uchanges]
(let [child-shape (cph/get-shape page child-id)
[child-rchanges child-uchanges]
(if (nil? (:component-id child-shape))
(generate-sync-shape-inverse-normal page
child-shape
component
root-shape
root-component)
(generate-sync-shape-inverse-nested page
child-shape
component
root-shape
root-component))]
(recur (next children-ids)
(d/concat rchanges child-rchanges)
(d/concat uchanges child-uchanges))))))))
(defn- generate-sync-shape-inverse-nested
[page shape component root-shape root-component]
(let [component-shape (d/seek #(= (:shape-ref %)
(:shape-ref shape))
(vals (:objects component)))
[rchanges uchanges]
(let [[shape-rchanges shape-uchanges]
(generate-sync-shape->component
shape
root-shape
root-component
component
page-id)]
(recur (next shapes)
(d/concat rchanges shape-rchanges)
(d/concat uchanges shape-uchanges)))))))
(update-attrs component-shape
shape
root-component
root-shape
nil
(:id component)
{:omit-touched? false
:reset-touched? false
:set-touched? false
:copy-touched? true})
children-ids (get shape :shapes [])]
(loop [children-ids (seq children-ids)
rchanges rchanges
uchanges uchanges]
(let [child-id (first children-ids)]
(if (nil? child-id)
[rchanges uchanges]
(let [child-shape (cph/get-shape page child-id)
[child-rchanges child-uchanges]
(generate-sync-shape-inverse-nested page
child-shape
component
root-shape
root-component)]
(recur (next children-ids)
(d/concat rchanges child-rchanges)
(d/concat uchanges child-uchanges))))))))
(defn- generate-sync-shape<-component
"Generate changes to synchronize one shape that is linked to other shape
@ -552,13 +674,17 @@
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}]
{:keys [omit-touched? reset-touched? set-touched? copy-touched?]
:as options :or {omit-touched? false
reset-touched? false
set-touched? false
copy-touched? false}}]
;; === Uncomment this to debug synchronization ===
;; (println "SYNC"
;; "[C]" (:name origin-shape)
;; (:name origin-shape)
;; "->"
;; (if page-id "[W]" ["C"])
;; (if page-id "[W]" "[C]")
;; (:name dest-shape)
;; (str options))
@ -582,16 +708,24 @@
(let [attr (first attrs)]
(if (nil? attr)
(let [roperations (if reset-touched?
(let [roperations (cond
reset-touched?
(conj roperations
{:type :set-touched
:touched nil})
copy-touched?
(conj roperations
{:type :set-touched
:touched (:touched origin-shape)})
:else
roperations)
uoperations (if reset-touched?
uoperations (cond
(or reset-touched? copy-touched?)
(conj uoperations
{:type :set-touched
:touched (:touched dest-shape)})
:else
uoperations)
rchanges [(d/without-nils {:type :mod-obj

View file

@ -85,8 +85,9 @@
(logjs "state")))))
(defn ^:export dump-tree
([] (dump-tree false))
([show-touched]
([] (dump-tree false false))
([show-ids] (dump-tree show-ids false))
([show-ids show-touched]
(let [page-id (get @state :current-page-id)
objects (get-in @state [:workspace-data :pages-index page-id :objects])
components (get-in @state [:workspace-data :components])
@ -98,6 +99,7 @@
(println (str/pad (str (str/repeat " " level)
(:name shape)
(when (seq (:touched shape)) "*")
(when show-ids (str/format " <%s>" (:id shape))))
{:length 20
:type :right})
(show-component shape objects))
@ -107,7 +109,7 @@
(str (:touched shape)))))
(when (:shapes shape)
(dorun (for [shape-id (:shapes shape)]
(show-shape shape-id (inc level) objects)))))))
(show-shape shape-id (inc level) objects))))))
(show-component [shape objects]
(if (nil? (:shape-ref shape))
@ -129,7 +131,8 @@
(when component-file (str/format "<%s> " (:name component-file)))
(:name component-shape)
(if (or (:component-root? shape)
(nil? (:component-id shape)))
(nil? (:component-id shape))
true)
""
(let [component-id (:component-id shape)
component-file-id (:component-file shape)