From c38d0e3211d902a2aa98991871144e2966e044ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Thu, 22 Oct 2020 16:14:46 +0200 Subject: [PATCH] :sparkles: Rework nested components to avoid indirect references --- common/app/common/pages_helpers.cljc | 23 +- frontend/src/app/main/data/modal.cljs | 12 +- .../app/main/data/workspace/libraries.cljs | 831 +++++++++--------- .../data/workspace/libraries_helpers.cljs | 314 +++++-- frontend/src/app/main/store.cljs | 11 +- 5 files changed, 674 insertions(+), 517 deletions(-) diff --git a/common/app/common/pages_helpers.cljc b/common/app/common/pages_helpers.cljc index 28f5a3b40..b53f2714b 100644 --- a/common/app/common/pages_helpers.cljc +++ b/common/app/common/pages_helpers.cljc @@ -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] diff --git a/frontend/src/app/main/data/modal.cljs b/frontend/src/app/main/data/modal.cljs index cd0a7c2c5..2d5181bb7 100644 --- a/frontend/src/app/main/data/modal.cljs +++ b/frontend/src/app/main/data/modal.cljs @@ -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 diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 44aa7881f..e0f03ff49 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -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)))))) + diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 6144cbcfb..c1b57cf95 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -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 diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index 5814c4a5d..28936dd5f 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -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)