🎉 Group component sync changes in a single undo

This commit is contained in:
Andrés Moya 2023-03-17 17:05:36 +01:00
parent fe898315c3
commit ad786ab95f
7 changed files with 142 additions and 116 deletions

View file

@ -39,6 +39,12 @@
[changes stack-undo?] [changes stack-undo?]
(assoc changes :stack-undo? stack-undo?)) (assoc changes :stack-undo? stack-undo?))
(defn set-undo-group
[changes undo-group]
(cond-> changes
(some? undo-group)
(assoc :undo-group undo-group)))
(defn with-page (defn with-page
[changes page] [changes page]
(vary-meta changes assoc (vary-meta changes assoc
@ -80,7 +86,8 @@
[changes1 changes2] [changes1 changes2]
{:redo-changes (d/concat-vec (:redo-changes changes1) (:redo-changes changes2)) {:redo-changes (d/concat-vec (:redo-changes changes1) (:redo-changes changes2))
:undo-changes (d/concat-vec (:undo-changes changes1) (:undo-changes changes2)) :undo-changes (d/concat-vec (:undo-changes changes1) (:undo-changes changes2))
:origin (:origin changes1)}) :origin (:origin changes1)
:undo-group (:undo-group changes1)})
; TODO: remove this when not needed ; TODO: remove this when not needed
(defn- assert-page-id (defn- assert-page-id

View file

@ -34,21 +34,20 @@
(declare commit-changes) (declare commit-changes)
(defn- add-group-id (defn- add-undo-group
[changes state] [changes state]
(let [undo (:workspace-undo state) (let [undo (:workspace-undo state)
items (:items undo) items (:items undo)
index (or (:index undo) (dec (count items))) index (or (:index undo) (dec (count items)))
prev-item (when-not (or (empty? items) (= index -1)) prev-item (when-not (or (empty? items) (= index -1))
(get items index)) (get items index))
group-id (:group-id prev-item) undo-group (:undo-group prev-item)
add-group-id? (and add-undo-group? (and
(not (nil? group-id)) (not (nil? undo-group))
(= (get-in changes [:redo-changes 0 :type]) :mod-obj) (= (get-in changes [:redo-changes 0 :type]) :mod-obj)
(= (get-in prev-item [:redo-changes 0 :type]) :add-obj)) ;; This is a copy-and-move with mouse+alt (= (get-in prev-item [:redo-changes 0 :type]) :add-obj))] ;; This is a copy-and-move with mouse+alt
]
(cond-> changes add-group-id? (assoc :group-id group-id))))
(cond-> changes add-undo-group? (assoc :undo-group undo-group))))
(def commit-changes? (ptk/type? ::commit-changes)) (def commit-changes? (ptk/type? ::commit-changes))
@ -81,7 +80,7 @@
(pcb/set-stack-undo? stack-undo?) (pcb/set-stack-undo? stack-undo?)
(pcb/with-objects objects)) (pcb/with-objects objects))
ids) ids)
changes (add-group-id changes state)] changes (add-undo-group changes state)]
(rx/concat (rx/concat
(if (seq (:redo-changes changes)) (if (seq (:redo-changes changes))
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids)) (let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))
@ -164,15 +163,24 @@
changes))) changes)))
(defn commit-changes (defn commit-changes
"Schedules a list of changes to execute now, and add the corresponding undo changes to
the undo stack.
Options:
- save-undo?: if set to false, do not add undo changes.
- undo-group: if some consecutive changes (or even transactions) share the same
undo-group, they will be undone or redone in a single step
"
[{:keys [redo-changes undo-changes [{:keys [redo-changes undo-changes
origin save-undo? file-id group-id stack-undo?] origin save-undo? file-id undo-group stack-undo?]
:or {save-undo? true stack-undo? false}}] :or {save-undo? true stack-undo? false undo-group (uuid/next)}}]
(log/debug :msg "commit-changes" (log/debug :msg "commit-changes"
:js/undo-group (str undo-group)
:js/redo-changes redo-changes :js/redo-changes redo-changes
:js/undo-changes undo-changes) :js/undo-changes undo-changes)
(let [error (volatile! nil) (let [error (volatile! nil)
page-id (:current-page-id @st/state) page-id (:current-page-id @st/state)
frames (changed-frames redo-changes (wsh/lookup-page-objects @st/state))] frames (changed-frames redo-changes (wsh/lookup-page-objects @st/state))]
(ptk/reify ::commit-changes (ptk/reify ::commit-changes
cljs.core/IDeref cljs.core/IDeref
(-deref [_] (-deref [_]
@ -183,8 +191,8 @@
:page-id page-id :page-id page-id
:frames frames :frames frames
:save-undo? save-undo? :save-undo? save-undo?
:stack-undo? stack-undo? :undo-group undo-group
:group-id group-id}) :stack-undo? stack-undo?})
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
@ -233,5 +241,5 @@
(when (and save-undo? (seq undo-changes)) (when (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes (let [entry {:undo-changes undo-changes
:redo-changes redo-changes :redo-changes redo-changes
:group-id group-id}] :undo-group undo-group}]
(rx/of (dwu/append-undo entry stack-undo?))))))))))) (rx/of (dwu/append-undo entry stack-undo?)))))))))))

View file

@ -63,16 +63,16 @@
(when-not (or (empty? items) (= index -1)) (when-not (or (empty? items) (= index -1))
(let [item (get items index) (let [item (get items index)
changes (:undo-changes item) changes (:undo-changes item)
group-id (:group-id item) undo-group (:undo-group item)
find-first-group-idx (fn ffgidx[index] find-first-group-idx (fn ffgidx[index]
(let [item (get items index)] (let [item (get items index)]
(if (= (:group-id item) group-id) (if (= (:undo-group item) undo-group)
(ffgidx (dec index)) (ffgidx (dec index))
(inc index)))) (inc index))))
undo-group-index (when group-id undo-group-index (when undo-group
(find-first-group-idx index))] (find-first-group-idx index))]
(if group-id (if undo-group
(rx/of (undo-to-index (dec undo-group-index))) (rx/of (undo-to-index (dec undo-group-index)))
(rx/of (dwu/materialize-undo changes (dec index)) (rx/of (dwu/materialize-undo changes (dec index))
(dch/commit-changes {:redo-changes changes (dch/commit-changes {:redo-changes changes
@ -94,16 +94,16 @@
(when-not (or (empty? items) (= index (dec (count items)))) (when-not (or (empty? items) (= index (dec (count items))))
(let [item (get items (inc index)) (let [item (get items (inc index))
changes (:redo-changes item) changes (:redo-changes item)
group-id (:group-id item) undo-group (:undo-group item)
find-last-group-idx (fn flgidx [index] find-last-group-idx (fn flgidx [index]
(let [item (get items index)] (let [item (get items index)]
(if (= (:group-id item) group-id) (if (= (:undo-group item) undo-group)
(flgidx (inc index)) (flgidx (inc index))
(dec index)))) (dec index))))
redo-group-index (when group-id redo-group-index (when undo-group
(find-last-group-idx (inc index)))] (find-last-group-idx (inc index)))]
(if group-id (if undo-group
(rx/of (undo-to-index redo-group-index)) (rx/of (undo-to-index redo-group-index))
(rx/of (dwu/materialize-undo changes (inc index)) (rx/of (dwu/materialize-undo changes (inc index))
(dch/commit-changes {:redo-changes changes (dch/commit-changes {:redo-changes changes

View file

@ -587,76 +587,79 @@
NOTE: It's possible that the component to update is defined in an NOTE: It's possible that the component to update is defined in an
external library file, so this function may cause to modify a file external library file, so this function may cause to modify a file
different of that the one we are currently editing." different of that the one we are currently editing."
[id] ([id] (update-component id nil))
(us/assert ::us/uuid id) ([id undo-group]
(ptk/reify ::update-component (us/assert ::us/uuid id)
ptk/WatchEvent (ptk/reify ::update-component
(watch [it state _] ptk/WatchEvent
(log/info :msg "UPDATE-COMPONENT of shape" :id (str id)) (watch [it state _]
(let [page-id (get state :current-page-id) (log/info :msg "UPDATE-COMPONENT of shape" :id (str id) :undo-group undo-group)
local-file (wsh/get-local-file state) (let [page-id (get state :current-page-id)
container (cph/get-container local-file :page page-id) local-file (wsh/get-local-file state)
shape (ctn/get-shape container id)] container (cph/get-container local-file :page page-id)
shape (ctn/get-shape container id)]
(when (ctk/in-component-instance? shape) (when (ctk/in-component-instance? shape)
(let [libraries (wsh/get-libraries state) (let [libraries (wsh/get-libraries state)
changes changes
(-> (pcb/empty-changes it) (-> (pcb/empty-changes it)
(pcb/with-container container) (pcb/set-undo-group undo-group)
(dwlh/generate-sync-shape-inverse libraries container id)) (pcb/with-container container)
(dwlh/generate-sync-shape-inverse libraries container id))
file-id (:component-file shape) file-id (:component-file shape)
file (wsh/get-file state file-id) file (wsh/get-file state file-id)
xf-filter (comp xf-filter (comp
(filter :local-change?) (filter :local-change?)
(map #(dissoc % :local-change?))) (map #(dissoc % :local-change?)))
local-changes (-> changes local-changes (-> changes
(update :redo-changes #(into [] xf-filter %)) (update :redo-changes #(into [] xf-filter %))
(update :undo-changes #(into [] xf-filter %))) (update :undo-changes #(into [] xf-filter %)))
xf-remove (comp xf-remove (comp
(remove :local-change?) (remove :local-change?)
(map #(dissoc % :local-change?))) (map #(dissoc % :local-change?)))
nonlocal-changes (-> changes nonlocal-changes (-> changes
(update :redo-changes #(into [] xf-remove %)) (update :redo-changes #(into [] xf-remove %))
(update :undo-changes #(into [] xf-remove %)))] (update :undo-changes #(into [] xf-remove %)))]
(log/debug :msg "UPDATE-COMPONENT finished" (log/debug :msg "UPDATE-COMPONENT finished"
:js/local-changes (log-changes :js/local-changes (log-changes
(:redo-changes local-changes) (:redo-changes local-changes)
file) file)
:js/nonlocal-changes (log-changes :js/nonlocal-changes (log-changes
(:redo-changes nonlocal-changes) (:redo-changes nonlocal-changes)
file)) file))
(rx/of (rx/of
(when (seq (:redo-changes local-changes)) (when (seq (:redo-changes local-changes))
(dch/commit-changes (assoc local-changes (dch/commit-changes (assoc local-changes
:file-id (:id local-file)))) :file-id (:id local-file))))
(when (seq (:redo-changes nonlocal-changes)) (when (seq (:redo-changes nonlocal-changes))
(dch/commit-changes (assoc nonlocal-changes (dch/commit-changes (assoc nonlocal-changes
:file-id file-id)))))))))) :file-id file-id)))))))))))
(defn update-component-sync (defn update-component-sync
[shape-id file-id] ([shape-id file-id] (update-component-sync shape-id file-id nil))
(ptk/reify ::update-component-sync ([shape-id file-id undo-group]
ptk/WatchEvent (ptk/reify ::update-component-sync
(watch [_ state _] ptk/WatchEvent
(let [current-file-id (:current-file-id state) (watch [_ state _]
page (wsh/lookup-page state) (let [current-file-id (:current-file-id state)
shape (ctn/get-shape page shape-id) page (wsh/lookup-page state)
undo-id (js/Symbol)] shape (ctn/get-shape page shape-id)
(rx/of undo-id (js/Symbol)]
(dwu/start-undo-transaction undo-id) (rx/of
(update-component shape-id) (dwu/start-undo-transaction undo-id)
(sync-file current-file-id file-id :components (:component-id shape)) (update-component shape-id undo-group)
(when (not= current-file-id file-id) (sync-file current-file-id file-id :components (:component-id shape) undo-group)
(sync-file file-id file-id :components (:component-id shape))) (when (not= current-file-id file-id)
(dwu/commit-undo-transaction undo-id)))))) (sync-file file-id file-id :components (:component-id shape) undo-group))
(dwu/commit-undo-transaction undo-id)))))))
(defn update-component-in-bulk (defn update-component-in-bulk
[shapes file-id] [shapes file-id]
@ -666,7 +669,7 @@
(let [undo-id (js/Symbol)] (let [undo-id (js/Symbol)]
(rx/concat (rx/concat
(rx/of (dwu/start-undo-transaction undo-id)) (rx/of (dwu/start-undo-transaction undo-id))
(rx/map #(update-component-sync (:id %) file-id) (rx/from shapes)) (rx/map #(update-component-sync (:id %) file-id (uuid/next)) (rx/from shapes))
(rx/of (dwu/commit-undo-transaction undo-id))))))) (rx/of (dwu/commit-undo-transaction undo-id)))))))
(declare sync-file-2nd-stage) (declare sync-file-2nd-stage)
@ -683,6 +686,8 @@
([file-id library-id] ([file-id library-id]
(sync-file file-id library-id nil nil)) (sync-file file-id library-id nil nil))
([file-id library-id asset-type asset-id] ([file-id library-id asset-type asset-id]
(sync-file file-id library-id asset-type asset-id nil))
([file-id library-id asset-type asset-id undo-group]
(us/assert ::us/uuid file-id) (us/assert ::us/uuid file-id)
(us/assert ::us/uuid library-id) (us/assert ::us/uuid library-id)
(us/assert (s/nilable #{:colors :components :typographies}) asset-type) (us/assert (s/nilable #{:colors :components :typographies}) asset-type)
@ -702,7 +707,8 @@
:file (dwlh/pretty-file file-id state) :file (dwlh/pretty-file file-id state)
:library (dwlh/pretty-file library-id state) :library (dwlh/pretty-file library-id state)
:asset-type asset-type :asset-type asset-type
:asset-id asset-id) :asset-id asset-id
:undo-group undo-group)
(let [file (wsh/get-file state file-id) (let [file (wsh/get-file state file-id)
sync-components? (or (nil? asset-type) (= asset-type :components)) sync-components? (or (nil? asset-type) (= asset-type :components))
@ -711,7 +717,8 @@
library-changes (reduce library-changes (reduce
pcb/concat-changes pcb/concat-changes
(pcb/empty-changes it) (-> (pcb/empty-changes it)
(pcb/set-undo-group undo-group))
[(when sync-components? [(when sync-components?
(dwlh/generate-sync-library it file-id :components asset-id library-id state)) (dwlh/generate-sync-library it file-id :components asset-id library-id state))
(when sync-colors? (when sync-colors?
@ -720,7 +727,8 @@
(dwlh/generate-sync-library it file-id :typographies asset-id library-id state))]) (dwlh/generate-sync-library it file-id :typographies asset-id library-id state))])
file-changes (reduce file-changes (reduce
pcb/concat-changes pcb/concat-changes
(pcb/empty-changes it) (-> (pcb/empty-changes it)
(pcb/set-undo-group undo-group))
[(when sync-components? [(when sync-components?
(dwlh/generate-sync-file it file-id :components asset-id library-id state)) (dwlh/generate-sync-file it file-id :components asset-id library-id state))
(when sync-colors? (when sync-colors?
@ -751,7 +759,7 @@
:library-id library-id}))) :library-id library-id})))
(when (and (seq (:redo-changes library-changes)) (when (and (seq (:redo-changes library-changes))
sync-components?) sync-components?)
(rx/of (sync-file-2nd-stage file-id library-id asset-id)))))))))) (rx/of (sync-file-2nd-stage file-id library-id asset-id undo-group))))))))))
(defn- sync-file-2nd-stage (defn- sync-file-2nd-stage
"If some components have been modified, we need to launch another synchronization "If some components have been modified, we need to launch another synchronization
@ -762,7 +770,7 @@
;; recursively. But for this not to cause an infinite loop, we need to ;; recursively. But for this not to cause an infinite loop, we need to
;; implement updated-at at component level, to detect what components have ;; implement updated-at at component level, to detect what components have
;; not changed, and then not to apply sync and terminate the loop. ;; not changed, and then not to apply sync and terminate the loop.
[file-id library-id asset-id] [file-id library-id asset-id undo-group]
(us/assert ::us/uuid file-id) (us/assert ::us/uuid file-id)
(us/assert ::us/uuid library-id) (us/assert ::us/uuid library-id)
(us/assert (s/nilable ::us/uuid) asset-id) (us/assert (s/nilable ::us/uuid) asset-id)
@ -775,7 +783,8 @@
(let [file (wsh/get-file state file-id) (let [file (wsh/get-file state file-id)
changes (reduce changes (reduce
pcb/concat-changes pcb/concat-changes
(pcb/empty-changes it) (-> (pcb/empty-changes it)
(pcb/set-undo-group undo-group))
[(dwlh/generate-sync-file it file-id :components asset-id library-id state) [(dwlh/generate-sync-file it file-id :components asset-id library-id state)
(dwlh/generate-sync-library it file-id :components asset-id library-id state)])] (dwlh/generate-sync-library it file-id :components asset-id library-id state)])]
@ -849,7 +858,7 @@
check-changes check-changes
(fn [[event data]] (fn [[event data]]
(let [{:keys [changes save-undo?]} (deref event) (let [{:keys [changes save-undo? undo-group]} (deref event)
components-changed (reduce #(into %1 (ch/components-changed data %2)) components-changed (reduce #(into %1 (ch/components-changed data %2))
#{} #{}
changes)] changes)]
@ -857,7 +866,7 @@
(log/info :msg "DETECTED COMPONENTS CHANGED" (log/info :msg "DETECTED COMPONENTS CHANGED"
:ids (map str components-changed)) :ids (map str components-changed))
(run! st/emit! (run! st/emit!
(map #(update-component-sync % (:id data)) (map #(update-component-sync % (:id data) undo-group)
components-changed)))))] components-changed)))))]
(when components-v2 (when components-v2

View file

@ -548,7 +548,7 @@
(defn duplicate-selected (defn duplicate-selected
([move-delta?] ([move-delta?]
(duplicate-selected move-delta? false)) (duplicate-selected move-delta? false))
([move-delta? add-group-id?] ([move-delta? add-undo-group?]
(ptk/reify ::duplicate-selected (ptk/reify ::duplicate-selected
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
@ -567,7 +567,7 @@
changes (->> (prepare-duplicate-changes objects page selected delta it libraries) changes (->> (prepare-duplicate-changes objects page selected delta it libraries)
(duplicate-changes-update-indices objects selected)) (duplicate-changes-update-indices objects selected))
changes (cond-> changes add-group-id? (assoc :group-id (uuid/random))) changes (cond-> changes add-undo-group? (assoc :undo-group (uuid/random)))
id-original (first selected) id-original (first selected)

View file

@ -67,11 +67,11 @@
(add-undo-entry state entry)))) (add-undo-entry state entry))))
(defn- accumulate-undo-entry (defn- accumulate-undo-entry
[state {:keys [undo-changes redo-changes group-id]}] [state {:keys [undo-changes redo-changes undo-group]}]
(-> state (-> state
(update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %)) (update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %))
(update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes)) (update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes))
(assoc-in [:workspace-undo :transaction :group-id] group-id))) (assoc-in [:workspace-undo :transaction :undo-group] undo-group)))
(defn append-undo (defn append-undo
[entry stack?] [entry stack?]
@ -79,29 +79,31 @@
(ptk/reify ::append-undo (ptk/reify ::append-undo
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(cond (cond
(and (get-in state [:workspace-undo :transaction]) (and (get-in state [:workspace-undo :transaction])
(or (not stack?) (or (not stack?)
(d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes])) (d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes]))
(d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes])))) (d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes]))))
(accumulate-undo-entry state entry) (accumulate-undo-entry state entry)
stack? stack?
(stack-undo-entry state entry) (stack-undo-entry state entry)
:else :else
(add-undo-entry state entry))))) (add-undo-entry state entry)))))
(def empty-tx (def empty-tx
{:undo-changes [] :redo-changes []}) {:undo-changes [] :redo-changes []})
(defn start-undo-transaction [id] (defn start-undo-transaction
"Start a transaction, so that every changes inside are added together in a single undo entry."
[id]
(ptk/reify ::start-undo-transaction (ptk/reify ::start-undo-transaction
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
;; We commit the old transaction before starting the new one ;; We commit the old transaction before starting the new one
(let [current-tx (get-in state [:workspace-undo :transaction]) (let [current-tx (get-in state [:workspace-undo :transaction])
pending-tx (get-in state [:workspace-undo :transactions-pending])] pending-tx (get-in state [:workspace-undo :transactions-pending])]
(cond-> state (cond-> state
(nil? current-tx) (assoc-in [:workspace-undo :transaction] empty-tx) (nil? current-tx) (assoc-in [:workspace-undo :transaction] empty-tx)
(nil? pending-tx) (assoc-in [:workspace-undo :transactions-pending] #{id}) (nil? pending-tx) (assoc-in [:workspace-undo :transactions-pending] #{id})

View file

@ -6,7 +6,7 @@
(ns app.main.ui.workspace.sidebar.options.menus.component (ns app.main.ui.workspace.sidebar.options.menus.component
(:require (:require
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]