repo.cljs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. (ns frontend.components.repo
  2. (:require [frontend.components.widgets :as widgets]
  3. [frontend.config :as config]
  4. [frontend.context.i18n :refer [t]]
  5. [frontend.db :as db]
  6. [frontend.handler.repo :as repo-handler]
  7. [frontend.handler.file-based.nfs :as nfs-handler]
  8. [frontend.state :as state]
  9. [frontend.ui :as ui]
  10. [frontend.util :as util]
  11. [rum.core :as rum]
  12. [frontend.mobile.util :as mobile-util]
  13. [frontend.util.text :as text-util]
  14. [promesa.core :as p]
  15. [electron.ipc :as ipc]
  16. [goog.object :as gobj]
  17. [cljs.core.async :as async :refer [go <!]]
  18. [clojure.string :as string]
  19. [frontend.handler.file-sync :as file-sync]
  20. [reitit.frontend.easy :as rfe]
  21. [frontend.handler.notification :as notification]
  22. [frontend.util.fs :as fs-util]
  23. [frontend.handler.user :as user-handler]
  24. [logseq.shui.ui :as shui]
  25. [frontend.handler.db-based.rtc :as rtc-handler]))
  26. (rum/defc normalized-graph-label
  27. [{:keys [url remote? GraphName GraphUUID] :as graph} on-click]
  28. (when graph
  29. [:span.flex.items-center
  30. (if (or (config/local-file-based-graph? url)
  31. (config/db-based-graph? url))
  32. (let [local-dir (config/get-local-dir url)
  33. graph-name (text-util/get-graph-name-from-path url)]
  34. [:a.flex.items-center {:title local-dir
  35. :on-click #(on-click graph)}
  36. [:span graph-name (when GraphName [:strong.px-1 "(" GraphName ")"])]
  37. (when remote? [:strong.pr-1.flex.items-center (ui/icon "cloud")])])
  38. [:a.flex.items-center {:title GraphUUID
  39. :on-click #(on-click graph)}
  40. (db/get-repo-path (or url GraphName))
  41. (when remote? [:strong.pl-1.flex.items-center (ui/icon "cloud")])])]))
  42. (rum/defc repos-inner
  43. "Graph list in `All graphs` page"
  44. [repos]
  45. (for [{:keys [url remote? GraphUUID GraphName] :as repo} repos
  46. :let [only-cloud? (and remote? (nil? url))
  47. db-based? (config/db-based-graph? url)]]
  48. [:div.flex.justify-between.mb-4.items-center {:key (or url GraphUUID)}
  49. (normalized-graph-label repo #(if only-cloud?
  50. (state/pub-event! [:graph/pull-down-remote-graph repo])
  51. (state/pub-event! [:graph/switch url])))
  52. [:div.controls
  53. [:div.flex.flex-row.items-center
  54. (ui/tippy {:html [:div.text-sm.max-w-xs
  55. (cond
  56. only-cloud?
  57. "Deletes this remote graph. Note this can't be recovered."
  58. db-based?
  59. "Unsafe delete this DB-based graph. Note this can't be recovered."
  60. :else
  61. "Removes Logseq's access to the local file path of your graph. It won't remove your local files.")]
  62. :class "tippy-hover"
  63. :interactive true}
  64. [:a.text-gray-400.ml-4.font-medium.text-sm.whitespace-nowrap
  65. {:on-click (fn []
  66. (let [has-prompt? (or only-cloud? db-based?)
  67. prompt-str (cond only-cloud?
  68. (str "Are you sure to permanently delete the graph \"" GraphName "\" from our server?")
  69. db-based?
  70. (str "Are you sure to permanently delete the graph \"" url "\" from Logseq?")
  71. :else
  72. "")
  73. unlink-or-remote-fn (fn []
  74. (let [current-repo (state/get-current-repo)]
  75. (repo-handler/remove-repo! repo)
  76. (state/pub-event! [:graph/unlinked repo current-repo])))
  77. action-confirm-fn (if only-cloud?
  78. (fn []
  79. (state/set-state! [:file-sync/remote-graphs :loading] true)
  80. (go (<! (file-sync/<delete-graph GraphUUID))
  81. (state/delete-repo! repo)
  82. (state/delete-remote-graph! repo)
  83. (state/set-state! [:file-sync/remote-graphs :loading] false)))
  84. unlink-or-remote-fn)
  85. confirm-fn
  86. (fn []
  87. (ui/make-confirm-modal
  88. {:title [:div
  89. {:style {:max-width 700}}
  90. prompt-str]
  91. :sub-title [:div.small.mt-1
  92. "Notice that we can't recover this graph after being deleted. Make sure you have backups before deleting it."]
  93. :on-confirm (fn [_ {:keys [close-fn]}]
  94. (close-fn)
  95. (action-confirm-fn))}))]
  96. (if has-prompt?
  97. (state/set-modal! (confirm-fn))
  98. (unlink-or-remote-fn))))}
  99. (if (or db-based? only-cloud?) "Remove" "Unlink")])]]]))
  100. (rum/defc repos < rum/reactive
  101. []
  102. (let [login? (boolean (state/sub :auth/id-token))
  103. repos (state/sub [:me :repos])
  104. repos (util/distinct-by :url repos)
  105. remotes (state/sub [:file-sync/remote-graphs :graphs])
  106. remotes-loading? (state/sub [:file-sync/remote-graphs :loading])
  107. repos (if (and login? (seq remotes))
  108. (repo-handler/combine-local-&-remote-graphs repos remotes) repos)
  109. repos (remove #(= (:url %) config/demo-repo) repos)
  110. {remote-graphs true local-graphs false} (group-by (comp boolean :remote?) repos)]
  111. (if (seq repos)
  112. [:div#graphs
  113. [:h1.title (t :graph/all-graphs)]
  114. [:div.pl-1.content.mt-3
  115. [:div
  116. [:h2.text-lg.font-medium.my-4 (t :graph/local-graphs)]
  117. (when (seq local-graphs)
  118. (repos-inner local-graphs))
  119. [:div.flex.flex-row.my-4
  120. (when (or (nfs-handler/supported?)
  121. (mobile-util/native-platform?))
  122. [:div.mr-8
  123. (ui/button
  124. (t :open-a-directory)
  125. :on-click #(state/pub-event! [:graph/setup-a-repo]))])]]
  126. ;; TODO: support rtc
  127. (when (and (file-sync/enable-sync?) login?)
  128. [:div
  129. [:hr]
  130. [:div.flex.align-items.justify-between
  131. [:h2.text-lg.font-medium.my-4 (t :graph/remote-graphs)]
  132. [:div
  133. (ui/button
  134. [:span.flex.items-center "Refresh"
  135. (when remotes-loading? [:small.pl-2 (ui/loading nil)])]
  136. :background "gray"
  137. :disabled remotes-loading?
  138. :on-click #(file-sync/load-session-graphs))]]
  139. (repos-inner remote-graphs)])]]
  140. (widgets/add-graph))))
  141. (defn- check-multiple-windows?
  142. [state]
  143. (when (util/electron?)
  144. (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
  145. (reset! (::electron-multiple-windows? state) multiple-windows?))))
  146. (defn- repos-dropdown-links [repos current-repo *multiple-windows? & {:as opts}]
  147. (let [switch-repos (if-not (nil? current-repo)
  148. (remove (fn [repo] (= current-repo (:url repo))) repos) repos) ; exclude current repo
  149. repo-links (mapv
  150. (fn [{:keys [url remote? rtc-graph? GraphName GraphUUID] :as graph}]
  151. (let [local? (config/local-file-based-graph? url)
  152. db-only? (config/db-based-graph? url)
  153. repo-url (cond
  154. local? (db/get-repo-name url)
  155. db-only? url
  156. :else GraphName)
  157. short-repo-name (if (or local? db-only?)
  158. (text-util/get-graph-name-from-path repo-url)
  159. GraphName)]
  160. (when short-repo-name
  161. {:title [:span.flex.items-center.whitespace-nowrap short-repo-name
  162. (when remote? [:span.pl-1.flex.items-center
  163. {:title (str "<" GraphName "> #" GraphUUID)}
  164. (ui/icon "cloud" {:size 18})])]
  165. :hover-detail repo-url ;; show full path on hover
  166. :options {:on-click (fn [e]
  167. (when-let [on-click (:on-click opts)]
  168. (on-click e))
  169. (if (gobj/get e "shiftKey")
  170. (state/pub-event! [:graph/open-new-window url])
  171. (cond
  172. (or local? db-only?)
  173. (state/pub-event! [:graph/switch url])
  174. (and rtc-graph? remote?)
  175. (state/pub-event! [:rtc/download-remote-graph GraphName GraphUUID])
  176. :else
  177. (state/pub-event! [:graph/pull-down-remote-graph graph]))))}})))
  178. switch-repos)
  179. refresh-link (let [nfs-repo? (config/local-file-based-graph? current-repo)]
  180. (when (and nfs-repo?
  181. (not= current-repo config/demo-repo)
  182. (or (nfs-handler/supported?)
  183. (mobile-util/native-platform?)))
  184. {:title (t :sync-from-local-files)
  185. :hover-detail (t :sync-from-local-files-detail)
  186. :options {:on-click #(state/pub-event! [:graph/ask-for-re-fresh])}}))
  187. reindex-link {:title (t :re-index)
  188. :hover-detail (t :re-index-detail)
  189. :options (cond->
  190. {:on-click
  191. (fn []
  192. (state/pub-event! [:graph/ask-for-re-index *multiple-windows? nil]))})}]
  193. (->>
  194. (concat repo-links
  195. [(when (seq repo-links) {:hr true})
  196. (if (or (nfs-handler/supported?) (mobile-util/native-platform?))
  197. {:title (t :new-graph) :options {:on-click #(state/pub-event! [:graph/setup-a-repo])}}
  198. {:title (t :new-graph) :options {:href (rfe/href :repos)}}) ;; Brings to the repos page for showing fallback message
  199. (when config/db-graph-enabled?
  200. {:title (str (t :new-graph) " - DB version")
  201. :options {:on-click #(state/pub-event! [:graph/new-db-graph])}})
  202. {:title (t :all-graphs) :options {:href (rfe/href :repos)}}
  203. refresh-link
  204. (when-not (config/db-based-graph? current-repo)
  205. reindex-link)])
  206. (remove nil?))))
  207. (rum/defcs repos-dropdown < rum/reactive
  208. (rum/local false ::electron-multiple-windows?)
  209. [state & {:as opts}]
  210. (let [multiple-windows? (::electron-multiple-windows? state)
  211. current-repo (state/sub :git/current-repo)
  212. login? (boolean (state/sub :auth/id-token))
  213. remotes-loading? (state/sub [:file-sync/remote-graphs :loading])]
  214. (when (or login? current-repo)
  215. (let [repos (state/sub [:me :repos])
  216. remotes (state/sub [:file-sync/remote-graphs :graphs])
  217. rtc-graphs (state/sub :rtc/graphs)
  218. repos (if (and (or (seq remotes) (seq rtc-graphs)) login?)
  219. (repo-handler/combine-local-&-remote-graphs repos (concat remotes rtc-graphs)) repos)
  220. links (repos-dropdown-links repos current-repo multiple-windows? opts)
  221. render-content (fn [{:keys [toggle-fn]}]
  222. (let [remote? (:remote? (first (filter #(= current-repo (:url %)) repos)))
  223. repo-name (db/get-repo-name current-repo)
  224. short-repo-name (if repo-name
  225. (db/get-short-repo-name repo-name)
  226. "Select a Graph")]
  227. [:a.item.group.flex.items-center.p-2.text-sm.font-medium.rounded-md
  228. {:on-click (fn [_e]
  229. (check-multiple-windows? state)
  230. (toggle-fn))
  231. :title repo-name} ;; show full path on hover
  232. [:div.flex.flex-row.items-center
  233. [:div.flex.relative.graph-icon.rounded
  234. (let [icon "database"
  235. opts {:size 14}]
  236. (ui/icon icon opts))]
  237. [:div.graphs
  238. [:span#repo-switch.block.pr-2.whitespace-nowrap
  239. [:span [:span#repo-name.font-medium
  240. [:span.overflow-hidden.text-ellipsis (if (= config/demo-repo short-repo-name) "Demo" short-repo-name)]
  241. (when remote? [:span.pl-1 (ui/icon "cloud")])]]
  242. [:span.dropdown-caret.ml-2 {:style {:border-top-color "#6b7280"}}]]]]]))
  243. links-header (cond->
  244. {:z-index 1000
  245. :modal-class (util/hiccup->class
  246. "origin-top-right.absolute.left-0.mt-2.rounded-md.shadow-lg")}
  247. (> (count repos) 1) ; show switch to if there are multiple repos
  248. (assoc :links-header [:div.font-medium.text-sm.opacity-70.px-4.pt-2.pb-1.flex.flex-row.justify-between.items-center
  249. [:div (t :left-side-bar/switch)]
  250. (when (and (file-sync/enable-sync?) login?)
  251. (if remotes-loading?
  252. (ui/loading "")
  253. [:a.flex {:title "Refresh remote graphs"
  254. :on-click (fn []
  255. (file-sync/load-session-graphs)
  256. (when config/dev? (rtc-handler/<get-remote-graphs)))}
  257. (ui/icon "refresh")]))]))]
  258. (when (seq repos)
  259. (ui/dropdown-with-links render-content links links-header))))))
  260. (defn invalid-graph-name-warning
  261. []
  262. (notification/show!
  263. [:div
  264. [:p "Graph name can't contain following reserved characters:"]
  265. [:ul
  266. [:li "< (less than)"]
  267. [:li "> (greater than)"]
  268. [:li ": (colon)"]
  269. [:li "\" (double quote)"]
  270. [:li "/ (forward slash)"]
  271. [:li "\\ (backslash)"]
  272. [:li "| (vertical bar or pipe)"]
  273. [:li "? (question mark)"]
  274. [:li "* (asterisk)"]
  275. [:li "# (hash)"]
  276. ;; `+` is used to encode path that includes `:` or `/`
  277. [:li "+ (plus)"]]]
  278. :warning false))
  279. (defn invalid-graph-name?
  280. "Returns boolean indicating if DB graph name is invalid. Must be kept in sync with invalid-graph-name-warning"
  281. [graph-name]
  282. (or (fs-util/include-reserved-chars? graph-name)
  283. (string/includes? graph-name "+")))
  284. (rum/defcs new-db-graph <
  285. (rum/local "" ::graph-name)
  286. (rum/local false ::cloud?)
  287. [state]
  288. (let [*graph-name (::graph-name state)
  289. *cloud? (::cloud? state)
  290. new-db-f (fn []
  291. (when-not (string/blank? @*graph-name)
  292. (if (invalid-graph-name? @*graph-name)
  293. (invalid-graph-name-warning)
  294. (p/let [repo (repo-handler/new-db! @*graph-name)]
  295. (when @*cloud?
  296. (p/let [_create-result (rtc-handler/<rtc-create-graph! repo)
  297. _start-result (rtc-handler/<rtc-start! repo)]
  298. ;; TODO: can't pr-str result
  299. (state/close-modal!)))))))]
  300. [:div.new-graph.flex.flex-col.p-4.gap-4
  301. [:h1.title.mb-4 "Create new graph: "]
  302. [:input.form-input {:value @*graph-name
  303. :auto-focus true
  304. :on-change #(reset! *graph-name (util/evalue %))
  305. :on-key-down (fn [^js e]
  306. (when (= (gobj/get e "key") "Enter")
  307. (new-db-f)))}]
  308. [:div.flex.flex-row.items-center.gap-1
  309. (when (user-handler/logged-in?)
  310. (shui/checkbox
  311. {:value @*cloud?
  312. :on-checked-change #(swap! *cloud? not)}))
  313. [:div.opacity-70.text-sm "Use Logseq Sync?"]]
  314. (ui/button "Submit"
  315. :on-click new-db-f
  316. :on-key-down (fn [^js e]
  317. (when (= (gobj/get e "key") "Enter")
  318. (new-db-f))))]))