mirror of
https://github.com/penpot/penpot.git
synced 2025-01-23 23:18:48 -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]
|
||||
|
@ -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
Reference in a new issue