Tienson Qin 5 лет назад
Родитель
Сommit
bf33bfa628

+ 4 - 0
backend/src/backend/db/repo.clj

@@ -26,3 +26,7 @@
 (defn delete
   [id]
   (db/delete! Repo {:id id}))
+
+(defn update
+  [id url]
+  (db/update! Repo id {:url url}))

+ 36 - 5
backend/src/backend/routes.clj

@@ -6,10 +6,13 @@
             [backend.auth :as auth]
             [backend.db.user :as u]
             [backend.db.token :as token]
+            [backend.db.repo :as repo]
             [ring.util.response :as resp]
             [backend.views.home :as home]
             [backend.interceptors :as interceptors]))
 
+;; TODO: spec validate, authorization (owner?)
+
 (def routes
   [["/swagger.json"
     {:get {:no-doc true
@@ -63,10 +66,38 @@
             :handler
             (fn [{:keys [app-context] :as req}]
               (if-let [user (:user app-context)]
-                (let [tokens (token/get-user-tokens (:id user))]
+                (let [user-id (:id user)]
                   {:status 200
                    :body {:user user
-                          :tokens tokens}})
-                {:status 200
-                 :body {:user nil}}))}}]]
-   ])
+                          :tokens (token/get-user-tokens user-id)
+                          :repos (repo/get-user-repos user-id)}})
+                {:status 404
+                 :body "not-found"}))}}]
+
+    ["/repos"
+     {:post {:summary "Add a repo"
+             :handler
+             (fn [{:keys [app-context body-params] :as req}]
+               (let [user (:user app-context)
+                     result (repo/insert {:user_id (:id user)
+                                          :url (:url body-params)})]
+                 {:status 201
+                  :body result}))}
+      }]
+
+    ["/repos/:id"
+     {:patch {:summary "Update a repo's url"
+              :handler
+              (fn [{:keys [app-context params body-params] :as req}]
+                (let [user (:user app-context)
+                      result (repo/update (:id params)
+                                          (:url body-params))]
+                  {:status 200
+                   :body result}))}
+      :delete {:summary "Delete a repo"
+               :handler
+               (fn [{:keys [app-context params] :as req}]
+                 (let [user (:user app-context)
+                       result (repo/delete (:id params))]
+                   {:status 200
+                    :body {:result true}}))}}]]])

+ 135 - 0
frontend/src/frontend/api.cljs

@@ -0,0 +1,135 @@
+(ns frontend.api
+  (:require [clojure.string :as str]
+            [cognitect.transit :as t]
+            [clojure.walk :as walk]
+            [goog.events :as events]
+            [frontend.config :as config])
+  (:import [goog.net XhrIo EventType]))
+
+(defn parse-headers [headers]
+  (reduce
+   #(let [[k v] (str/split %2 #":\s+")]
+      (if (or (str/blank? k) (str/blank? v))
+        %1 (assoc %1 (str/lower-case k) v)))
+   {} (str/split (or headers "") #"(\n)|(\r)|(\r\n)|(\n\r)")))
+
+;; goog.net.ErrorCode constants to CLJS keywords
+(def error-kw
+  {0 :no-error
+   1 :access-denied
+   2 :file-not-found
+   3 :ff-silent-error
+   4 :custom-error
+   5 :exception
+   6 :http-error
+   7 :abort
+   8 :timeout
+   9 :offline})
+
+(defn to-transit
+  "Serialization for clojure data."
+  [msg]
+  (let [writer (t/writer :json)]
+    (t/write writer msg)))
+
+(defn from-transit
+  "Deserialization for clojure data."
+  [in keywordize?]
+  (let [reader (t/reader :json)]
+    (cond-> (t/read reader in)
+        keywordize?
+        (walk/keywordize-keys))))
+
+(defn from-json
+  [in]
+  (-> in
+      js/JSON.parse
+      (js->clj :keywordize-keys true)))
+
+(defn to-json [params]
+  (.stringify js/JSON (clj->js params)))
+
+(defn- type->header [type]
+  (case type
+    :text {"Content-Type" "text/plain"}
+    :edn {"Content-Type" "application/edn"}
+    :transit {"Content-Type" "application/transit+json"
+              "Accept" "application/transit+json"}
+    nil))
+
+(defn- token->header [token]
+  (if token
+    {"Authorization" (str "Bearer " token)}
+    {}))
+
+(defn fetch [api-host {:keys [endpoint params method type headers token on-success on-error
+                              on-progress on-upload on-download
+                              keywordize?]
+                       :or {method :post
+                            type   :transit
+                            endpoint nil
+                            keywordize? true}
+                       :as args}]
+  (let [xhr (XhrIo.)
+        ;; (doto (XhrIo.)
+        ;;       (.setTimeoutInterval 5000))
+        named-method (str/upper-case (name method))
+        body         (case type
+                       :json (to-json params)
+                       :edn  (pr-str params)
+                       :transit (to-transit params)
+                       :raw params)
+        headers      (merge (type->header type)
+                            headers
+                            ;; (token->header token)
+                            )]
+    (when (or on-upload on-download)
+      (.setProgressEventsEnabled xhr true)
+      ;; (events/listen xhr EventType.PROGRESS
+      ;;                (fn [e]
+      ;;                  (on-progress {:loaded (.-loaded e)
+      ;;                                :total (.-total e)})))
+      (when on-upload
+        (events/listen xhr EventType.UPLOAD_PROGRESS on-upload))
+      (when on-download
+        (events/listen xhr EventType.DOWNLOAD_PROGRESS on-download)))
+    (events/listen xhr EventType.COMPLETE
+                   (fn [e]
+                     (let [target ^js (.-target e)
+                           ]
+                       (if (.isSuccess target)
+                         (let [body (from-transit (.getResponseText target) keywordize?)]
+                           (on-success body))
+                         (let [response {:status (.getStatus target)
+                                         :success (.isSuccess target)
+                                         :body (.getResponseText target)
+                                         :headers (parse-headers (.getAllResponseHeaders target))
+                                         :error-code (error-kw (.getLastErrorCode target))
+                                         :error-text (.getLastError target)}])))))
+    (.send xhr (str api-host endpoint)
+           named-method
+           body
+           headers
+           ;; ;; timeoutInterval
+           ;; 5000
+           ;; ;; withCredentials
+           ;; true
+           )))
+
+(defn get-me
+  [on-success on-error]
+  (fetch config/api
+         {:endpoint "me"
+          :method :get
+          :on-success on-success
+          :on-error on-error}))
+
+;; TODO: add spec
+(defn add-repo
+  [url on-success on-error]
+  (fetch config/api
+         {:endpoint "repos"
+          :method :post
+          :params {:url url}
+          :on-success on-success
+          :on-error on-error}))

+ 13 - 7
frontend/src/frontend/components/home.cljs

@@ -8,6 +8,7 @@
             [frontend.components.agenda :as agenda]
             [frontend.components.file :as file]
             [frontend.components.settings :as settings]
+            [frontend.components.repo :as repo]
             [frontend.format :as format]
             [clojure.string :as string]))
 
@@ -28,7 +29,7 @@
 (rum/defc home < rum/reactive
   []
   (let [state (rum/react state/state)
-        {:keys [cloned? github-username github-token github-repo contents loadings current-file files width drawer? tasks links cloning?]} state
+        {:keys [user tokens repos repo-url cloned? github-username github-token github-repo contents loadings current-file files width drawer? tasks links cloning?]} state
         loading? (get loadings current-file)
         width (or width (util/get-width))
         mobile? (and width (<= width 600))]
@@ -39,6 +40,16 @@
               ;; TODO: fewer spacing for mobile, 24px
               :margin-top 64}}
      (cond
+       (nil? user)
+       (mui/button {:variant "contained"
+                    :color "primary"
+                    :start-icon (mui/github-icon)
+                    :href "/login/github"}
+         "Login with Github")
+
+       (empty? repos)
+       (repo/add-repo repo-url)
+
        cloned?
        (mui/grid
         {:container true
@@ -70,11 +81,6 @@
        [:div "Cloning..."]
 
        :else
-       (mui/button {:variant "contained"
-                    :color "primary"
-                    :start-icon (mui/github-icon)
-                    :href "/login/github"}
-         "Login with Github")
-
+       [:div "TBC"]
        ;; (settings/settings-form github-username github-token github-repo)
        ))))

+ 27 - 0
frontend/src/frontend/components/repo.cljs

@@ -0,0 +1,27 @@
+(ns frontend.components.repo
+  (:require [rum.core :as rum]
+            [frontend.mui :as mui]
+            [frontend.util :as util]
+            [frontend.state :as state]
+            [frontend.handler :as handler]
+            [clojure.string :as string]))
+
+(defn add-repo
+  [repo-url]
+  [:form {:style {:min-width 300}}
+   (mui/grid
+    {:container true
+     :direction "column"}
+    (mui/text-field {:id "standard-basic"
+                     :style {:margin-bottom 12}
+                     :label "Repo url"
+                     :on-change (fn [event]
+                                  (let [v (util/evalue event)]
+                                    (swap! state/state assoc :repo-url v)))
+                     :value repo-url
+                     })
+    (mui/button {:variant "contained"
+                 :color "primary"
+                 :on-click (fn []
+                             (handler/add-repo-and-clone repo-url))}
+      "Sync"))])

+ 6 - 0
frontend/src/frontend/config.cljs

@@ -5,3 +5,9 @@
 (defonce tasks-org "tasks.org")
 (defonce links-org "links.org")
 (defonce hidden-file ".hidden")
+(defonce dev? ^boolean goog.DEBUG)
+(def website
+  (if dev?
+    "http://localhost:8080"
+    "https://gitnotes.com"))
+(def api (str website "/api/v1/"))

+ 5 - 3
frontend/src/frontend/core.cljs

@@ -6,7 +6,8 @@
             [frontend.state :as state]
             [frontend.handler :as handler]
             [frontend.routes :as routes]
-            [frontend.page :as page]))
+            [frontend.page :as page]
+            [frontend.api :as api]))
 
 (defn start []
   (rum/mount (page/current-page)
@@ -16,6 +17,7 @@
   ;; init is called ONCE when the page loads
   ;; this is called in the index.html and must be exported
   ;; so it is available even in :advanced release builds
+  (handler/get-me)
 
   (handler/load-from-disk)
 
@@ -26,9 +28,9 @@
 
   (handler/listen-to-resize)
 
-  (handler/request-notifications-if-not-asked)
+  ;; (handler/request-notifications-if-not-asked)
 
-  (handler/run-notify-worker!)
+  ;; (handler/run-notify-worker!)
 
   (start))
 

+ 41 - 5
frontend/src/frontend/handler.cljs

@@ -11,7 +11,8 @@
             [frontend.config :as config]
             [clojure.walk :as walk]
             [clojure.string :as string]
-            [promesa.core :as p])
+            [promesa.core :as p]
+            [frontend.api :as api])
   (:import [goog.events EventHandler]))
 
 (defn load-file
@@ -143,10 +144,6 @@
 
 (defn clone
   [github-username github-token github-repo]
-  (storage/set :github-username github-username)
-  (storage/set :github-token github-token)
-  (storage/set :github-repo github-repo)
-
   (util/p-handle
    (do
      (swap! state/state assoc
@@ -368,3 +365,42 @@
      (let [headings (extract-all-headings)]
        (reset! headings-atom headings)
        (db/transact-headings! headings)))))
+
+(defn get-me
+  []
+  (api/get-me (fn [body]
+                (let [{:keys [user tokens repos]} body]
+                  (swap! state/state assoc
+                         :user user
+                         :tokens tokens
+                         :repos repos)))
+              (fn [response]
+                (prn "Can't get user's information, error response: " response))))
+
+(defn get-user-token-repos
+  []
+  (let [user (:user @state/state)
+        token (:oauth_token (first (:tokens @state/state)))
+        repos (map :url (:repos @state/state))]
+    [user token repos]))
+
+(defn add-repo-and-clone
+  [url]
+  (api/add-repo url
+                (fn [repo]
+                  (let [[user token _] (get-user-token-repos)]
+                    (swap! state/state
+                           update :repos conj repo)
+                    ;; clone
+                    (clone (:name user) token url)))
+                (fn [response]
+                  (prn "Can't add repo: " url))))
+
+(defn sync
+  []
+  (let [[user token repos] (get-user-token-repos)]
+    (doseq [repo repos]
+      (prn {:name (:name user)
+            :token token
+            :repo repo})
+      (clone (:name user) token repo))))

+ 2 - 2
frontend/src/frontend/layout.cljs

@@ -39,8 +39,8 @@
 
         (mui/button {:color "inherit"
                      :on-click (fn []
-                                 (handler/change-page :links))}
-          "Links")
+                                 (handler/sync))}
+          "Sync")
 
         (mui/button {:color "inherit"
                      :on-click (fn []

+ 5 - 4
frontend/src/frontend/state.cljs

@@ -1,16 +1,17 @@
 (ns frontend.state
   (:require [frontend.storage :as storage]))
 
-(def state (atom {:current-page :home
+(def state (atom {:user nil
+                  :tokens []
+                  :repos []
+                  :repo-url ""
+                  :current-page :home
                   :cloning? false
                   :cloned? (storage/get :cloned?)
                   :files []
                   :contents {}          ; file name -> string
                   :current-file nil
                   :loadings {}            ; file name -> bool
-                  :github-username ""
-                  :github-token ""
-                  :github-repo ""
                   :width nil
                   :drawer? false
                   :tasks {}