0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-09 13:31:23 -05:00

Undo transactions to batch changes

This commit is contained in:
alonso.torres 2020-09-15 17:47:09 +02:00 committed by Andrey Antukh
parent a70e4eeb01
commit 0b5ae3632e
13 changed files with 198 additions and 106 deletions

View file

@ -27,7 +27,7 @@
overflow: auto;
margin: 0.5rem;
&.selected {
&.transaction {
border: 2px solid $color-primary;
}
}

View file

@ -166,14 +166,14 @@
(assoc-in [:workspace-local :picked-shift?] shift?)))))
(defn change-fill-selected [color id file-id]
(ptk/reify ::change-fill-selected
(defn change-fill [ids color id file-id]
(ptk/reify ::change-fill
ptk/WatchEvent
(watch [_ state s]
(let [selected (get-in state [:workspace-local :selected])
objects (get-in state [:workspace-data :pages-index (:current-page-id state) :objects])
children (mapcat #(cph/get-children % objects) selected)
ids (into selected children)
(let [pid (:current-page-id state)
objects (get-in state [:workspace-data :pages-index pid :objects])
children (mapcat #(cph/get-children % objects) ids)
ids (into ids children)
is-text? #(= :text (:type (get objects %)))
text-ids (filter is-text? ids)
@ -183,19 +183,22 @@
:fill-color-ref-id id
:fill-color-ref-file file-id))
editor (get-in state [:workspace-local :editor])
converted-attrs {:fill color}]
converted-attrs {:fill color}
reduce-fn (fn [state id]
(update-in state [:workspace-data :pages-index pid :objects id] update-fn))]
(rx/from (conj
(map #(dwt/update-text-attrs {:id % :editor editor :attrs converted-attrs}) text-ids)
(dwc/update-shapes shape-ids update-fn)))))))
(defn change-stroke-selected [color id file-id]
(ptk/reify ::change-stroke-selected
(defn change-stroke [ids color id file-id]
(ptk/reify ::change-stroke
ptk/WatchEvent
(watch [_ state s]
(let [selected (get-in state [:workspace-local :selected])
objects (get-in state [:workspace-data :pages-index (:current-page-id state) :objects])
children (mapcat #(cph/get-children % objects) selected)
ids (into selected children)
(let [objects (get-in state [:workspace-data :pages-index (:current-page-id state) :objects])
children (mapcat #(cph/get-children % objects) ids)
ids (into ids children)
update-fn (fn [s]
(cond-> s
@ -211,12 +214,14 @@
(rx/of (dwc/update-shapes ids update-fn))))))
(defn picker-for-selected-shape []
;; TODO: replace st/emit! by a subject push and set that in the WatchEvent
(let [handle-change-color (fn [color _ shift?]
(st/emit!
(if shift?
(change-stroke-selected color nil nil)
(change-fill-selected color nil nil))
(md/hide-modal)))]
(let [ids (get-in @st/state [:workspace-local :selected])]
(st/emit!
(if shift?
(change-stroke ids color nil nil)
(change-fill ids color nil nil))
(md/hide-modal))))]
(ptk/reify ::start-picker
ptk/UpdateEvent
(update [_ state]
@ -224,5 +229,5 @@
(assoc-in [:workspace-local :picking-color?] true)
(assoc ::md/modal {:id (random-uuid)
:type :colorpicker
:props {:on-change handle-change-color}
:props {:on-close handle-change-color}
:allow-click-outside true}))))))

View file

@ -1456,6 +1456,7 @@
:option :background
:value color}]
[{:type :set-option
:page-id page-id
:option :background
:value ccolor}]
{:commit-local? true}))))))

View file

@ -265,14 +265,49 @@
(update :workspace-undo dissoc :undo-index)
(update-in [:workspace-undo :items] (fn [queue] (into [] (take (inc index) queue))))))))
(defn- add-undo-entry [state entry]
(if entry
(let [state (update-in state [:workspace-undo :items] (fnil conj-undo-entry []) entry)]
(assoc-in state [:workspace-undo :index] (dec (count (get-in state [:workspace-undo :items])))))
state))
(defn- accumulate-undo-entry [state {:keys [undo-changes redo-changes]}]
(-> state
(update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %))
(update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes))))
(defn- append-undo
[entry]
(us/verify ::undo-entry entry)
(ptk/reify ::append-undo
ptk/UpdateEvent
(update [_ state]
(let [state (update-in state [:workspace-undo :items] (fnil conj-undo-entry []) entry)]
(assoc-in state [:workspace-undo :index] (dec (count (get-in state [:workspace-undo :items]))))))))
(if (get-in state [:workspace-undo :transaction])
(accumulate-undo-entry state entry)
(add-undo-entry state entry)))))
(def start-undo-transaction
(ptk/reify ::start-undo-transaction
ptk/UpdateEvent
(update [_ state]
;; We commit the old transaction before starting the new one
(-> state
(add-undo-entry (get-in state [:workspace-undo :transaction]))
(assoc-in [:workspace-undo :transaction] {:undo-changes []
:redo-changes []})))))
(def discard-undo-transaction
(ptk/reify ::discard-undo-transaction
ptk/UpdateEvent
(update [_ state]
(update state :workspace-undo dissoc :transaction))))
(def commit-undo-transaction
(ptk/reify ::commit-undo-transaction
ptk/UpdateEvent
(update [_ state]
(-> state
(add-undo-entry (get-in state [:workspace-undo :transaction]))
(update :workspace-undo dissoc :transaction)))))
(def undo
(ptk/reify ::undo

View file

@ -427,5 +427,7 @@
uchanges (conj (dwc/generate-changes page-id objects2 objects0) regchg)
]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dwc/rehash-shape-frame-relationship ids))))))
(rx/of dwc/start-undo-transaction
(dwc/commit-changes rchanges uchanges {:commit-local? true})
(dwc/rehash-shape-frame-relationship ids)
dwc/commit-undo-transaction)))))

View file

@ -369,30 +369,41 @@
(t locale "workspace.libraries.colors.save-color")]])])
)
(defn calculate-position
"Calculates the style properties for the given coordinates and position"
[position x y]
(cond
(or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"}
(= position :left) {:left (str (- x 270) "px") :top (str (- y 50) "px")}
:else {:left (str (+ x 24) "px") :top (str (- y 50) "px")}))
(mf/defc colorpicker-modal
{::mf/register modal/components
::mf/register-as :colorpicker}
[{:keys [x y default value opacity page on-change disable-opacity position on-accept] :as props}]
(let [position (or position :left)
style (cond
(or (nil? x) (nil? y))
{:left "auto"
:right "16rem"
:top "4rem"}
[{:keys [x y default value opacity page on-change on-close disable-opacity position on-accept] :as props}]
(let [dirty? (mf/use-var false)
last-change (mf/use-var nil)
position (or position :left)
style (calculate-position position x y)
(= position :left)
{:left (str (- x 270) "px")
:top (str (- y 50) "px")}
handle-change (fn [new-value new-opacity op1 op2]
(when (or (not= new-value value) (not= new-opacity opacity))
(reset! dirty? true))
(reset! last-change [new-value new-opacity op1 op2])
(on-change new-value new-opacity op1 op2))]
(mf/use-effect
(fn []
#(when (and @dirty? on-close)
(when-let [[value opacity op1 op2] @last-change]
(on-close value opacity op1 op2)))))
:else
{:left (str (+ x 24) "px")
:top (str (- y 50) "px")})
]
[:div.colorpicker-tooltip
{:style (clj->js style)}
[:& colorpicker {:value (or value default)
:opacity (or opacity 1)
:on-change on-change
:on-change handle-change
:on-accept on-accept
:disable-opacity disable-opacity}]]))

View file

@ -15,6 +15,7 @@
[rumext.alpha :as mf]
[app.common.data :as d]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.texts :as dwt]
[app.main.refs :as refs]
[app.main.store :as st]
@ -273,9 +274,12 @@
(fn []
(let [lkey1 (events/listen js/document EventType.CLICK on-click)
lkey2 (events/listen js/document EventType.KEYUP on-key-up)]
(st/emit! (dwt/assign-editor id editor))
(st/emit! (dwt/assign-editor id editor)
dwc/start-undo-transaction)
#(do
(st/emit! (dwt/assign-editor id nil))
(st/emit! (dwt/assign-editor id nil)
dwc/commit-undo-transaction)
(events/unlistenByKey lkey1)
(events/unlistenByKey lkey2))))

View file

@ -136,12 +136,12 @@
:top nil
:left nil
:editing rename?})
click-color
(fn [event]
(if (kbd/shift? event)
(st/emit! (dc/change-stroke-selected (:value color) id (if local? nil file-id)))
(st/emit! (dc/change-fill-selected (:value color) id (if local? nil file-id)))))
(let [ids (get-in @st/state [:workspace-local :selected])]
(if (kbd/shift? event)
(st/emit! (dc/change-stroke ids (:value color) id (if local? nil file-id)))
(st/emit! (dc/change-fill ids (:value color) id (if local? nil file-id))))))
rename-color
(fn [name]

View file

@ -27,24 +27,40 @@
(def workspace-undo
(l/derived :workspace-undo st/state))
(mf/defc undo-entry [{:keys [index entry objects is-transaction?] :or {is-transaction? false}}]
(let [{:keys [redo-changes]} entry]
[:li.undo-entry {:class (when is-transaction? "transaction")}
(for [[idx-change {:keys [type id operations]}] (map-indexed vector redo-changes)]
[:div.undo-entry-change
[:div.undo-entry-change-data (when type (str type)) " " (when id (str (get-in objects [id :name] (subs (str id) 0 8))))]
(when operations
[:div.undo-entry-change-data (str/join ", " (map (comp name :attr) operations))])])]))
(mf/defc history-toolbox []
(let [locale (mf/deref i18n/locale)
{:keys [items index]} (mf/deref workspace-undo)
{:keys [items index transaction]} (mf/deref workspace-undo)
objects (mf/deref refs/workspace-page-objects)]
[:div.history-toolbox
[:div.history-toolbox-title "History"]
(when (> (count items) 0)
[:ul.undo-history
[:*
(when (or (nil? index) (>= index (count items))) [:hr.separator])
(for [[idx-entry {:keys [redo-changes]}] (->> items (map-indexed vector) reverse)]
[:*
(when (= index idx-entry) [:hr.separator {:data-index index}])
[:li.undo-entry {:key (str "entry-" idx-entry)}
(for [[idx-change {:keys [type id operations]}] (map-indexed vector redo-changes)]
[:div.undo-entry-change
[:div.undo-entry-change-data (when type (str type)) " " (when id (str (get-in objects [id :name] (subs (str id) 0 8))))]
(when operations
[:div.undo-entry-change-data (str/join ", " (map (comp name :attr) operations))])])]])
(when (= index -1) [:hr.separator])]])]))
[:ul.undo-history
[:*
(when (and
(> (count items) 0)
(or (nil? index)
(>= index (count items))))
[:hr.separator])
(when transaction
[:& undo-entry {:key (str "transaction")
:objects objects
:is-transaction? true
:entry transaction}])
(for [[idx-entry entry] (->> items (map-indexed vector) reverse)]
[:*
(when (= index idx-entry) [:hr.separator {:data-index index}])
[:& undo-entry {:key (str "entry-" idx-entry)
:objects objects
:entry entry}]])
(when (= index -1) [:hr.separator])]]]))

View file

@ -13,6 +13,7 @@
[app.common.pages :as cp]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.texts :as dwt]
[app.main.data.colors :as dc]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as i]
@ -37,14 +38,11 @@
(= (:fill-opacity new-values)
(:fill-opacity old-values)))))
(mf/defc fill-menu
{::mf/wrap [#(mf/memo' % fill-menu-props-equals?)]}
[{:keys [ids type values editor] :as props}]
(let [locale (mf/deref i18n/locale)
shapes (deref (refs/objects-by-id ids))
is-text? #(= (:type %) :text)
text-ids (map :id (filter is-text? shapes))
other-ids (map :id (filter (comp not is-text?) shapes))
show? (not (nil? (:fill-color values)))
label (case type
@ -61,51 +59,32 @@
(mf/use-callback
(mf/deps ids)
(fn [event]
(when-not (empty? other-ids)
(st/emit! (dwc/update-shapes other-ids #(assoc % :fill-color cp/default-color))))
(when-not (empty? text-ids)
(run! #(st/emit! (dwt/update-text-attrs
{:id %
:editor editor
:attrs {:fill cp/default-color}}))
text-ids))))
(st/emit! (dc/change-fill ids cp/default-color nil nil))))
on-delete
(mf/use-callback
(mf/deps ids)
(fn [event]
(when-not (empty? other-ids)
(st/emit! (dwc/update-shapes other-ids #(dissoc % :fill-color))))
(when-not (empty? text-ids)
(run! #(st/emit! (dwt/update-text-attrs
{:id %
:editor editor
:attrs {:fill nil}}))
text-ids))))
(st/emit! (dc/change-fill ids nil nil nil))))
on-change
(mf/use-callback
(mf/deps ids)
(fn [value opacity id file-id]
(let [change #(cond-> %
value (assoc :fill-color value
:fill-color-ref-id id
:fill-color-ref-file file-id)
opacity (assoc :fill-opacity opacity))
converted-attrs (cond-> {}
value (assoc :fill value)
opacity (assoc :opacity opacity))]
(st/emit! (dc/change-fill ids value id file-id))))
on-open-picker
(mf/use-callback
(mf/deps ids)
(fn [value opacity id file-id]
(st/emit! dwc/start-undo-transaction)))
on-close-picker
(mf/use-callback
(mf/deps ids)
(fn [value opacity id file-id]
(st/emit! dwc/commit-undo-transaction)))]
(when-not (empty? other-ids)
(st/emit! (dwc/update-shapes ids change)))
(when-not (empty? text-ids)
(run! #(st/emit! (dwt/update-text-attrs
{:id %
:editor editor
:attrs converted-attrs}))
text-ids)))))]
(if show?
[:div.element-set
[:div.element-set-title
@ -113,10 +92,14 @@
[:div.add-page {:on-click on-delete} i/minus]]
[:div.element-set-content
[:& color-row {:color color :on-change on-change}]]]
[:& color-row {:color color
:on-change on-change
:on-open on-open-picker
:on-close on-close-picker}]]]
[:div.element-set
[:div.element-set-title
[:span label]
[:div.add-page {:on-click on-add} i/close]]])))

View file

@ -15,6 +15,7 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwc]
[app.util.i18n :as i18n :refer [t]]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]))
@ -28,12 +29,25 @@
[{:keys [page-id] :as props}]
(let [locale (i18n/use-locale)
options (mf/deref refs/workspace-page-options)
handle-change-color (use-change-color page-id)]
handle-change-color (use-change-color page-id)
on-open
(mf/use-callback
(mf/deps page-id)
#(st/emit! dwc/start-undo-transaction))
on-close
(mf/use-callback
(mf/deps page-id)
#(st/emit! dwc/commit-undo-transaction))]
[:div.element-set
[:div.element-set-title (t locale "workspace.options.canvas-background")]
[:div.element-set-content
[:& color-row {:disable-opacity true
:color {:value (get options :background "#E8E9EA")
:opacity 1}
:on-change handle-change-color}]]]))
:on-change handle-change-color
:on-open on-open
:on-close on-close}]]]))

View file

@ -19,16 +19,18 @@
[app.main.refs :as refs]))
(defn color-picker-callback
[color handle-change-color disable-opacity]
[color handle-change-color handle-open handle-close disable-opacity]
(fn [event]
(let [x (.-clientX event)
y (.-clientY event)
props {:x x
:y y
:on-change handle-change-color
:on-close handle-close
:value (:value color)
:opacity (:opacity color)
:disable-opacity disable-opacity}]
(handle-open)
(modal/show! :colorpicker props))))
(defn value-to-background [value]
@ -57,7 +59,7 @@
(if (= v :multiple) nil v))
(mf/defc color-row
[{:keys [color on-change disable-opacity]}]
[{:keys [color on-change on-open on-close disable-opacity]}]
(let [;;
file-colors (mf/deref refs/workspace-file-colors)
shared-libs (mf/deref refs/workspace-libraries)
@ -91,6 +93,11 @@
(reset! state {:value new-value :opacity new-opacity})
(when on-change (on-change new-value new-opacity id file-id)))
handle-open (fn [] (when on-open (on-open)))
handle-close (fn [value opacity id file-id]
(when on-close (on-close value opacity id file-id)))
handle-value-change (fn [event]
(let [target (dom/get-target event)]
(when (dom/valid? target)
@ -119,7 +126,7 @@
[:span.color-th
{:class (when (:id color) "color-name")
:style {:background-color (-> value value-to-background)}
:on-click (color-picker-callback @state handle-pick-color disable-opacity)}
:on-click (color-picker-callback @state handle-pick-color handle-open handle-close disable-opacity)}
(when (= value :multiple) "?")]
(if (:id color)

View file

@ -119,7 +119,19 @@
on-del-stroke
(fn [event]
(st/emit! (dwc/update-shapes ids #(assoc % :stroke-style :none))))]
(st/emit! (dwc/update-shapes ids #(assoc % :stroke-style :none))))
on-open-picker
(mf/use-callback
(mf/deps ids)
(fn [value opacity id file-id]
(st/emit! dwc/start-undo-transaction)))
on-close-picker
(mf/use-callback
(mf/deps ids)
(fn [value opacity id file-id]
(st/emit! dwc/commit-undo-transaction)))]
(if show-options
[:div.element-set
@ -130,7 +142,9 @@
[:div.element-set-content
;; Stroke Color
[:& color-row {:color current-stroke-color
:on-change handle-change-stroke-color}]
:on-change handle-change-stroke-color
:on-open on-open-picker
:on-close on-close-picker}]
;; Stroke Width, Alignment & Style
[:div.row-flex