diff --git a/frontend/deps.edn b/frontend/deps.edn index f728d1ecd..41f154c6f 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -1,7 +1,7 @@ -{:deps {org.clojure/clojurescript {:mvn/version "1.10.516"} +{:deps {org.clojure/clojurescript {:mvn/version "1.10.520"} org.clojure/clojure {:mvn/version "1.10.1"} - funcool/promesa {:mvn/version "2.0.0"} - com.cognitect/transit-cljs {:mvn/version "0.8.239"} + funcool/promesa {:mvn/version "2.0.1"} + com.cognitect/transit-cljs {:mvn/version "0.8.256"} funcool/rumext {:git/url "https://github.com/funcool/rumext.git", :sha "3d598e749ba429eae6544e532fc9f81369b307f5"} @@ -12,20 +12,25 @@ environ/environ {:mvn/version "1.1.0"} - funcool/beicon {:mvn/version "3.2.0"} - funcool/bide {:mvn/version "1.6.0"} + metosin/reitit-core {:mvn/version "0.3.9"} + metosin/reitit-frontend {:mvn/version "0.3.9"} + + funcool/beicon {:mvn/version "5.0.0"} + funcool/bide {:mvn/version "1.6.1-SNAPSHOT"} funcool/cuerdas {:mvn/version "2.2.0"} funcool/lentes {:mvn/version "1.2.0"} - funcool/potok {:mvn/version "2.1.0"} + funcool/potok {:mvn/version "2.3.0"} } - :paths ["src" "vendor"] + :paths ["src" "vendor" "resources"] :aliases {:dev {:extra-deps {com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"} com.bhauman/rebel-readline {:mvn/version "0.1.4"} com.bhauman/figwheel-main {:mvn/version "0.2.1-SNAPSHOT"} - org.clojure/tools.namespace {:mvn/version "0.2.11"}} + org.clojure/tools.namespace {:mvn/version "0.3.0"}} :extra-paths ["test"]} :repl {:main-opts ["-m" "rebel-readline.main"]} + :ancient {:main-opts ["-m" "deps-ancient.deps-ancient"] + :extra-deps {deps-ancient {:mvn/version "RELEASE"}}} }} diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs index 01d2623b8..6b6533806 100644 --- a/frontend/src/uxbox/main.cljs +++ b/frontend/src/uxbox/main.cljs @@ -2,14 +2,28 @@ ;; 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 +;; Copyright (c) 2015-2019 Andrey Antukh -(ns uxbox.main - (:require [uxbox.main.store :as st] - [uxbox.main.ui :as ui] - [uxbox.main.locales.en :as en] - [uxbox.main.locales.fr :as fr] - [uxbox.util.i18n :as i18n])) +(ns ^:figwheel-hooks uxbox.main + (:require + [rumext.core :as mx :include-macros true] + [uxbox.main.data.auth :refer [logout]] + [uxbox.main.locales.en :as en] + [uxbox.main.locales.fr :as fr] + [uxbox.main.store :as st] + [uxbox.main.ui :refer [app]] + [uxbox.main.ui.lightbox :refer [lightbox]] + [uxbox.main.ui.loader :refer [loader]] + [uxbox.util.dom :as dom] + [uxbox.util.html-history :as html-history] + [uxbox.util.i18n :as i18n :refer [tr]] + [uxbox.util.messages :as uum] + [uxbox.util.router :as rt] + [uxbox.util.timers :as ts])) + +;; --- i18n + +(declare reinit) (i18n/update-locales! (fn [locales] (-> locales @@ -19,10 +33,101 @@ (i18n/on-locale-change! (fn [new old] (println "Locale changed from" old " to " new) - (ui/reinit))) + (reinit))) + +;; --- Error Handling + +(defn- on-error + "A default error handler." + [{:keys [status] :as error}] + (js/console.error "on-error:" (pr-str error)) + (js/console.error (.-stack error)) + (reset! st/loader false) + (cond + ;; Unauthorized or Auth timeout + (and (:status error) + (or (= (:status error) 403) + (= (:status error) 419))) + (ts/schedule 100 #(st/emit! (logout))) + + ;; Conflict + (= status 412) + (ts/schedule 100 #(st/emit! (uum/error (tr "errors.conflict")))) + + ;; Network error + (= (:status error) 0) + (ts/schedule 100 #(st/emit! (uum/error (tr "errors.network")))) + + ;; Something else + :else + (ts/schedule 100 #(st/emit! (uum/error (tr "errors.generic")))))) + +(set! st/*on-error* on-error) + +(def routes + [["/auth" + ["/login" :auth/login] + ["/register" :auth/register] + ["/recovery/request" :auth/recovery-request] + ["/recovery/token/:token" :auth/recovery]] + ["/settings" + ["/profile" :settings/profile] + ["/password" :settings/password] + ["/notifications" :settings/notifications]] + ["/dashboard" + ["/projects" :dashboard/projects] + ["/elements" :dashboard/elements] + ["/icons" :dashboard/icons] + ["/images" :dashboard/images] + ["/colors" :dashboard/colors]] + ["/workspace/:project/:page" :workspace/page]]) + +(defn- on-navigate + [router path] + (let [match (rt/match router path)] + (prn "on-navigate" path match) + (cond + (and (= path "") (nil? match)) + (html-history/set-path! "/dashboard/projects") + + (nil? match) + (prn "TODO 404") + + :else + (st/emit! #(assoc % :route match))))) + +(defn init-ui + [] + (let [router (rt/init routes) + cpath (deref html-history/path)] + + (st/emit! #(assoc % :router router)) + (add-watch html-history/path ::main #(on-navigate router %4)) + + (mx/mount (app) (dom/get-element "app")) + (mx/mount (lightbox) (dom/get-element "lightbox")) + (mx/mount (loader) (dom/get-element "loader")) + + (on-navigate router cpath))) (defn ^:export init [] (st/init) - (ui/init-routes) - (ui/init)) + (init-ui)) + +(defn reinit + [] + (remove-watch html-history/path ::main) + (.unmountComponentAtNode js/ReactDOM (dom/get-element "app")) + (.unmountComponentAtNode js/ReactDOM (dom/get-element "lightbox")) + (.unmountComponentAtNode js/ReactDOM (dom/get-element "loader")) + (init-ui)) + +(defn ^:after-load after-load + [] + (reinit)) + + + + + diff --git a/frontend/src/uxbox/main/data/auth.cljs b/frontend/src/uxbox/main/data/auth.cljs index 8131f7891..63a87ef28 100644 --- a/frontend/src/uxbox/main/data/auth.cljs +++ b/frontend/src/uxbox/main/data/auth.cljs @@ -51,7 +51,7 @@ (defrecord Login [username password] ptk/UpdateEvent (update [_ state] - (merge state (dissoc (initial-state) :route))) + (merge state (dissoc initial-state :route :router))) ptk/WatchEvent (watch [this state s] @@ -59,7 +59,7 @@ :password password :scope "webapp"} on-error #(rx/of (uum/error (tr "errors.auth.unauthorized")))] - (->> (rp/req :fetch/token params) + (->> (rp/req :auth/login params) (rx/map :payload) (rx/map logged-in) (rx/catch rp/client-error? on-error))))) @@ -78,11 +78,12 @@ ptk/UpdateEvent (update [_ state] (swap! storage dissoc :auth) - (merge state (dissoc (initial-state) :route))) + (merge state (dissoc initial-state :route :router))) ptk/WatchEvent (watch [_ state s] - (rx/of (rt/navigate :auth/login)))) + (->> (rp/req :auth/logout) + (rx/map (constantly (rt/nav :auth/login)))))) (defn logout [] diff --git a/frontend/src/uxbox/main/data/images.cljs b/frontend/src/uxbox/main/data/images.cljs index 8503ef482..ddd9a5251 100644 --- a/frontend/src/uxbox/main/data/images.cljs +++ b/frontend/src/uxbox/main/data/images.cljs @@ -12,6 +12,7 @@ [uxbox.main.store :as st] [uxbox.main.repo :as rp] [uxbox.util.i18n :refer [tr]] + [uxbox.util.router :as rt] [uxbox.util.data :refer (jscoll->vec)] [uxbox.util.uuid :as uuid] [uxbox.util.time :as ts] @@ -80,21 +81,6 @@ [type id] (Initialize. type id)) -;; --- Select a Collection - -(defrecord SelectCollection [type id] - ptk/WatchEvent - (watch [_ state stream] - (rx/of (r/navigate :dashboard/images - {:type type :id id})))) - -(defn select-collection - ([type] - (select-collection type nil)) - ([type id] - {:pre [(keyword? type)]} - (SelectCollection. type id))) - ;; --- Color Collections Fetched (defrecord CollectionsFetched [items] @@ -135,7 +121,7 @@ ptk/WatchEvent (watch [_ state stream] - (rx/of (select-collection :own (:id item))))) + (rx/of (rt/nav :dashboard/images nil {:type :own :id (:id item)})))) (defn collection-created [item] @@ -213,7 +199,7 @@ (watch [_ state s] (let [type (get-in state [:dashboard :images :type])] (->> (rp/req :delete/image-collection id) - (rx/map #(select-collection type)))))) + (rx/map #(rt/nav :dashboard/images nil {:type type})))))) (defn delete-collection [id] diff --git a/frontend/src/uxbox/main/exports.cljs b/frontend/src/uxbox/main/exports.cljs index 7b3bdecdb..6ff65121f 100644 --- a/frontend/src/uxbox/main/exports.cljs +++ b/frontend/src/uxbox/main/exports.cljs @@ -70,4 +70,5 @@ (when (:shapes page) (dom/render-to-html (page-svg page)))) (catch :default e + (js/console.log e) nil))) diff --git a/frontend/src/uxbox/main/repo/auth.cljs b/frontend/src/uxbox/main/repo/auth.cljs index accba3bfb..2f0e77567 100644 --- a/frontend/src/uxbox/main/repo/auth.cljs +++ b/frontend/src/uxbox/main/repo/auth.cljs @@ -15,7 +15,7 @@ (let [url (str url "/profile/me")] (send! {:method :get :url url}))) -(defmethod request :fetch/token +(defmethod request :auth/login [type data] (let [url (str url "/auth/login")] (send! {:url url @@ -23,6 +23,12 @@ :auth false :body data}))) + +(defmethod request :auth/logout + [type data] + (let [url (str url "/auth/logout")] + (send! {:url url :method :post :auth false}))) + (defmethod request :update/profile [type data] (let [params {:url (str url "/profile/me") diff --git a/frontend/src/uxbox/main/repo/projects.cljs b/frontend/src/uxbox/main/repo/projects.cljs index 1353c5214..5c7c2f06d 100644 --- a/frontend/src/uxbox/main/repo/projects.cljs +++ b/frontend/src/uxbox/main/repo/projects.cljs @@ -21,7 +21,7 @@ (defmethod request :fetch/project-by-token [_ token] - (send! {:url (str url "/projects-by-token/" token) + (send! {:url (str url "/projects/by-token/" token) :method :get})) (defmethod request :create/project diff --git a/frontend/src/uxbox/main/store.cljs b/frontend/src/uxbox/main/store.cljs index 62d080c05..26a88c262 100644 --- a/frontend/src/uxbox/main/store.cljs +++ b/frontend/src/uxbox/main/store.cljs @@ -10,7 +10,6 @@ [potok.core :as ptk] [uxbox.builtins.colors :as colors] [uxbox.util.storage :refer [storage]])) - (enable-console-print!) (def ^:dynamic *on-error* identity) @@ -30,13 +29,13 @@ ([event & events] (apply ptk/emit! store (cons event events)))) -(defn initial-state - [] +(def initial-state {:dashboard {:project-order :name :project-filter "" :images-order :name :images-filter ""} :route nil + :router nil :auth (:auth storage nil) :clipboard #queue [] :undo {} @@ -53,6 +52,7 @@ (defn init "Initialize the state materialization." - [] - (emit! initial-state) - (rx/to-atom store state)) + ([] (init {})) + ([props] + (emit! #(merge % initial-state props)) + (rx/to-atom store state))) diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs index ab7423d8a..51e3149cc 100644 --- a/frontend/src/uxbox/main/ui.cljs +++ b/frontend/src/uxbox/main/ui.cljs @@ -5,11 +5,10 @@ ;; Copyright (c) 2015-2017 Andrey Antukh ;; Copyright (c) 2015-2017 Juan de la Cruz -(ns ^:figwheel-hooks uxbox.main.ui +(ns uxbox.main.ui (:require [beicon.core :as rx] [lentes.core :as l] [cuerdas.core :as str] - [bide.core :as bc] [potok.core :as ptk] [uxbox.builtins.icons :as i] [uxbox.main.store :as st] @@ -24,6 +23,7 @@ [uxbox.main.ui.workspace :refer [workspace]] [uxbox.main.ui.shapes] [uxbox.util.messages :as uum] + [uxbox.util.html-history :as html-history] [uxbox.util.router :as rt] [uxbox.util.timers :as ts] [uxbox.util.i18n :refer [tr]] @@ -33,48 +33,10 @@ ;; --- Constants -(def +unrestricted+ - #{:auth/login - :auth/register - :auth/recovery-request - :auth/recovery}) - -(def restricted? - (complement +unrestricted+)) - (def route-ref (-> (l/key :route) (l/derive st/state))) -;; --- Error Handling - -(defn- on-error - "A default error handler." - [{:keys [status] :as error}] - (js/console.error "on-error:" (pr-str error)) - (js/console.error (.-stack error)) - (reset! st/loader false) - (cond - ;; Unauthorized or Auth timeout - (and (:status error) - (or (= (:status error) 403) - (= (:status error) 419))) - (ts/schedule 100 #(st/emit! (logout))) - - ;; Conflict - (= status 412) - (ts/schedule 100 #(st/emit! (uum/error (tr "errors.conflict")))) - - ;; Network error - (= (:status error) 0) - (ts/schedule 100 #(st/emit! (uum/error (tr "errors.network")))) - - ;; Something else - :else - (ts/schedule 100 #(st/emit! (uum/error (tr "errors.generic")))))) - -(set! st/*on-error* on-error) - ;; --- Main App (Component) (defn app-will-mount @@ -88,95 +50,45 @@ :mixins [mx/reactive]} [] (let [route (mx/react route-ref) - auth (mx/react st/auth-ref) - location (:id route) - params (:params route)] - (if (and (restricted? location) (not auth)) - (do (ts/schedule 0 #(st/emit! (rt/navigate :auth/login))) nil) - (case location - :auth/login (auth/login-page) - :auth/register (auth/register-page) - :auth/recovery-request (auth/recovery-request-page) - :auth/recovery (auth/recovery-page (:token params)) - :dashboard/projects (dashboard/projects-page) - ;; :dashboard/elements (dashboard/elements-page) - :dashboard/icons (let [{:keys [id type]} params - type (when (str/alpha? type) (keyword type)) - id (cond - (str/digits? id) (parse-int id) - (uuid-str? id) (uuid id) - :else nil)] - (dashboard/icons-page type id)) + auth (mx/react st/auth-ref)] + (prn "main$app" route) + (case (get-in route [:data :name]) + :auth/login (auth/login-page) + :auth/register (auth/register-page) + :auth/recovery-request (auth/recovery-request-page) + :auth/recovery (let [token (get-in route [:params :path :token])] + (auth/recovery-page token)) + :dashboard/projects (dashboard/projects-page) + ;; ;; :dashboard/elements (dashboard/elements-page) - :dashboard/images (let [{:keys [id type]} params - type (when (str/alpha? type) (keyword type)) - id (cond - (str/digits? id) (parse-int id) - (uuid-str? id) (uuid id) - :else nil)] - (dashboard/images-page type id)) + :dashboard/icons (let [{:keys [id type]} (get-in route [:params :query]) + id (cond + (str/digits? id) (parse-int id) + (uuid-str? id) (uuid id) + :else nil) + type (when (str/alpha? type) (keyword type))] + (dashboard/icons-page type id)) - :dashboard/colors (let [{:keys [id type]} params - type (when (str/alpha? type) (keyword type)) - id (cond - (str/digits? id) (parse-int id) - (uuid-str? id) (uuid id) - :else nil)] - (dashboard/colors-page type id)) - :settings/profile (settings/profile-page) - :settings/password (settings/password-page) - :settings/notifications (settings/notifications-page) - :workspace/page (let [projectid (uuid (:project params)) - pageid (uuid (:page params))] - (workspace projectid pageid)) - nil - )))) + :dashboard/images (let [{:keys [id type]} (get-in route [:params :query]) + id (cond + (str/digits? id) (parse-int id) + (uuid-str? id) (uuid id) + :else nil) + type (when (str/alpha? type) (keyword type))] + (dashboard/images-page type id)) -;; --- Routes - -(def routes - [["/auth/login" :auth/login] - ["/auth/register" :auth/register] - ["/auth/recovery/request" :auth/recovery-request] - ["/auth/recovery/token/:token" :auth/recovery] - ["/settings/profile" :settings/profile] - ["/settings/password" :settings/password] - ["/settings/notifications" :settings/notifications] - ["/dashboard/projects" :dashboard/projects] - ["/dashboard/elements" :dashboard/elements] - - ["/dashboard/icons" :dashboard/icons] - ["/dashboard/icons/:type/:id" :dashboard/icons] - ["/dashboard/icons/:type" :dashboard/icons] - - ["/dashboard/images" :dashboard/images] - ["/dashboard/images/:type/:id" :dashboard/images] - ["/dashboard/images/:type" :dashboard/images] - - ["/dashboard/colors" :dashboard/colors] - ["/dashboard/colors/:type/:id" :dashboard/colors] - ["/dashboard/colors/:type" :dashboard/colors] - - ["/workspace/:project/:page" :workspace/page]]) - -;; --- Main Entry Point - -(defn init-routes - [] - (rt/init st/store routes {:default :auth/login})) - -(defn ^:export init - [] - (mx/mount (app) (dom/get-element "app")) - (mx/mount (lightbox) (dom/get-element "lightbox")) - (mx/mount (loader) (dom/get-element "loader"))) - -(defn reinit - [] - (.unmountComponentAtNode js/ReactDOM (dom/get-element "app")) - (.unmountComponentAtNode js/ReactDOM (dom/get-element "lightbox")) - (.unmountComponentAtNode js/ReactDOM (dom/get-element "loader")) - (init)) - -(defn ^:after-load my-after-reload-callback [] - (reinit)) + :dashboard/colors (let [{:keys [id type]} (get-in route [:params :query]) + type (when (str/alpha? type) (keyword type)) + id (cond + (str/digits? id) (parse-int id) + (uuid-str? id) (uuid id) + :else nil)] + (dashboard/colors-page type id)) + :settings/profile (settings/profile-page) + :settings/password (settings/password-page) + :settings/notifications (settings/notifications-page) + :workspace/page (let [projectid (uuid (get-in route [:params :path :project])) + pageid (uuid (get-in route [:params :path :page]))] + (workspace projectid pageid)) + nil + ))) diff --git a/frontend/src/uxbox/main/ui/dashboard/header.cljs b/frontend/src/uxbox/main/ui/dashboard/header.cljs index f348424d1..a7bb84cd0 100644 --- a/frontend/src/uxbox/main/ui/dashboard/header.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/header.cljs @@ -13,7 +13,7 @@ [uxbox.builtins.icons :as i] [uxbox.main.ui.users :as ui.u] [uxbox.util.i18n :refer (tr)] - [uxbox.util.router :as r] + [uxbox.util.router :as rt] [rumext.core :as mx :include-macros true])) (def header-ref @@ -22,8 +22,8 @@ (mx/defc header-link [section content] - (let [link (r/route-for section)] - [:a {:href (str "/#" link)} content])) + (let [on-click #(st/emit! (rt/navigate section))] + [:a {:on-click on-click} content])) (mx/defc header {:mixins [mx/static mx/reactive]} diff --git a/frontend/src/uxbox/main/ui/dashboard/icons.cljs b/frontend/src/uxbox/main/ui/dashboard/icons.cljs index 301c50e45..ad72c6853 100644 --- a/frontend/src/uxbox/main/ui/dashboard/icons.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/icons.cljs @@ -16,6 +16,7 @@ [uxbox.main.ui.lightbox :as lbx] [uxbox.main.ui.dashboard.header :refer (header)] [uxbox.main.ui.keyboard :as kbd] + [uxbox.util.router :as rt] [uxbox.util.i18n :as t :refer (tr)] [uxbox.util.data :refer (read-string)] [rumext.core :as mx :include-macros true] @@ -135,7 +136,7 @@ local (:rum/local own)] (letfn [(on-click [event] (let [type (or type :own)] - (st/emit! (di/select-collection type id)))) + (st/emit! (rt/navigate :dashboard/icons {} {:type type :id id})))) (on-input-change [event] (let [value (dom/get-target event) value (dom/get-value value)] @@ -199,8 +200,8 @@ (let [colls (->> (map second colls) (filter #(= :builtin (:type %))) (sort-by :name))] - (st/emit! (di/select-collection type (:id (first colls))))) - (st/emit! (di/select-collection type))))] + (st/emit! (rt/navigate :dashboard/icons {} {:type type :id (first colls)}))) + (st/emit! (rt/navigate :dashboard/icons {} {:type type}))))] [:div.library-bar {} [:div.library-bar-inside {} [:ul.library-tabs {} diff --git a/frontend/src/uxbox/main/ui/dashboard/images.cljs b/frontend/src/uxbox/main/ui/dashboard/images.cljs index 474fe10ed..94d88d934 100644 --- a/frontend/src/uxbox/main/ui/dashboard/images.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/images.cljs @@ -17,6 +17,7 @@ [uxbox.main.ui.lightbox :as lbx] [uxbox.main.ui.keyboard :as kbd] [uxbox.main.ui.dashboard.header :refer [header]] + [uxbox.util.router :as rt] [uxbox.util.time :as dt] [uxbox.util.data :refer [read-string jscoll->vec]] [uxbox.util.dom :as dom])) @@ -130,7 +131,7 @@ [{:keys [rum/local] :as own} {:keys [id type name num-images] :as coll} selected?] (letfn [(on-click [event] (let [type (or type :own)] - (st/emit! (di/select-collection type id)))) + (st/emit! (rt/nav :dashboard/images {} {:type type :id id})))) (on-input-change [event] (let [value (dom/get-target event) value (dom/get-value value)] @@ -192,14 +193,14 @@ builtin? (= type :builtin)] (letfn [(select-tab [type] (if own? - (st/emit! (di/select-collection type)) + (st/emit! (rt/nav :dashboard/images nil {:type type})) (let [coll (->> (map second colls) (filter #(= type (:type %))) (sort-by :name) (first))] (if coll - (st/emit! (di/select-collection type (:id coll))) - (st/emit! (di/select-collection type))))))] + (st/emit! (rt/nav :dashboard/images nil {:type type :id (:id coll)})) + (st/emit! (rt/nav :dashboard/images nil {:type type}))))))] [:div.library-bar {} [:div.library-bar-inside {} [:ul.library-tabs {} diff --git a/frontend/src/uxbox/main/ui/workspace/download.cljs b/frontend/src/uxbox/main/ui/workspace/download.cljs index f65d25c73..022744dc3 100644 --- a/frontend/src/uxbox/main/ui/workspace/download.cljs +++ b/frontend/src/uxbox/main/ui/workspace/download.cljs @@ -123,7 +123,7 @@ [:p.info {} "Download a single page of your project in SVG."] [:select.input-select {:ref "page" :default-value (pr-str current)} (for [{:keys [id name]} pages] - [:option {:value (pr-str id)} name])] + [:option {:value (pr-str id) :key (pr-str id)} name])] [:a.btn-primary {:href "#" :on-click download-page} "Download page"]] [:div.content-col {} [:span.icon {} i/folder-zip] diff --git a/frontend/src/uxbox/main/ui/workspace/header.cljs b/frontend/src/uxbox/main/ui/workspace/header.cljs index b76d09f87..06e7ea232 100644 --- a/frontend/src/uxbox/main/ui/workspace/header.cljs +++ b/frontend/src/uxbox/main/ui/workspace/header.cljs @@ -48,7 +48,7 @@ pages (deref refs/selected-project-pages) index (index-of pages page) rval (rand-int 1000000) - url (str cfg/viewurl "?v=" rval "#/" token "/" index)] + url (str cfg/viewurl "?v=" rval "#/preview/" token "/" index)] (st/emit! (udp/persist-page (:id page) #(js/open url "new tab" ""))))) (mx/defc header diff --git a/frontend/src/uxbox/util/dom.cljs b/frontend/src/uxbox/util/dom.cljs index c4540a1ec..26c20159a 100644 --- a/frontend/src/uxbox/util/dom.cljs +++ b/frontend/src/uxbox/util/dom.cljs @@ -27,7 +27,7 @@ (defn render-to-html [component] - (.renderToStatciMarkup js/ReactDOMServer component)) + (.renderToStaticMarkup js/ReactDOMServer component)) (defn get-element-by-class ([classname] diff --git a/frontend/src/uxbox/util/html_history.cljs b/frontend/src/uxbox/util/html_history.cljs new file mode 100644 index 000000000..760810e89 --- /dev/null +++ b/frontend/src/uxbox/util/html_history.cljs @@ -0,0 +1,29 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; 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-2017 Andrey Antukh + +(ns uxbox.util.html-history + "A singleton abstraction for the html5 fragment based history." + (:require [goog.events :as e]) + (:import bide.impl.TokenTransformer + goog.history.Html5History + goog.history.EventType)) + +(defonce +instance+ + (doto (Html5History. nil (TokenTransformer.)) + (.setUseFragment true) + (.setEnabled true))) + +(defonce path (atom (.getToken +instance+))) + +(e/listen +instance+ EventType.NAVIGATE #(reset! path (.-token %))) + +(defn set-path! + [path] + (.setToken +instance+ path)) + +(defn replace-path! + [path] + (.replaceToken +instance+ path)) diff --git a/frontend/src/uxbox/util/router.cljs b/frontend/src/uxbox/util/router.cljs index 696b87065..26f109b0b 100644 --- a/frontend/src/uxbox/util/router.cljs +++ b/frontend/src/uxbox/util/router.cljs @@ -2,65 +2,104 @@ ;; 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-2017 Andrey Antukh +;; Copyright (c) 2015-2019 Andrey Antukh (ns uxbox.util.router - (:require [bide.core :as r] - [beicon.core :as rx] - [potok.core :as ptk])) + (:require [reitit.core :as r] + [cuerdas.core :as str] + [potok.core :as ptk] + [uxbox.util.html-history :as html-history]) + (:import goog.Uri + goog.Uri.QueryData)) (defonce +router+ nil) -;; --- Update Location (Event) +;; --- API -(deftype UpdateLocation [id params] - ptk/UpdateEvent - (update [_ state] - (let [route (merge {:id id} - (when params - {:params params}))] - (assoc state :route route)))) +(defn- parse-query-data + [^QueryData qdata] + (persistent! + (reduce (fn [acc key] + (let [values (.getValues qdata key) + rkey (str/keyword key)] + (cond + (> (alength values) 1) + (assoc! acc rkey (into [] values)) -(defn update-location? - [v] - (instance? UpdateLocation v)) + (= (alength values) 1) + (assoc! acc rkey (aget values 0)) -(defn update-location - [name params] - (UpdateLocation. name params)) + :else + acc))) + (transient {}) + (.getKeys qdata)))) -;; --- Navigate (Event) - -(deftype Navigate [id params] - ptk/EffectEvent - (effect [_ state stream] - (r/navigate! +router+ id params))) - -(defn navigate - ([id] (navigate id nil)) - ([id params] - {:pre [(keyword? id)]} - (Navigate. id params))) - -;; --- Public Api +(defn- resolve-url + ([router id] (resolve-url router id {} {})) + ([router id params] (resolve-url router id params {})) + ([router id params qparams] + (when-let [match (r/match-by-name router id params)] + (if (empty? qparams) + (r/match->path match) + (let [uri (.parse goog.Uri (r/match->path match)) + qdt (.createFromMap QueryData (clj->js qparams))] + (.setQueryData uri qdt) + (.toString uri)))))) (defn init - [store routes {:keys [default] :or {default :auth/login}}] - (let [opts {:on-navigate #(ptk/emit! store (update-location %1 %2)) - :default default} - router (-> (r/router routes) - (r/start! opts))] - (set! +router+ router) - router)) + [routes] + (r/router routes)) + +(defn query-params + "Given goog.Uri, read query parameters into Clojure map." + [^goog.Uri uri] + (let [q (.getQueryData uri)] + (->> q + (.getKeys) + (map (juxt keyword #(.get q %))) + (into {})))) + +(defn navigate! + ([router id] (navigate! router id {} {})) + ([router id params] (navigate! router id params {})) + ([router id params qparams] + (-> (resolve-url router id params qparams) + (html-history/set-path!)))) + +(defn match + "Given routing tree and current path, return match with possibly + coerced parameters. Return nil if no match found." + [router path] + (let [uri (.parse Uri path)] + (when-let [match (r/match-by-path router (.getPath uri))] + (let [qparams (parse-query-data (.getQueryData uri)) + params {:path (:path-params match) :query qparams}] + (assoc match + :params params + :query-params qparams))))) (defn route-for "Given a location handler and optional parameter map, return the URI for such handler and parameters." - ([id] - (if +router+ - (r/resolve +router+ id) - "")) + ([id] (route-for id {})) ([id params] - (if +router+ - (r/resolve +router+ id params) - ""))) + (str (some-> +router+ (resolve-url id params))))) + +;; --- Navigate (Event) + +(deftype Navigate [id params qparams] + ptk/EffectEvent + (effect [_ state stream] + (let [router (:router state)] + (prn "Navigate:" id params qparams "| Match:" (resolve-url router id params qparams)) + (navigate! router id params qparams)))) + +(defn nav + ([id] (navigate id nil nil)) + ([id params] (navigate id params nil)) + ([id params qparams] + {:pre [(keyword? id)]} + (Navigate. id params qparams))) + +(def navigate nav) + diff --git a/frontend/src/uxbox/view.cljs b/frontend/src/uxbox/view.cljs index 04e242216..e451d1b55 100644 --- a/frontend/src/uxbox/view.cljs +++ b/frontend/src/uxbox/view.cljs @@ -4,26 +4,101 @@ ;; ;; Copyright (c) 2016-2017 Andrey Antukh -(ns uxbox.view - (:require [uxbox.config] - [uxbox.view.store :as st] - [uxbox.view.ui :as ui] - [uxbox.main.locales.en :as en] - [uxbox.main.locales.fr :as fr] - [uxbox.util.i18n :as i18n])) +(ns ^:figwheel-hooks uxbox.view + (:require + [rumext.core :as mx :include-macros true] + [uxbox.config] + [uxbox.util.dom :as dom] + [uxbox.util.html-history :as html-history] + [uxbox.util.i18n :as i18n :refer [tr]] + [uxbox.util.messages :as uum] + [uxbox.util.router :as rt] + [uxbox.view.locales.en :as en] + [uxbox.view.locales.fr :as fr] + [uxbox.view.store :as st] + [uxbox.view.ui :refer [app]] + [uxbox.view.ui.lightbox :refer [lightbox]] + [uxbox.view.ui.loader :refer [loader]])) (i18n/update-locales! (fn [locales] (-> locales (assoc :en en/locales) (assoc :fr fr/locales)))) +(declare reinit) + (i18n/on-locale-change! (fn [new old] (println "Locale changed from" old " to " new) - (ui/init))) + (reinit))) + +(defn- on-error + "A default error handler." + [error] + (cond + ;; Network error + (= (:status error) 0) + (do + (st/emit! (uum/error (tr "errors.network"))) + (js/console.error "Stack:" (.-stack error))) + + ;; Something else + :else + (do + (st/emit! (uum/error (tr "errors.generic"))) + (js/console.error "Stack:" (.-stack error))))) + +(set! st/*on-error* on-error) + +;; --- Routes + +(def routes + [["/preview/:token/:index" :view/viewer] + ["/not-found" :view/notfound]]) + +(defn- on-navigate + [router path] + (let [match (rt/match router path)] + (prn "on-navigate" path match) + (cond + (and (= path "") (nil? match)) + (html-history/set-path! "/not-found") + + (nil? match) + (prn "TODO 404") + + :else + (st/emit! #(assoc % :route match))))) + +(defn init-ui + [] + (let [router (rt/init routes) + cpath (deref html-history/path)] + + (st/emit! #(assoc % :router router)) + (add-watch html-history/path ::view #(on-navigate router %4)) + + (mx/mount (app) (dom/get-element "app")) + (mx/mount (lightbox) (dom/get-element "lightbox")) + (mx/mount (loader) (dom/get-element "loader")) + + (on-navigate router cpath))) (defn ^:export init [] (st/init) - (ui/init-routes) - (ui/init)) + (init-ui)) + +(defn reinit + [] + (remove-watch html-history/path ::view) + (.unmountComponentAtNode js/ReactDOM (dom/get-element "app")) + (.unmountComponentAtNode js/ReactDOM (dom/get-element "lightbox")) + (.unmountComponentAtNode js/ReactDOM (dom/get-element "loader")) + (init-ui)) + +(defn ^:after-load after-load + [] + (reinit)) + + diff --git a/frontend/src/uxbox/view/data/viewer.cljs b/frontend/src/uxbox/view/data/viewer.cljs index 61a514b63..7b4798da9 100644 --- a/frontend/src/uxbox/view/data/viewer.cljs +++ b/frontend/src/uxbox/view/data/viewer.cljs @@ -76,8 +76,8 @@ (defrecord SelectPage [index] ptk/WatchEvent (watch [_ state stream] - (let [token (get-in state [:route :params :token])] - (rx/of (rt/navigate :view/viewer {:token token :index index :id nil}))))) + (let [token (get-in state [:route :params :path :token])] + (rx/of (rt/nav :view/viewer {:token token :index index}))))) (defn select-page [index] diff --git a/frontend/src/uxbox/view/locales/fr.cljs b/frontend/src/uxbox/view/locales/fr.cljs index 5a9dc75a4..f381b4d30 100644 --- a/frontend/src/uxbox/view/locales/fr.cljs +++ b/frontend/src/uxbox/view/locales/fr.cljs @@ -5,7 +5,7 @@ ;; Copyright (c) 2015-2016 Andrey Antukh ;; Copyright (c) 2015-2016 Juan de la Cruz -(ns uxbox.view.locales.en) +(ns uxbox.view.locales.fr) (defonce locales {"viewer.sitemap" "plan du site" diff --git a/frontend/src/uxbox/view/store.cljs b/frontend/src/uxbox/view/store.cljs index 081849cbe..a639f537f 100644 --- a/frontend/src/uxbox/view/store.cljs +++ b/frontend/src/uxbox/view/store.cljs @@ -26,8 +26,7 @@ ([event & events] (apply ptk/emit! store (cons event events)))) -(defn- initial-state - [] +(def initial-state {:route nil :project nil :pages nil diff --git a/frontend/src/uxbox/view/ui.cljs b/frontend/src/uxbox/view/ui.cljs index 12f24091b..d5ed0f3cf 100644 --- a/frontend/src/uxbox/view/ui.cljs +++ b/frontend/src/uxbox/view/ui.cljs @@ -20,56 +20,20 @@ [rumext.core :as mx :include-macros true] [uxbox.util.dom :as dom])) - (def route-ref (-> (l/key :route) (l/derive st/state))) -(defn- on-error - "A default error handler." - [error] - (cond - ;; Network error - (= (:status error) 0) - (do - (st/emit! (uum/error (tr "errors.network"))) - (js/console.error "Stack:" (.-stack error))) - - ;; Something else - :else - (do - (st/emit! (uum/error (tr "errors.generic"))) - (js/console.error "Stack:" (.-stack error))))) - -(set! st/*on-error* on-error) - ;; --- Main App (Component) (mx/defc app {:mixins [mx/static mx/reactive]} [] - (let [{loc :id params :params} (mx/react route-ref)] - (case loc + (let [route (mx/react route-ref)] + (prn "view$app" route) + (case (get-in route [:data :name]) :view/notfound (notfound-page) - :view/viewer (let [{:keys [index token]} params] + :view/viewer (let [{:keys [index token]} (get-in route [:params :path])] (viewer-page token (parse-int index 0))) nil))) -;; --- Routes - -(def routes - [["/:token/:index" :view/viewer] - ["/:token" :view/viewer] - ["/not-found" :view/notfound]]) - -;; --- Main Entry Point - -(defn init-routes - [] - (rt/init st/store routes {:default :view/notfound})) - -(defn init - [] - (mx/mount (app) (dom/get-element "app")) - (mx/mount (lightbox) (dom/get-element "lightbox")) - (mx/mount (loader) (dom/get-element "loader"))) diff --git a/frontend/tools.clj b/frontend/tools.clj index 02a07a2ef..9163b643c 100644 --- a/frontend/tools.clj +++ b/frontend/tools.clj @@ -16,15 +16,15 @@ (def demo? (boolean (:uxbox-demo env nil))) (def closure-defines - {"uxbox.config.url" (:uxbox-api-url env "http://127.0.0.1:6060/api") - "uxbox.config.viewurl" (:uxbox-view-url env "/view/") + {"uxbox.config.url" (:uxbox-api-url env "http://localhost:6060/api") + "uxbox.config.viewurl" (:uxbox-view-url env "/view/index.html") "uxbox.config.isdemo" demo?}) (def default-build-options {:cache-analysis true :parallel-build true :language-in :ecmascript6 - :language-out :ecmascript5 + :language-out :ecmascript6 :closure-defines closure-defines :optimizations :none :verbose false @@ -76,6 +76,7 @@ [args] (figwheel/start {:open-url false + :load-warninged-code true :auto-testing false :css-dirs ["resources/public/css" "resources/public/view/css"]