mirror of
https://github.com/penpot/penpot.git
synced 2025-06-10 14:52:46 +02:00
🎉 Add save indicator.
And improve persistence loop error handling.
This commit is contained in:
parent
9755516178
commit
1b598e2f6d
8 changed files with 414 additions and 254 deletions
|
@ -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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}))))))
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue