diff --git a/src/uxbox/data/pages.cljs b/src/uxbox/data/pages.cljs index dd368e393..e46f359c7 100644 --- a/src/uxbox/data/pages.cljs +++ b/src/uxbox/data/pages.cljs @@ -21,6 +21,10 @@ [uxbox.util.datetime :as dt] [uxbox.util.data :refer (without-keys)])) +(defprotocol IPageUpdate + "A marker protocol for mark events that alters the + page and is subject to perform a backend synchronization.") + ;; --- Pages Fetched (defrecord PagesFetched [pages] @@ -82,20 +86,12 @@ (sc/validate! +create-page-schema+ data) (map->CreatePage data)) -;; --- Update Page - -(defrecord UpdatePage [id name width height layout] - rs/UpdateEvent - (-apply-update [_ state] - (letfn [(updater [page] - (merge page - (when width {:width width}) - (when height {:height height}) - (when name {:name name})))] - (update-in state [:pages-by-id id] updater))) +;; --- Sync Page +(defrecord SyncPage [id] rs/WatchEvent (-apply-watch [this state s] + (println "SyncPage") (letfn [(on-success [{page :payload}] (rx/of #(assoc-in % [:pages-by-id id :version] (:version page)) @@ -108,31 +104,38 @@ (rx/mapcat on-success) (rx/catch on-failure)))))) -(def ^:const +update-page-schema+ - {:name [sc/required sc/string] - :width [sc/required sc/integer] - :height [sc/required sc/integer] - :layout [sc/required sc/string]}) +(defn sync-page + [id] + (SyncPage. id)) + +;; --- Update Page + +(declare fetch-page-history) +(declare fetch-pinned-page-history) + +(defrecord UpdatePage [id] + rs/WatchEvent + (-apply-watch [this state s] + (println "UpdatePage") + (let [page (get-in state [:pages-by-id id])] + (if (:history page) + (rx/empty) + (rx/of (sync-page id) + (fetch-page-history id) + (fetch-pinned-page-history id)))))) (defn update-page - [data] - (sc/validate! +update-page-schema+ data) - (map->UpdatePage data)) + [id] + (UpdatePage. id)) (defn watch-page-changes [id] - (letfn [(on-page-change [buffer] - (let [page (second buffer)] - (rs/emit! (update-page page))))] - (let [lens (l/getter #(stpr/pack-page % id))] - (as-> (l/focus-atom lens st/state) $ - (rx/from-atom $) - (rx/debounce 1000 $) - (rx/scan (fn [acc page] - (if (>= (:version page) (:version acc)) page acc)) $) - (rx/dedupe #(dissoc % :version) $) - (rx/buffer 2 1 $) - (rx/subscribe $ on-page-change #(throw %)))))) + (letfn [(on-value [] + (rs/emit! (update-page id)))] + (as-> rs/stream $ + (rx/filter #(satisfies? IPageUpdate %) $) + (rx/debounce 2000 $) + (rx/on-next $ on-value)))) ;; --- Update Page Metadata @@ -161,6 +164,12 @@ (rx/map on-success) (rx/catch on-failure))))) +(def ^:const +update-page-schema+ + {:name [sc/required sc/string] + :width [sc/required sc/integer] + :height [sc/required sc/integer] + :layout [sc/required sc/string]}) + (defn update-page-metadata [data] (sc/validate! +update-page-schema+ data) @@ -269,8 +278,7 @@ (let [page (get-in state [:pages-by-id id]) page' (assoc page :history true - :data (:data history) - :version (:version history))] + :data (:data history))] (-> state (stpr/unpack-page page') (assoc-in [:workspace :history :selected] (:id history))))))) @@ -278,3 +286,35 @@ (defn select-page-history [id history] (SelectPageHistory. id history)) + +;; --- Apply selected history + +(defrecord ApplySelectedHistory [id] + rs/UpdateEvent + (-apply-update [_ state] + (println "ApplySelectedHistory" id) + (-> state + (update-in [:pages-by-id id] dissoc :history) + (assoc-in [:workspace :history :selected] nil))) + + rs/WatchEvent + (-apply-watch [_ state s] + (rx/of (update-page id)))) + +(defn apply-selected-history + [id] + (ApplySelectedHistory. id)) + +;; --- Discard Selected History + +(defrecord DiscardSelectedHistory [id] + rs/UpdateEvent + (-apply-update [_ state] + (let [packed (get-in state [:pagedata-by-id id])] + (-> state + (stpr/unpack-page packed) + (assoc-in [:workspace :history :selected] nil))))) + +(defn discard-selected-history + [id] + (DiscardSelectedHistory. id)) diff --git a/src/uxbox/data/shapes.cljs b/src/uxbox/data/shapes.cljs index 1ec5521d0..3da7c1921 100644 --- a/src/uxbox/data/shapes.cljs +++ b/src/uxbox/data/shapes.cljs @@ -16,6 +16,7 @@ [uxbox.schema :as sc] [uxbox.xforms :as xf] [uxbox.shapes :as sh] + [uxbox.data.pages :as udp] [uxbox.util.geom.point :as gpt] [uxbox.util.data :refer (index-of)])) @@ -77,6 +78,7 @@ [shape] (sc/validate! +shape-schema+ shape) (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (let [page (get-in state [:workspace :page])] @@ -86,6 +88,7 @@ "Remove the shape using its id." [id] (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (let [shape (get-in state [:shapes-by-id id])] @@ -96,6 +99,7 @@ [sid delta] {:pre [(gpt/point? delta)]} (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (let [shape (get-in state [:shapes-by-id sid])] @@ -105,6 +109,7 @@ [sid {:keys [x1 y1 x2 y2] :as opts}] (sc/validate! +shape-line-attrs-schema+ opts) (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (let [shape (get-in state [:shapes-by-id sid]) @@ -119,6 +124,7 @@ (>= rotation 0) (>= 360 rotation)]} (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (update-in state [:shapes-by-id sid] @@ -134,6 +140,7 @@ [sid {:keys [width height] :as opts}] (sc/validate! +shape-size-schema+ opts) (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (let [shape (get-in state [:shapes-by-id sid]) @@ -143,6 +150,7 @@ (defn update-vertex-position [id {:keys [vid delta]}] (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (update-in state [:shapes-by-id id] sh/move-vertex vid delta)))) @@ -161,6 +169,7 @@ [sid {:keys [content]}] {:pre [(string? content)]} (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (assoc-in state [:shapes-by-id sid :content] content)))) @@ -169,6 +178,7 @@ [sid {:keys [color opacity] :as opts}] (sc/validate! +shape-fill-attrs-schema+ opts) (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (update-in state [:shapes-by-id sid] @@ -181,6 +191,7 @@ letter-spacing line-height] :as opts}] (sc/validate! +shape-font-attrs-schema+ opts) (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (update-in state [:shapes-by-id sid :font] @@ -197,6 +208,7 @@ [sid {:keys [color opacity type width] :as opts}] (sc/validate! +shape-stroke-attrs-schema+ opts) (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (update-in state [:shapes-by-id sid] @@ -210,6 +222,7 @@ [sid {:keys [rx ry] :as opts}] (sc/validate! +shape-radius-attrs-schema+ opts) (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (update-in state [:shapes-by-id sid] @@ -220,6 +233,7 @@ (defn hide-shape [sid] (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (assoc-in state [:shapes-by-id sid :hidden] true)) @@ -235,6 +249,7 @@ (defn show-shape [sid] (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (assoc-in state [:shapes-by-id sid :hidden] false)) @@ -250,6 +265,7 @@ (defn block-shape [sid] (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (assoc-in state [:shapes-by-id sid :blocked] true)) @@ -265,6 +281,7 @@ (defn unblock-shape [sid] (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (assoc-in state [:shapes-by-id sid :blocked] false)) @@ -280,6 +297,7 @@ (defn lock-shape [sid] (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (assoc-in state [:shapes-by-id sid :locked] true)) @@ -295,6 +313,7 @@ (defn unlock-shape [sid] (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (assoc-in state [:shapes-by-id sid :locked] false)) @@ -314,6 +333,7 @@ {:pre [(not (nil? tid)) (not (nil? sid))]} (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (stsh/drop-shape state sid tid loc)))) diff --git a/src/uxbox/data/workspace.cljs b/src/uxbox/data/workspace.cljs index fe5a7b308..6e9a0494b 100644 --- a/src/uxbox/data/workspace.cljs +++ b/src/uxbox/data/workspace.cljs @@ -16,6 +16,7 @@ [uxbox.schema :as sc] [uxbox.xforms :as xf] [uxbox.shapes :as sh] + [uxbox.data.pages :as udp] [uxbox.data.shapes :as uds] [uxbox.util.geom.point :as gpt] [uxbox.util.data :refer (index-of)])) @@ -123,6 +124,7 @@ (let [groups (into #{} (map :group shapes))] (= 1 (count groups))))] (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (let [shapes-by-id (get state :shapes-by-id) @@ -147,6 +149,7 @@ (defn duplicate-selected [] (reify + udp/IPageUpdate rs/UpdateEvent (-apply-update [_ state] (let [selected (get-in state [:workspace :selected])] diff --git a/src/uxbox/locales/en.cljs b/src/uxbox/locales/en.cljs index 40d705516..ce2f1e234 100644 --- a/src/uxbox/locales/en.cljs +++ b/src/uxbox/locales/en.cljs @@ -37,6 +37,8 @@ "ds.help.circle" "Circle (Ctrl + E)" "ds.help.line" "Line (Ctrl + L)" + "history.alert-message" "You are seeng version %s" + "errors.auth" "Username or passwords seems to be wrong." }) diff --git a/src/uxbox/ui/messages.cljs b/src/uxbox/ui/messages.cljs index eedd0ced7..98a59781a 100644 --- a/src/uxbox/ui/messages.cljs +++ b/src/uxbox/ui/messages.cljs @@ -9,57 +9,138 @@ [uxbox.util.data :refer (classnames)] [uxbox.util.dom :as dom])) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Api -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; --- Constants (defonce +message+ (atom nil)) + (def ^:const +animation-timeout+ 600) +;; --- Helpers + (defn set-timeout! [ms callback] (js/setTimeout callback ms)) (defn abort-timeout! [v] - (js/clearTimeout v)) + (when v + (js/clearTimeout v))) + +;; --- Public Api + +(defn- clean-prev-msgstate! + [message] + (let [type (namespace (:type message))] + (case type + "notification" + (do + (abort-timeout! (:tsem-main message)) + (abort-timeout! (:tsem message))) + + "dialog" + (abort-timeout! (:tsem message))))) (defn error ([message] (error message nil)) - ([message {:keys [timeout] :or {timeout 30000}}] - (when-let [prev-message @+message+] - (abort-timeout! (:timeout-total prev-message)) - (abort-timeout! (:timeout prev-message))) - - (let [timeout-total (set-timeout! (+ timeout +animation-timeout+) - #(reset! +message+ nil)) - timeout (set-timeout! timeout #(swap! +message+ assoc :state :hide))] - (reset! +message+ {:type :error + ([message {:keys [timeout] :or {timeout 6000}}] + (when-let [prev @+message+] + (clean-prev-msgstate! prev)) + (let [timeout' (+ timeout +animation-timeout+) + tsem-main (set-timeout! timeout' #(reset! +message+ nil)) + tsem (set-timeout! timeout #(swap! +message+ assoc :state :hide))] + (reset! +message+ {:type :notification/error :state :normal - :timeout-total timeout-total - :timeout timeout + :tsem-main tsem-main + :tsem tsem :content message})))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Component -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn dialog + [& {:keys [message on-accept on-cancel] + :or {on-cancel (constantly nil)} + :as opts}] + {:pre [(ifn? on-accept) + (string? message)]} + (when-let [prev @+message+] + (clean-prev-msgstate! prev)) + (reset! +message+ {:type :dialog/simple + :state :normal + :content message + :on-accept on-accept + :on-cancel on-cancel})) +(defn close + [] + (when @+message+ + (let [timeout +animation-timeout+ + tsem (set-timeout! timeout #(reset! +message+ nil))] + (swap! +message+ assoc + :state :hide + :tsem tsem)))) -(defn messages-render - [own] - (when-let [message (rum/react +message+)] - (let [classes (classnames :error (= (:type message) :error) - :info (= (:type message) :info) - :hide-message (= (:state message) :hide) - :quick true)] +;; --- Notification Component + +(defn notification-render + [own message] + (let [msgtype (name (:type message)) + classes (classnames :error (= msgtype "error") + :info (= msgtype "info") + :hide-message (= (:state message) :hide) + :quick true)] + (html + [:div.message {:class classes} + [:div.message-body + [:span.close i/close] + [:span (:content message)]]]))) + +(def ^:private notification-box + (mx/component + {:render notification-render + :name "notification" + :mixins [mx/static]})) + +;; --- Dialog Component + +(defn dialog-render + [own {:keys [on-accept on-cancel] :as message}] + (let [classes (classnames :info true + :hide-message (= (:state message) :hide)) + local (:rum/local own)] + (letfn [(accept [event] + (dom/prevent-default event) + (close) + (on-accept)) + (cancel [event] + (dom/prevent-default event) + (close) + (when on-cancel + (on-cancel)))] (html [:div.message {:class classes} [:div.message-body [:span.close i/close] [:span (:content message)] [:div.message-action - [:a.btn-transparent.btn-small "Accept"] - [:a.btn-transparent.btn-small "Cancel"] - ]]])))) + [:a.btn-transparent.btn-small + {:on-click accept} + "Accept"] + [:a.btn-transparent.btn-small + {:on-click cancel} + "Cancel"]]]])))) + +(def ^:private dialog-box + (mx/component + {:render dialog-render + :name "dialog" + :mixins [mx/static]})) + +;; --- Main Component (entry point) + +(defn messages-render + [own] + (when-let [message (rum/react +message+)] + (case (namespace (:type message)) + "notification" (notification-box message) + "dialog" (dialog-box message) + (throw (ex-info "Invalid message type" message))))) (def ^:const messages (mx/component diff --git a/src/uxbox/ui/workspace.cljs b/src/uxbox/ui/workspace.cljs index fbf46b510..92d60ca30 100644 --- a/src/uxbox/ui/workspace.cljs +++ b/src/uxbox/ui/workspace.cljs @@ -16,6 +16,7 @@ [uxbox.ui.core :as uuc] [uxbox.ui.icons :as i] [uxbox.ui.mixins :as mx] + [uxbox.ui.messages :as uum] [uxbox.ui.workspace.base :as uuwb] [uxbox.ui.workspace.shortcuts :as wshortcuts] [uxbox.ui.workspace.header :refer (header)] @@ -33,7 +34,10 @@ (let [[projectid pageid] (:rum/props own)] (rs/emit! (dw/initialize projectid pageid) (dp/fetch-projects) - (udp/fetch-pages projectid)) + (udp/fetch-pages projectid) + (udp/fetch-page-history pageid) + (udp/fetch-pinned-page-history pageid)) + own)) (defn- workspace-did-mount @@ -119,6 +123,8 @@ [:div (header) (colorpalette) + (uum/messages) + [:main.main-content [:section.workspace-content {:class classes :on-scroll on-scroll} diff --git a/src/uxbox/ui/workspace/sidebar/history.cljs b/src/uxbox/ui/workspace/sidebar/history.cljs index 0c6d96746..2c7090a42 100644 --- a/src/uxbox/ui/workspace/sidebar/history.cljs +++ b/src/uxbox/ui/workspace/sidebar/history.cljs @@ -15,13 +15,14 @@ [uxbox.state :as st] [uxbox.shapes :as shapes] [uxbox.library :as library] - [uxbox.util.datetime :as dt] - [uxbox.util.data :refer (read-string)] [uxbox.data.workspace :as dw] - [uxbox.data.pages :as dpg] + [uxbox.data.pages :as udp] [uxbox.ui.workspace.base :as wb] + [uxbox.ui.messages :as msg] [uxbox.ui.icons :as i] [uxbox.ui.mixins :as mx] + [uxbox.util.datetime :as dt] + [uxbox.util.data :refer (read-string)] [uxbox.util.dom :as dom])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -38,7 +39,7 @@ (defn history-list-render [own page history] - (let [select #(rs/emit! (dpg/select-page-history (:id page) %)) + (let [select #(rs/emit! (udp/select-page-history (:id page) %)) min-version (apply min (map :version (:items history))) show-more? (pos? min-version)] (html @@ -58,9 +59,24 @@ [:li [:a.btn-primary.btn-small "view more"]])]))) +(defn history-list-will-update + [own] + (let [[page history] (:rum/props own)] + (if (:selected history) + (let [selected (->> (:items history) + (filter #(= (:selected history) (:id %))) + (first))] + (msg/dialog + :message (tr "history.alert-message" (:version selected)) + :on-accept #(rs/emit! (udp/apply-selected-history (:id page))) + :on-cancel #(rs/emit! (udp/discard-selected-history (:id page))))) + (msg/close)) + own)) + (def history-list (mx/component {:render history-list-render + :will-update history-list-will-update :name "history-list" :mixins [mx/static]})) @@ -82,27 +98,6 @@ :name "history-pinned-list" :mixins [mx/static]})) - -(defn- history-toolbox-will-mount - [own] - (let [page @wb/page-l] - (rs/emit! (dpg/fetch-page-history (:id page)) - (dpg/fetch-pinned-page-history (:id page))) - (add-watch wb/page-l ::key - (fn [_ _ ov nv] - (when (or (and (> (:version nv) (:version ov)) - (not (:history nv))) - (not= (:id ov) (:id nv))) - (rs/emit! (dpg/fetch-page-history (:id nv)) - (dpg/fetch-pinned-page-history (:id nv)))))) - own)) - -(defn- history-toolbox-will-unmount - [own] - (rs/emit! (dpg/clean-page-history)) - (remove-watch wb/page-l ::key) - own) - (defn history-toolbox-render [own] (let [local (:rum/local own) @@ -136,6 +131,4 @@ (mx/component {:render history-toolbox-render :name "document-history-toolbox" - :will-mount history-toolbox-will-mount - :will-unmount history-toolbox-will-unmount :mixins [mx/static rum/reactive (mx/local)]}))