🎉 Add save indicator.

And improve persistence loop error handling.
This commit is contained in:
Andrey Antukh 2020-09-17 17:59:48 +02:00 committed by Alonso Torres
parent 9755516178
commit 1b598e2f6d
8 changed files with 414 additions and 254 deletions

View file

@ -159,7 +159,12 @@
(ptk/reify ::finalize
ptk/UpdateEvent
(update [_ state]
(dissoc state :workspace-file :workspace-project :workspace-media-objects :workspace-users))
(dissoc state
:workspace-file
:workspace-project
:workspace-media-objects
:workspace-users
:workspace-persistence))
ptk/WatchEvent
(watch [_ state stream]
@ -1294,7 +1299,7 @@
(dws/prepare-remove-group page-id group objects)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Interactions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -32,59 +32,120 @@
(declare persist-changes)
(declare shapes-changes-persisted)
(declare update-persistence-status)
;; --- Persistence
(defn initialize-file-persistence
[file-id]
(letfn [(enable-reload-stoper []
(obj/set! js/window "onbeforeunload" (constantly false)))
(disable-reload-stoper []
(obj/set! js/window "onbeforeunload" nil))]
(ptk/reify ::initialize-persistence
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter #(= ::finalize %) stream)
notifier (->> stream
(rx/filter (ptk/type? ::dwc/commit-changes))
(rx/debounce 2000)
(rx/merge stoper))]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::dwc/commit-changes))
(rx/map deref)
(rx/tap enable-reload-stoper)
(rx/buffer-until notifier)
(rx/map vec)
(rx/filter (complement empty?))
(rx/map #(persist-changes file-id %))
(rx/take-until (rx/delay 100 stoper)))
(->> stream
(rx/filter (ptk/type? ::changes-persisted))
(rx/tap disable-reload-stoper)
(rx/ignore)
(rx/take-until stoper))))))))
(ptk/reify ::initialize-persistence
ptk/EffectEvent
(effect [_ state stream]
(let [stoper (rx/filter #(= ::finalize %) stream)
notifier (->> stream
(rx/filter (ptk/type? ::dwc/commit-changes))
(rx/debounce 2000)
(rx/merge stoper))
on-dirty
(fn []
;; Enable reload stoper
(obj/set! js/window "onbeforeunload" (constantly false))
(st/emit! (update-persistence-status {:status :pending})))
on-saving
(fn []
(st/emit! (update-persistence-status {:status :saving})))
on-saved
(fn []
;; Disable reload stoper
(obj/set! js/window "onbeforeunload" nil)
(st/emit! (update-persistence-status {:status :saved})))]
(->> (rx/merge
(->> stream
(rx/filter (ptk/type? ::dwc/commit-changes))
(rx/map deref)
(rx/tap on-dirty)
(rx/buffer-until notifier)
(rx/map vec)
(rx/filter (complement empty?))
(rx/map #(persist-changes file-id %))
(rx/tap on-saving)
(rx/take-until (rx/delay 100 stoper)))
(->> stream
(rx/filter (ptk/type? ::changes-persisted))
(rx/tap on-saved)
(rx/ignore)
(rx/take-until stoper)))
(rx/subs #(st/emit! %)))))))
(defn persist-changes
[file-id changes]
(ptk/reify ::persist-changes
ptk/UpdateEvent
(update [_ state]
(let [conj (fnil conj [])
chng {:id (uuid/next)
:changes changes}]
(update-in state [:workspace-persistence :queue] conj chng)))
ptk/WatchEvent
(watch [_ state stream]
(let [sid (:session-id state)
file (:workspace-file state)]
(when (= (:id file) file-id)
(let [changes (into [] (mapcat identity) changes)
params {:id (:id file)
:revn (:revn file)
:session-id sid
:changes changes}]
(->> (rp/mutation :update-file params)
(rx/map (fn [lagged]
(if (= #{sid} (into #{} (map :session-id) lagged))
(map #(assoc % :changes []) lagged)
lagged)))
(rx/mapcat seq)
(rx/map #(shapes-changes-persisted file-id %)))))))))
file (:workspace-file state)
queue (get-in state [:workspace-persistence :queue] [])
xf-cat (comp (mapcat :changes)
(mapcat identity))
changes (into [] xf-cat queue)
params {:id (:id file)
:revn (:revn file)
:session-id sid
:changes changes}
ids (into #{} (map :id) queue)
update-persistence-queue
(fn [state]
(update-in state [:workspace-persistence :queue]
(fn [items] (into [] (remove #(ids (:id %))) items))))
handle-response
(fn [lagged]
(let [lagged (cond->> lagged
(= #{sid} (into #{} (map :session-id) lagged))
(map #(assoc % :changes [])))]
(rx/concat
(rx/of update-persistence-queue)
(->> (rx/of lagged)
(rx/mapcat seq)
(rx/map #(shapes-changes-persisted file-id %))))))
on-error
(fn [error]
(rx/of (update-persistence-status {:status :error
:reason (:type error)})))]
(when (= file-id (:id file))
(->> (rp/mutation :update-file params)
(rx/mapcat handle-response)
(rx/catch on-error)))))))
(defn update-persistence-status
[{:keys [status reason]}]
(ptk/reify ::update-persistence-status
ptk/UpdateEvent
(update [_ state]
(update state :workspace-persistence
(fn [local]
(assoc local
:reason reason
:status status
:updated-at (dt/now)))))))
(s/def ::shapes-changes-persisted
(s/keys :req-un [::revn ::cp/changes]))

View file

@ -20,11 +20,16 @@
(http/success? response)
(rx/of (:body response))
(http/client-error? response)
(or (http/client-error? response)
(= 500 (:status response)))
(rx/throw (:body response))
(http/server-error? response)
(rx/throw (:body response))
(= 502 (:status response))
(rx/throw {:type :bad-gateway
:body (:body response)})
(= 0 (:status response))
(rx/throw {:type :offline})
:else
(rx/throw {:type :unexpected

View file

@ -158,7 +158,7 @@
(defmethod ptk/handle-error :validation
[error]
(js/console.error (if (map? error) (pr-str error) error))
(js/console.error "handle-error(validation):" (if (map? error) (pr-str error) error))
(when-let [explain (:explain error)]
(println "============ SERVER RESPONSE ERROR ================")
(println explain)
@ -179,7 +179,8 @@
(if (instance? ExceptionInfo error)
(ptk/handle-error (ex-data error))
(do
(js/console.error (if (map? error) (pr-str error) error))
(js/console.error "handle-error(default):"
(if (map? error) (pr-str error) error))
(ts/schedule 100 #(st/emit! (dm/show {:content "Something wrong has happened."
:type :error
:timeout 5000}))))))

View file

@ -9,6 +9,7 @@
(ns app.main.ui.workspace.header
(:require
[okulary.core :as l]
[rumext.alpha :as mf]
[app.main.ui.icons :as i :include-macros true]
[app.config :as cfg]
@ -26,8 +27,37 @@
;; --- Zoom Widget
(def workspace-persistence-ref
(l/derived :workspace-persistence st/state))
(mf/defc persistence-state-widget
{::mf/wrap [mf/memo]}
[{:keys [locale]}]
(let [data (mf/deref workspace-persistence-ref)]
[:div.persistence-status-widget
(cond
(= :pending (:status data))
[:div.pending
[:span.label (t locale "workspace.header.unsaved")]]
(= :saving (:status data))
[:div.saving
[:span.icon i/toggle]
[:span.label (t locale "workspace.header.saving")]]
(= :saved (:status data))
[:div.saved
[:span.icon i/tick]
[:span.label (t locale "workspace.header.saved")]]
(= :error (:status data))
[:div.error {:title "There was an error saving the data. Please refresh if this persists."}
[:span.icon i/msg-warning]
[:span.label (t locale "workspace.header.save-error")]])]))
(mf/defc zoom-widget
{:wrap [mf/memo]}
{::mf/wrap [mf/memo]}
[{:keys [zoom
on-increase
on-decrease
@ -58,25 +88,25 @@
(mf/defc menu
[{:keys [layout project file] :as props}]
(let [show-menu? (mf/use-state false)
locale (i18n/use-locale)
locale (mf/deref i18n/locale)
add-shared-fn #(st/emit! nil (dw/set-file-shared (:id file) true))
on-add-shared
#(modal/show! :confirm-dialog
{:message (t locale "dashboard.grid.add-shared-message" (:name file))
:hint (t locale "dashboard.grid.add-shared-hint")
:accept-text (t locale "dashboard.grid.add-shared-accept")
:not-danger? true
:on-accept add-shared-fn})
{:message (t locale "dashboard.grid.add-shared-message" (:name file))
:hint (t locale "dashboard.grid.add-shared-hint")
:accept-text (t locale "dashboard.grid.add-shared-accept")
:not-danger? true
:on-accept add-shared-fn})
remove-shared-fn #(st/emit! nil (dw/set-file-shared (:id file) false))
on-remove-shared
#(modal/show! :confirm-dialog
{:message (t locale "dashboard.grid.remove-shared-message" (:name file))
:hint (t locale "dashboard.grid.remove-shared-hint")
:accept-text (t locale "dashboard.grid.remove-shared-accept")
:not-danger? false
:on-accept remove-shared-fn})]
{:message (t locale "dashboard.grid.remove-shared-message" (:name file))
:hint (t locale "dashboard.grid.remove-shared-hint")
:accept-text (t locale "dashboard.grid.remove-shared-accept")
:not-danger? false
:on-accept remove-shared-fn})]
[:div.menu-section
[:div.btn-icon-dark.btn-small {:on-click #(reset! show-menu? true)} i/actions]
@ -149,11 +179,10 @@
(mf/defc header
[{:keys [file layout project page-id] :as props}]
(let [locale (i18n/use-locale)
team-id (:team-id project)
(let [team-id (:team-id project)
go-back #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))
zoom (mf/deref refs/selected-zoom)
locale (i18n/use-locale)
locale (mf/deref i18n/locale)
router (mf/deref refs/router)
view-url (rt/resolve router :viewer {:page-id page-id :file-id (:id file)} {:index 0})]
[:header.workspace-header
@ -168,6 +197,9 @@
[:& presence/active-sessions]]
[:div.options-section
[:& persistence-state-widget
{:locale locale}]
[:& zoom-widget
{:zoom zoom
:on-increase #(st/emit! (dw/increase-zoom nil))