0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-30 16:41:20 -05:00

🎉 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?]
(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
[changes page]
(vary-meta changes assoc
@ -80,7 +86,8 @@
[changes1 changes2]
{:redo-changes (d/concat-vec (:redo-changes changes1) (:redo-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
(defn- assert-page-id

View file

@ -34,21 +34,20 @@
(declare commit-changes)
(defn- add-group-id
(defn- add-undo-group
[changes state]
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))
prev-item (when-not (or (empty? items) (= index -1))
(get items index))
group-id (:group-id prev-item)
add-group-id? (and
(not (nil? group-id))
(= (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
]
(cond-> changes add-group-id? (assoc :group-id group-id))))
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))
prev-item (when-not (or (empty? items) (= index -1))
(get items index))
undo-group (:undo-group prev-item)
add-undo-group? (and
(not (nil? undo-group))
(= (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
(cond-> changes add-undo-group? (assoc :undo-group undo-group))))
(def commit-changes? (ptk/type? ::commit-changes))
@ -81,7 +80,7 @@
(pcb/set-stack-undo? stack-undo?)
(pcb/with-objects objects))
ids)
changes (add-group-id changes state)]
changes (add-undo-group changes state)]
(rx/concat
(if (seq (:redo-changes changes))
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))
@ -164,15 +163,24 @@
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
origin save-undo? file-id group-id stack-undo?]
:or {save-undo? true stack-undo? false}}]
origin save-undo? file-id undo-group stack-undo?]
:or {save-undo? true stack-undo? false undo-group (uuid/next)}}]
(log/debug :msg "commit-changes"
:js/undo-group (str undo-group)
:js/redo-changes redo-changes
:js/undo-changes undo-changes)
(let [error (volatile! nil)
(let [error (volatile! nil)
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
cljs.core/IDeref
(-deref [_]
@ -183,8 +191,8 @@
:page-id page-id
:frames frames
:save-undo? save-undo?
:stack-undo? stack-undo?
:group-id group-id})
:undo-group undo-group
:stack-undo? stack-undo?})
ptk/UpdateEvent
(update [_ state]
@ -233,5 +241,5 @@
(when (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:group-id group-id}]
:undo-group undo-group}]
(rx/of (dwu/append-undo entry stack-undo?)))))))))))

View file

@ -63,16 +63,16 @@
(when-not (or (empty? items) (= index -1))
(let [item (get items index)
changes (:undo-changes item)
group-id (:group-id item)
undo-group (:undo-group item)
find-first-group-idx (fn ffgidx[index]
(let [item (get items index)]
(if (= (:group-id item) group-id)
(if (= (:undo-group item) undo-group)
(ffgidx (dec index))
(inc index))))
undo-group-index (when group-id
undo-group-index (when undo-group
(find-first-group-idx index))]
(if group-id
(if undo-group
(rx/of (undo-to-index (dec undo-group-index)))
(rx/of (dwu/materialize-undo changes (dec index))
(dch/commit-changes {:redo-changes changes
@ -94,16 +94,16 @@
(when-not (or (empty? items) (= index (dec (count items))))
(let [item (get items (inc index))
changes (:redo-changes item)
group-id (:group-id item)
undo-group (:undo-group item)
find-last-group-idx (fn flgidx [index]
(let [item (get items index)]
(if (= (:group-id item) group-id)
(if (= (:undo-group item) undo-group)
(flgidx (inc index))
(dec index))))
redo-group-index (when group-id
redo-group-index (when undo-group
(find-last-group-idx (inc index)))]
(if group-id
(if undo-group
(rx/of (undo-to-index redo-group-index))
(rx/of (dwu/materialize-undo changes (inc index))
(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
external library file, so this function may cause to modify a file
different of that the one we are currently editing."
[id]
(us/assert ::us/uuid id)
(ptk/reify ::update-component
ptk/WatchEvent
(watch [it state _]
(log/info :msg "UPDATE-COMPONENT of shape" :id (str id))
(let [page-id (get state :current-page-id)
local-file (wsh/get-local-file state)
container (cph/get-container local-file :page page-id)
shape (ctn/get-shape container id)]
([id] (update-component id nil))
([id undo-group]
(us/assert ::us/uuid id)
(ptk/reify ::update-component
ptk/WatchEvent
(watch [it state _]
(log/info :msg "UPDATE-COMPONENT of shape" :id (str id) :undo-group undo-group)
(let [page-id (get state :current-page-id)
local-file (wsh/get-local-file state)
container (cph/get-container local-file :page page-id)
shape (ctn/get-shape container id)]
(when (ctk/in-component-instance? shape)
(let [libraries (wsh/get-libraries state)
(when (ctk/in-component-instance? shape)
(let [libraries (wsh/get-libraries state)
changes
(-> (pcb/empty-changes it)
(pcb/with-container container)
(dwlh/generate-sync-shape-inverse libraries container id))
changes
(-> (pcb/empty-changes it)
(pcb/set-undo-group undo-group)
(pcb/with-container container)
(dwlh/generate-sync-shape-inverse libraries container id))
file-id (:component-file shape)
file (wsh/get-file state file-id)
file-id (:component-file shape)
file (wsh/get-file state file-id)
xf-filter (comp
(filter :local-change?)
(map #(dissoc % :local-change?)))
xf-filter (comp
(filter :local-change?)
(map #(dissoc % :local-change?)))
local-changes (-> changes
(update :redo-changes #(into [] xf-filter %))
(update :undo-changes #(into [] xf-filter %)))
local-changes (-> changes
(update :redo-changes #(into [] xf-filter %))
(update :undo-changes #(into [] xf-filter %)))
xf-remove (comp
(remove :local-change?)
(map #(dissoc % :local-change?)))
xf-remove (comp
(remove :local-change?)
(map #(dissoc % :local-change?)))
nonlocal-changes (-> changes
(update :redo-changes #(into [] xf-remove %))
(update :undo-changes #(into [] xf-remove %)))]
nonlocal-changes (-> changes
(update :redo-changes #(into [] xf-remove %))
(update :undo-changes #(into [] xf-remove %)))]
(log/debug :msg "UPDATE-COMPONENT finished"
:js/local-changes (log-changes
(:redo-changes local-changes)
file)
:js/nonlocal-changes (log-changes
(:redo-changes nonlocal-changes)
file))
(log/debug :msg "UPDATE-COMPONENT finished"
:js/local-changes (log-changes
(:redo-changes local-changes)
file)
:js/nonlocal-changes (log-changes
(:redo-changes nonlocal-changes)
file))
(rx/of
(when (seq (:redo-changes local-changes))
(dch/commit-changes (assoc local-changes
:file-id (:id local-file))))
(when (seq (:redo-changes nonlocal-changes))
(dch/commit-changes (assoc nonlocal-changes
:file-id file-id))))))))))
(rx/of
(when (seq (:redo-changes local-changes))
(dch/commit-changes (assoc local-changes
:file-id (:id local-file))))
(when (seq (:redo-changes nonlocal-changes))
(dch/commit-changes (assoc nonlocal-changes
:file-id file-id)))))))))))
(defn update-component-sync
[shape-id file-id]
(ptk/reify ::update-component-sync
ptk/WatchEvent
(watch [_ state _]
(let [current-file-id (:current-file-id state)
page (wsh/lookup-page state)
shape (ctn/get-shape page shape-id)
undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(update-component shape-id)
(sync-file current-file-id file-id :components (:component-id shape))
(when (not= current-file-id file-id)
(sync-file file-id file-id :components (:component-id shape)))
(dwu/commit-undo-transaction undo-id))))))
([shape-id file-id] (update-component-sync shape-id file-id nil))
([shape-id file-id undo-group]
(ptk/reify ::update-component-sync
ptk/WatchEvent
(watch [_ state _]
(let [current-file-id (:current-file-id state)
page (wsh/lookup-page state)
shape (ctn/get-shape page shape-id)
undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(update-component shape-id undo-group)
(sync-file current-file-id file-id :components (:component-id shape) undo-group)
(when (not= current-file-id file-id)
(sync-file file-id file-id :components (:component-id shape) undo-group))
(dwu/commit-undo-transaction undo-id)))))))
(defn update-component-in-bulk
[shapes file-id]
@ -666,7 +669,7 @@
(let [undo-id (js/Symbol)]
(rx/concat
(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)))))))
(declare sync-file-2nd-stage)
@ -683,6 +686,8 @@
([file-id library-id]
(sync-file file-id library-id nil nil))
([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 library-id)
(us/assert (s/nilable #{:colors :components :typographies}) asset-type)
@ -702,7 +707,8 @@
:file (dwlh/pretty-file file-id state)
:library (dwlh/pretty-file library-id state)
:asset-type asset-type
:asset-id asset-id)
:asset-id asset-id
:undo-group undo-group)
(let [file (wsh/get-file state file-id)
sync-components? (or (nil? asset-type) (= asset-type :components))
@ -711,7 +717,8 @@
library-changes (reduce
pcb/concat-changes
(pcb/empty-changes it)
(-> (pcb/empty-changes it)
(pcb/set-undo-group undo-group))
[(when sync-components?
(dwlh/generate-sync-library it file-id :components asset-id library-id state))
(when sync-colors?
@ -720,7 +727,8 @@
(dwlh/generate-sync-library it file-id :typographies asset-id library-id state))])
file-changes (reduce
pcb/concat-changes
(pcb/empty-changes it)
(-> (pcb/empty-changes it)
(pcb/set-undo-group undo-group))
[(when sync-components?
(dwlh/generate-sync-file it file-id :components asset-id library-id state))
(when sync-colors?
@ -751,7 +759,7 @@
:library-id library-id})))
(when (and (seq (:redo-changes library-changes))
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
"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
;; 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 library-id asset-id]
[file-id library-id asset-id undo-group]
(us/assert ::us/uuid file-id)
(us/assert ::us/uuid library-id)
(us/assert (s/nilable ::us/uuid) asset-id)
@ -775,7 +783,8 @@
(let [file (wsh/get-file state file-id)
changes (reduce
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-library it file-id :components asset-id library-id state)])]
@ -849,7 +858,7 @@
check-changes
(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))
#{}
changes)]
@ -857,7 +866,7 @@
(log/info :msg "DETECTED COMPONENTS CHANGED"
:ids (map str components-changed))
(run! st/emit!
(map #(update-component-sync % (:id data))
(map #(update-component-sync % (:id data) undo-group)
components-changed)))))]
(when components-v2

View file

@ -548,7 +548,7 @@
(defn duplicate-selected
([move-delta?]
(duplicate-selected move-delta? false))
([move-delta? add-group-id?]
([move-delta? add-undo-group?]
(ptk/reify ::duplicate-selected
ptk/WatchEvent
(watch [it state _]
@ -567,7 +567,7 @@
changes (->> (prepare-duplicate-changes objects page selected delta it libraries)
(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)

View file

@ -67,11 +67,11 @@
(add-undo-entry state entry))))
(defn- accumulate-undo-entry
[state {:keys [undo-changes redo-changes group-id]}]
[state {:keys [undo-changes redo-changes undo-group]}]
(-> state
(update-in [:workspace-undo :transaction :undo-changes] #(into undo-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
[entry stack?]
@ -79,29 +79,31 @@
(ptk/reify ::append-undo
ptk/UpdateEvent
(update [_ state]
(cond
(and (get-in state [:workspace-undo :transaction])
(or (not stack?)
(d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes]))
(d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes]))))
(accumulate-undo-entry state entry)
(cond
(and (get-in state [:workspace-undo :transaction])
(or (not stack?)
(d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes]))
(d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes]))))
(accumulate-undo-entry state entry)
stack?
(stack-undo-entry state entry)
stack?
(stack-undo-entry state entry)
:else
(add-undo-entry state entry)))))
:else
(add-undo-entry state entry)))))
(def empty-tx
{: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/UpdateEvent
(update [_ state]
;; We commit the old transaction before starting the new one
(let [current-tx (get-in state [:workspace-undo :transaction])
pending-tx (get-in state [:workspace-undo :transactions-pending])]
(let [current-tx (get-in state [:workspace-undo :transaction])
pending-tx (get-in state [:workspace-undo :transactions-pending])]
(cond-> state
(nil? current-tx) (assoc-in [:workspace-undo :transaction] empty-tx)
(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
(:require
[app.common.types.components-list :as ctkl]
[app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]