├── .gitignore
├── src
└── app
│ ├── client_app.cljs
│ ├── routing.cljs
│ ├── model
│ ├── session.clj
│ └── session.cljs
│ ├── server.clj
│ ├── ui
│ └── dynamic_menu.cljs
│ └── client.cljs
├── package.json
├── resources
└── public
│ └── index.html
├── shadow-cljs.edn
├── dev
└── user.clj
├── deps.edn
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .shadow-cljs
2 | node_modules
3 | resources/public/js
4 | .cpcache
5 | .idea
6 | target
7 | .nrepl-port
8 | /video-series.iml
9 |
--------------------------------------------------------------------------------
/src/app/client_app.cljs:
--------------------------------------------------------------------------------
1 | (ns app.client-app
2 | (:require [com.fulcrologic.fulcro.networking.http-remote :as http]
3 | [com.fulcrologic.fulcro.application :as app]))
4 |
5 | (defonce APP (app/fulcro-app {:remotes {:remote (http/fulcro-http-remote {})}}))
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "1.0.0",
4 | "dependencies": {
5 | "react": "^16.9.0",
6 | "react-dom": "^16.9.0",
7 | "shadow-cljs": "^2.8.52",
8 | "big.js": "^5.2.2",
9 | "react-number-format": "^4.2.0",
10 | "semantic-ui-react": "^0.88.1"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Loading...
8 |
9 |
10 |
--------------------------------------------------------------------------------
/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | {:deps {:aliases [:dev]}
2 | :nrepl {:port 9000}
3 |
4 | :dev-http {8000 "resources/public"}
5 |
6 | :builds {:main {:target :browser
7 | :output-dir "resources/public/js/main"
8 | :asset-path "/js/main"
9 |
10 | :modules {:main {:init-fn app.client/start}}
11 | :devtools {:after-load app.client/refresh
12 | :preloads [com.fulcrologic.fulcro.inspect.preload]}}}}
13 |
--------------------------------------------------------------------------------
/dev/user.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require [org.httpkit.server :as http]
3 | [app.server :refer [middleware]]
4 | [clojure.tools.namespace.repl :as tools-ns]
5 | [taoensso.timbre :as log]))
6 |
7 | ;; make sure not to find things in places like resources
8 | (tools-ns/set-refresh-dirs "src" "dev")
9 |
10 | (defonce server (atom nil))
11 |
12 | (defn start []
13 | (let [result (http/run-server middleware {:port 3000})]
14 | (log/info "Started web server on port 3000")
15 | (reset! server result)
16 | :ok))
17 |
18 | (defn stop []
19 | (when @server
20 | (log/info "Stopped web server")
21 | (@server)))
22 |
23 | (defn restart []
24 | (stop)
25 | (log/info "Reloading code")
26 | (tools-ns/refresh :after 'user/start))
27 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 |
3 | :deps {com.fulcrologic/fulcro {:mvn/version "3.5.26"}
4 | com.fulcrologic/semantic-ui-wrapper {:mvn/version "1.0.0"}
5 | ring/ring-core {:mvn/version "1.9.6"}
6 | kibu/pushy {:mvn/version "0.3.8"}
7 | http-kit/http-kit {:mvn/version "2.3.0"}
8 | com.wsscode/pathom {:mvn/version "2.3.1"}}
9 |
10 | :aliases {:dev {:extra-paths ["dev"]
11 | :extra-deps {org.clojure/clojurescript {:mvn/version "1.10.914"}
12 | org.clojure/tools.namespace {:mvn/version "1.3.0"}
13 | thheller/shadow-cljs {:mvn/version "2.16.11"}
14 | binaryage/devtools {:mvn/version "1.0.6"}}}}}
15 |
--------------------------------------------------------------------------------
/src/app/routing.cljs:
--------------------------------------------------------------------------------
1 | (ns app.routing
2 | (:require
3 | [app.client-app :refer [APP]]
4 | [taoensso.timbre :as log]
5 | [com.fulcrologic.fulcro.routing.dynamic-routing :as dr]
6 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
7 | [clojure.string :as str]
8 | [pushy.core :as pushy]))
9 |
10 | (defonce history (pushy/pushy
11 | (fn [p]
12 | (let [route-segments (vec (rest (str/split p "/")))]
13 | (dr/change-route APP route-segments)))
14 | identity))
15 |
16 | (defn start! []
17 | (pushy/start! history))
18 |
19 | (defn route-to!
20 | "Change routes to the given route-string (e.g. \"/home\"."
21 | [route-string]
22 | (pushy/set-token! history route-string))
23 |
24 | (defmutation route-to
25 | "Mutation to go to a specific route"
26 | [{:keys [route-string]}]
27 | (action [_]
28 | (route-to! route-string)))
29 |
--------------------------------------------------------------------------------
/src/app/model/session.clj:
--------------------------------------------------------------------------------
1 | (ns app.model.session
2 | (:require
3 | [com.wsscode.pathom.connect :as pc]
4 | [com.fulcrologic.fulcro.server.api-middleware :refer [augment-response]]
5 | [taoensso.timbre :as log]))
6 |
7 | (def users (atom {1 {:user/id 1
8 | :user/email "foo@bar.com"
9 | :user/password "letmein"}}))
10 |
11 | (pc/defresolver user-resolver [env {:user/keys [id]}]
12 | {::pc/input #{:user/id}
13 | ::pc/output [:user/email]}
14 | {:user/id id
15 | :user/email (get-in @users [id :user/email])})
16 |
17 | (pc/defresolver current-user-resolver [env _]
18 | {::pc/output [{:session/current-user [:user/id]}]}
19 | (let [{:user/keys [id email]} (log/spy :info (-> env :request :session))]
20 | {:session/current-user
21 | (if id
22 | {:user/email email
23 | :user/id id
24 | :user/valid? true}
25 | {:user/id :nobody
26 | :user/valid? false})}))
27 |
28 | (pc/defmutation login [env {:user/keys [email password]}]
29 | {::pc/params #{:user/email :user/password}
30 | ::pc/output [:user/id :user/valid?]}
31 | (log/info "Login " email)
32 | (Thread/sleep 500)
33 | (let [subject (first (filter (fn [u] (= (:user/email u) email)) (vals @users)))]
34 | (if (and subject (= password (:user/password subject)))
35 | (augment-response
36 | {:user/email email
37 | :user/id (:user/id subject)
38 | :user/valid? true}
39 | (fn [ring-resp] (assoc ring-resp :session subject)))
40 | {:user/valid? false})))
41 |
42 | (pc/defmutation logout [env {:user/keys [email password]}]
43 | {::pc/params #{:user/email :user/password}
44 | ::pc/output [:user/id :user/valid?]}
45 | (augment-response
46 | {:user/id :nobody
47 | :user/valid? false}
48 | (fn [ring-resp] (assoc ring-resp :session {}))))
49 |
50 | (def resolvers [user-resolver current-user-resolver login logout])
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Video Series File
2 |
3 | This repository contains snapshots of the repository as it existed at various interesting points
4 | in the video series. These are represented as tags (see the "Branch" menu at the top of the file list).
5 |
6 | NOTE: I consider these files part of a "performance" (the video recordings). They are *not* meant to be perfect examples of how you should build an application, but instead attempts to help you understand concepts and APIs. Being the product of a performance I *know* there are imperfections, and I do *not* intend to correct them, because that could put things out of sync between the videos and these files. You should *not* attempt to change the versions of any libraries used in these files, nor should you submit PRs to me to correct things like console warnings about things like React keys. Code perfection is not the objective here: you should be concentrating on learning broad concepts.
7 |
8 | Tags of interest for the files at the end of:
9 |
10 | * `intro` : The intro video
11 | * `normalization` : The normalization video
12 | * `initial-state` : The initial state/mutations video.
13 | * `dom+react` : The components, dom, and react video.
14 | * `how-rendering-works` : The How Rendering Works video.
15 | * `full-stack-1` : The first full-stack video.
16 | * `full-stack-over-time` : Parts 7 and 8.
17 | * `tx-processing` : Transaction fundamentals video.
18 | * `forms-part-1` : Part 10, Form inputs and state.
19 | * `forms-part-2` : Part 11, Form state tracking support.
20 | * `forms-part-3` : Part 12, Subforms and entity ownership
21 | * `forms-final` : Part 13, Finishing up the forms
22 | * `form-save-sql`: Part 13, final part of video.
23 | * `rendering-part-2`: Part 14, Rendering Revisited.
24 | * `sessions-and-routing`: Part 15, sessions and routing
25 | * `dynamic-ui-as-data`: Part 16, Driving dynamic UI with data.
26 | * `ui-state-machine`: Parts 17 + 18, UI State Machines
27 |
28 | If you are relatively new to Git, you would access these as
29 | follows:
30 |
31 | 1. Clone this repository
32 |
33 | ```
34 | git clone https://github.com/fulcrologic/video-series.git
35 | ```
36 |
37 | 2. Check out the tag you are interested in working against.
38 |
39 | ```
40 | git checkout dom+react
41 | ```
42 |
--------------------------------------------------------------------------------
/src/app/server.clj:
--------------------------------------------------------------------------------
1 | (ns app.server
2 | (:require
3 | [clojure.core.async :as async]
4 | [com.fulcrologic.fulcro.server.api-middleware :as fmw :refer [not-found-handler handle-api-request]]
5 | [com.wsscode.pathom.connect :as pc]
6 | [com.wsscode.pathom.core :as p]
7 | [app.model.session :as session]
8 | [ring.middleware.session :refer [wrap-session]]
9 | [ring.middleware.content-type :refer [wrap-content-type]]
10 | [ring.middleware.not-modified :refer [wrap-not-modified]]
11 | [ring.middleware.resource :refer [wrap-resource]]
12 | [clojure.string :as str]))
13 |
14 | (def my-resolvers [session/resolvers])
15 |
16 | ;; setup for a given connect system
17 | (def parser
18 | (p/parallel-parser
19 | {::p/env {::p/reader [p/map-reader
20 | pc/parallel-reader
21 | pc/open-ident-reader]
22 | ::pc/mutation-join-globals [:tempids]}
23 | ::p/mutate pc/mutate-async
24 | ::p/plugins [(pc/connect-plugin {::pc/register my-resolvers})
25 | (p/post-process-parser-plugin p/elide-not-found)
26 | p/error-handler-plugin]}))
27 |
28 | (defn all-routes-to-index [handler]
29 | (fn [{:keys [uri] :as req}]
30 | (if (or
31 | (= "/api" uri)
32 | (str/ends-with? uri ".css")
33 | (str/ends-with? uri ".map")
34 | (str/ends-with? uri ".jpg")
35 | (str/ends-with? uri ".png")
36 | (str/ends-with? uri ".js"))
37 | (handler req)
38 | (handler (assoc req :uri "/index.html")))))
39 |
40 | (defn wrap-api
41 | [handler]
42 | (let [parser (fn [env query] (async/ not-found-handler
49 | (wrap-api)
50 | (fmw/wrap-transit-params)
51 | (fmw/wrap-transit-response)
52 | (wrap-session)
53 | (wrap-resource "public")
54 | wrap-content-type
55 | wrap-not-modified
56 | (all-routes-to-index)))
57 |
--------------------------------------------------------------------------------
/src/app/ui/dynamic_menu.cljs:
--------------------------------------------------------------------------------
1 | (ns app.ui.dynamic-menu
2 | (:require
3 | [app.routing :as r]
4 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
5 | [com.fulcrologic.fulcro.mutations :refer [defmutation]]
6 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]]
7 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown-menu :refer [ui-dropdown-menu]]
8 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown-item :refer [ui-dropdown-item]]
9 | [com.fulcrologic.fulcro.dom :as dom]))
10 |
11 | (defn link
12 | ([title mutation mutation-params]
13 | {:type :link
14 | :label title
15 | :mutation mutation
16 | :mutation-params mutation-params})
17 | ([title mutation] (link title mutation {})))
18 |
19 | (defn dropdown
20 | ([title & items]
21 | {:type :dropdown
22 | :label title
23 | :items (vec items)}))
24 |
25 | (defn dropdown-item
26 | ([title mutation mutation-params]
27 | {:label title
28 | :mutation mutation
29 | :mutation-params mutation-params})
30 | ([title mutation] (dropdown-item title mutation {})))
31 |
32 | (defn menu [& items]
33 | {:dynamic-menu/items (vec items)})
34 |
35 |
36 | (defsc DynamicMenu [this {:keys [:dynamic-menu/items] :as props}]
37 | {:query [:dynamic-menu/items]
38 | :initial-state {:dynamic-menu/items []}
39 | :ident (fn [] [:component/id ::dynamic-menu])}
40 | (comp/fragment
41 | (map (fn [{:keys [type label mutation mutation-params items]}]
42 | (if (= type :link)
43 | (dom/div :.item {:onClick #(comp/transact! this [(list mutation (or mutation-params {}))])}
44 | (dom/div :.content label))
45 | (ui-dropdown {:item true
46 | :text label}
47 | (ui-dropdown-menu {}
48 | (map (fn [{:keys [label mutation mutation-params]}]
49 | (ui-dropdown-item {:onClick #(comp/transact! this [(list mutation (or mutation-params {}))])}
50 | label))
51 | items)))))
52 | items)))
53 |
54 | (def ui-dynamic-menu (comp/factory DynamicMenu))
55 |
56 | (defmutation set-menu [menu]
57 | (action [{:keys [state]}]
58 | (swap! state assoc-in [:component/id ::dynamic-menu] menu))
59 | (refresh [_]
60 | [:dynamic-menu/items]))
61 |
62 | (defn set-menu! [this menu]
63 | (comp/transact! this [(set-menu menu)]))
64 |
65 | (defn clear-menu! [this]
66 | (comp/transact! this [(set-menu (menu))]))
67 |
--------------------------------------------------------------------------------
/src/app/model/session.cljs:
--------------------------------------------------------------------------------
1 | (ns app.model.session
2 | (:require
3 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
4 | [com.fulcrologic.fulcro.dom :as dom :refer [div a]]
5 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
6 | [com.fulcrologic.fulcro.dom.html-entities :as ent]
7 | [com.fulcrologic.fulcro.routing.dynamic-routing :as dr]
8 | [com.fulcrologic.fulcro.algorithms.merge :as merge]
9 | [com.fulcrologic.fulcro.ui-state-machines :as uism :refer [defstatemachine]]
10 | [app.routing :as routing]
11 | [taoensso.timbre :as log]))
12 |
13 | (defn handle-login [{::uism/keys [event-data] :as env}]
14 | (let [user-class (uism/actor-class env :actor/user)]
15 | (-> env
16 | (uism/trigger-remote-mutation :actor/login-form `login
17 | (merge event-data
18 | {:com.fulcrologic.fulcro.mutations/returning user-class
19 | :com.fulcrologic.fulcro.algorithms.data-targeting/target [:session/current-user]
20 | ::uism/ok-event :event/ok
21 | ::uism/error-event :event/error}))
22 | (uism/activate :state/checking-credentials))))
23 |
24 | (def main-events
25 | {:event/logout {::uism/handler (fn [env]
26 | (routing/route-to! "/login")
27 | (-> env
28 | (uism/trigger-remote-mutation :actor/login `logout {})
29 | (uism/apply-action assoc-in [::session :current-user] {:user/id :nobody :user/valid? false})))}
30 | :event/login {::uism/handler handle-login}})
31 |
32 | (defstatemachine session-machine
33 | {::uism/actor-name
34 | #{:actor/user
35 | :actor/login-form}
36 |
37 | ::uism/aliases
38 | {:logged-in? [:actor/user :user/valid?]}
39 |
40 | ::uism/states
41 | {:initial
42 | {::uism/handler
43 | (fn [{::uism/keys [event-data] :as env}]
44 | (-> env
45 | (uism/store :config event-data) ; save desired path for later routing
46 | (uism/load :session/current-user :actor/user {::uism/ok-event :event/ok
47 | ::uism/error-event :event/error})
48 | (uism/activate :state/checking-existing-session)))}
49 |
50 | :state/checking-existing-session
51 | {::uism/events
52 | {:event/ok {::uism/handler (fn [env]
53 | (let [logged-in? (uism/alias-value env :logged-in?)]
54 | (when-not logged-in?
55 | (routing/route-to! "/login"))
56 | (uism/activate env :state/idle)))}
57 | :event/error {::uism/handler (fn [env] (uism/activate env :state/server-failed))}}}
58 |
59 | :state/bad-credentials
60 | {::uism/events main-events}
61 |
62 | :state/idle
63 | {::uism/events main-events}
64 |
65 | :state/checking-credentials
66 | {::uism/events {:event/ok {::uism/handler (fn [env]
67 | (let [logged-in? (uism/alias-value env :logged-in?)
68 | {:keys [desired-path]} (uism/retrieve env :config)]
69 | (when (and logged-in? desired-path)
70 | (routing/route-to! desired-path))
71 | (-> env
72 | (uism/activate (if logged-in?
73 | :state/idle
74 | :state/bad-credentials)))))}
75 | :event/error {::uism/handler (fn [env] (uism/activate env :state/server-failed))}}}
76 |
77 | :state/server-failed
78 | {::uism/events main-events}}})
79 |
80 | (defsc CurrentUser [this {:keys [:user/id :user/email :user/valid?] :as props}]
81 | {:query [:user/id :user/email :user/valid?]
82 | :initial-state {:user/id :nobody :user/valid? false}
83 | :ident (fn [] [::session :current-user])}
84 | (dom/div :.item
85 | (if valid?
86 | (div :.content
87 | email ent/nbsp (a {:onClick
88 | (fn [] (uism/trigger! this ::sessions :event/logout))}
89 | "Logout"))
90 | (a {:onClick #(dr/change-route this ["login"])} "Login"))))
91 |
92 | (def ui-current-user (comp/factory CurrentUser))
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/src/app/client.cljs:
--------------------------------------------------------------------------------
1 | (ns app.client
2 | (:require
3 | [app.client-app :refer [APP]]
4 | [app.routing :as routing]
5 | [app.ui.dynamic-menu :as dynamic-menu]
6 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]]
7 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown-menu :refer [ui-dropdown-menu]]
8 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown-item :refer [ui-dropdown-item]]
9 | [com.fulcrologic.fulcro.application :as app]
10 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
11 | [com.fulcrologic.fulcro.dom :as dom :refer [div ul li button h3 label a input table tr td th thead tbody tfoot]]
12 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
13 | [com.fulcrologic.fulcro.data-fetch :as df]
14 | [com.fulcrologic.fulcro.routing.dynamic-routing :as dr :refer [defrouter]]
15 | [app.model.session :as session :refer [CurrentUser ui-current-user]]
16 | [taoensso.timbre :as log]
17 | [com.fulcrologic.fulcro.dom.events :as evt]
18 | [app.routing :as r]
19 | [com.fulcrologic.fulcro.ui-state-machines :as uism]))
20 |
21 | (defsc LoginForm [this {:ui/keys [email password] :as props}]
22 | {:query [:ui/email :ui/password [::uism/asm-id '_]]
23 | :ident (fn [] [:component/id :login])
24 | :route-segment ["login"]
25 | :initial-state {:ui/email "foo@bar.com"
26 | :ui/password "letmein"}}
27 | (let [current-state (uism/get-active-state this ::session/sessions)
28 | busy? (= :state/checking-credentials current-state)
29 | bad-credentials? (= :state/bad-credentials current-state)
30 | error? (= :state/server-failed current-state)]
31 | (div :.ui.container.segment
32 | (dom/div :.ui.form {:classes [(when (or bad-credentials? error?) "error")]}
33 | (div :.field
34 | (label "Username")
35 | (input {:value email
36 | :disabled busy?
37 | :onChange #(m/set-string! this :ui/email :event %)}))
38 | (div :.field
39 | (label "Password")
40 | (input {:type "password"
41 | :value password
42 | :disabled busy?
43 | :onKeyDown (fn [evt]
44 | (when (evt/enter-key? evt)
45 | (uism/trigger! this ::session/sessions :event/login {:user/email email
46 | :user/password password})))
47 | :onChange #(m/set-string! this :ui/password :event %)}))
48 | (when bad-credentials?
49 | (div :.ui.error.message
50 | (div :.content
51 | "Invalid Credentials")))
52 | (when error?
53 | (div :.ui.error.message
54 | (div :.content
55 | "There was a server error. Please try again.")))
56 | (button :.ui.primary.button
57 | {:classes [(when busy? "loading")]
58 | :onClick #(uism/trigger! this ::session/sessions :event/login {:user/email email
59 | :user/password password})}
60 | "Login")))))
61 |
62 | (defsc Home [this props]
63 | {:query [:pretend-data]
64 | :ident (fn [] [:component/id :home])
65 | :route-segment ["home"]
66 | :initial-state {}}
67 | (dom/div :.ui.container.segment
68 | (h3 "Home Screen")))
69 |
70 | (defsc Settings [this props]
71 | {:query [:pretend-data]
72 | :ident (fn [] [:component/id :settings])
73 | :route-segment ["settings"]
74 | :componentDidMount (fn [this]
75 | (dynamic-menu/set-menu! this (dynamic-menu/menu
76 | (dynamic-menu/link "Other" `other))))
77 | :componentWillUnmount (fn [this] (dynamic-menu/clear-menu! this))
78 | :initial-state {}}
79 | (dom/div :.ui.container.segment
80 | (h3 "Settings Screen")))
81 |
82 | (defrouter MainRouter [_ _] {:router-targets [LoginForm Home Settings]})
83 |
84 | (def ui-main-router (comp/factory MainRouter))
85 |
86 | (defsc Root [this {:root/keys [router dynamic-menu]
87 | :session/keys [current-user]}]
88 | {:query [{:root/router (comp/get-query MainRouter)}
89 | [::uism/asm-id ::session/sessions]
90 | {:root/dynamic-menu (comp/get-query dynamic-menu/DynamicMenu)}
91 | {:session/current-user (comp/get-query CurrentUser)}]
92 | :initial-state (fn [_]
93 | {:root/router (comp/get-initial-state MainRouter)
94 | :session/current-user (comp/get-initial-state CurrentUser)
95 | :root/dynamic-menu (dynamic-menu/menu)})}
96 | (let [current-state (uism/get-active-state this ::session/sessions)
97 | ready? (not (contains? #{:initial :state/checking-existing-session} current-state))
98 | logged-in? (:user/valid? (log/spy :info current-user))]
99 | (div
100 | (div :.ui.top.fixed.menu
101 | (div :.item
102 | (div :.content "My Cool App"))
103 | (when logged-in?
104 | (comp/fragment
105 | (div :.item
106 | (div :.content (a {:href "/home"} "Home")))
107 | (div :.item
108 | (div :.content (a {:href "/settings"} "Settings")))
109 | (dynamic-menu/ui-dynamic-menu dynamic-menu)))
110 | (div :.right.floated.item
111 | (ui-current-user current-user)))
112 | (when ready?
113 | (div :.ui.grid {:style {:marginTop "4em"}}
114 | (ui-main-router router))))))
115 |
116 | (defn refresh []
117 | (app/mount! APP Root "app"))
118 |
119 | (defn ^:export start []
120 | (app/mount! APP Root "app")
121 | (routing/start!)
122 | (uism/begin! APP session/session-machine ::session/sessions
123 | {:actor/user session/CurrentUser
124 | :actor/login-form LoginForm}
125 | {:desired-path (some-> js/window .-location .-pathname)}))
126 |
127 |
--------------------------------------------------------------------------------