mirror of
https://github.com/penpot/penpot.git
synced 2025-01-24 07:29:08 -05: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
|
@ -1,3 +1,3 @@
|
|||
<svg width="500" height="500" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99941 1.62266C8.01413 1.36698 7.74905 1.14313 7.49807 1.21294C7.35967 1.27903 7.02149 1.60601 7.02149 1.60601C7.02149 1.60601 3.82775 4.79865 2.80464 5.76808C2.68664 5.81588 2.6167 5.68421 2.54114 5.62249C1.48872 4.57167 1.68802 4.66986 0.628214 3.62654C0.403393 3.46589 0.0441303 3.61642 0.00630665 3.8924C-0.0278168 4.07137 0.0811424 4.23539 0.212762 4.34466C1.2602 5.45375 1.12484 5.34669 2.1814 6.44725C2.31167 6.56977 2.42611 6.71875 2.58163 6.80902C2.77403 6.89051 2.99255 6.79154 3.11096 6.63235C4.317 5.44398 6.72612 3.05828 7.90933 1.84723C7.96218 1.78479 8.00392 1.70675 7.99941 1.62266Z" fill="black"/>
|
||||
<svg width="500" height="500" viewBox="0 0 8 8" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99941 1.62266C8.01413 1.36698 7.74905 1.14313 7.49807 1.21294C7.35967 1.27903 7.02149 1.60601 7.02149 1.60601C7.02149 1.60601 3.82775 4.79865 2.80464 5.76808C2.68664 5.81588 2.6167 5.68421 2.54114 5.62249C1.48872 4.57167 1.68802 4.66986 0.628214 3.62654C0.403393 3.46589 0.0441303 3.61642 0.00630665 3.8924C-0.0278168 4.07137 0.0811424 4.23539 0.212762 4.34466C1.2602 5.45375 1.12484 5.34669 2.1814 6.44725C2.31167 6.56977 2.42611 6.71875 2.58163 6.80902C2.77403 6.89051 2.99255 6.79154 3.11096 6.63235C4.317 5.44398 6.72612 3.05828 7.90933 1.84723C7.96218 1.78479 8.00392 1.70675 7.99941 1.62266Z" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 731 B After Width: | Height: | Size: 707 B |
File diff suppressed because it is too large
Load diff
|
@ -2,8 +2,10 @@
|
|||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
// Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
// This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
// defined by the Mozilla Public License, v. 2.0.
|
||||
//
|
||||
// Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
.workspace-header {
|
||||
align-items: center;
|
||||
|
@ -177,3 +179,35 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.persistence-status-widget {
|
||||
display: flex;
|
||||
margin-right: 10px;
|
||||
/* border: 1px solid red; */
|
||||
width: 150px;
|
||||
justify-content: flex-end;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
&.error {
|
||||
.label { color: $color-danger; }
|
||||
.icon svg { fill: $color-danger; }
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $color-gray-30;
|
||||
font-size: $fs14;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
fill: $color-gray-30;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
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))]
|
||||
(rx/merge
|
||||
(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 enable-reload-stoper)
|
||||
(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 disable-reload-stoper)
|
||||
(rx/tap on-saved)
|
||||
(rx/ignore)
|
||||
(rx/take-until stoper))))))))
|
||||
(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)
|
||||
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}]
|
||||
(->> (rp/mutation :update-file params)
|
||||
(rx/map (fn [lagged]
|
||||
(if (= #{sid} (into #{} (map :session-id) lagged))
|
||||
(map #(assoc % :changes []) lagged)
|
||||
lagged)))
|
||||
: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 %)))))))))
|
||||
(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,7 +88,7 @@
|
|||
(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
|
||||
|
@ -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
Reference in a new issue