1
0

repo.cljs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. (ns frontend.components.repo
  2. (:require [clojure.string :as string]
  3. [frontend.common.async-util :as async-util]
  4. [frontend.config :as config]
  5. [frontend.context.i18n :refer [t]]
  6. [frontend.db :as db]
  7. [frontend.handler.db-based.rtc :as rtc-handler]
  8. [frontend.handler.db-based.rtc-flows :as rtc-flows]
  9. [frontend.handler.file-based.nfs :as nfs-handler]
  10. [frontend.handler.file-sync :as file-sync]
  11. [frontend.handler.graph :as graph]
  12. [frontend.handler.notification :as notification]
  13. [frontend.handler.repo :as repo-handler]
  14. [frontend.handler.route :as route-handler]
  15. [frontend.handler.user :as user-handler]
  16. [frontend.mobile.util :as mobile-util]
  17. [frontend.state :as state]
  18. [frontend.ui :as ui]
  19. [frontend.util :as util]
  20. [frontend.util.fs :as fs-util]
  21. [frontend.util.text :as text-util]
  22. [goog.object :as gobj]
  23. [lambdaisland.glogi :as log]
  24. [logseq.shui.ui :as shui]
  25. [medley.core :as medley]
  26. [promesa.core :as p]
  27. [rum.core :as rum]))
  28. (rum/defc normalized-graph-label
  29. [{:keys [url remote? GraphName GraphUUID] :as graph} on-click]
  30. (when graph
  31. [:span.flex.items-center
  32. (if (or (config/local-file-based-graph? url)
  33. (config/db-based-graph? url))
  34. (let [local-dir (config/get-local-dir url)
  35. graph-name (text-util/get-graph-name-from-path url)]
  36. [:a.flex.items-center {:title local-dir
  37. :on-click #(on-click graph)}
  38. [:span graph-name (when GraphName [:strong.px-1 "(" GraphName ")"])]
  39. (when remote? [:strong.pr-1.flex.items-center (ui/icon "cloud")])])
  40. [:a.flex.items-center {:title GraphUUID
  41. :on-click #(on-click graph)}
  42. (db/get-repo-path (or url GraphName))
  43. (when remote? [:strong.pl-1.flex.items-center (ui/icon "cloud")])])]))
  44. (defn sort-repos-with-metadata-local
  45. [repos]
  46. (if-let [m (and (seq repos) (graph/get-metadata-local))]
  47. (->> repos
  48. (map (fn [r] (merge r (get m (:url r)))))
  49. (sort (fn [r1 r2]
  50. (compare (or (:last-seen-at r2) (:created-at r2))
  51. (or (:last-seen-at r1) (:created-at r1))))))
  52. repos))
  53. (defn- safe-locale-date
  54. [dst]
  55. (when (number? dst)
  56. (try
  57. (.toLocaleString (js/Date. dst))
  58. (catch js/Error _e nil))))
  59. (rum/defc repos-inner
  60. "Graph list in `All graphs` page"
  61. [repos]
  62. (for [{:keys [root url remote? GraphUUID GraphSchemaVersion GraphName created-at last-seen-at] :as repo}
  63. (sort-repos-with-metadata-local repos)
  64. :let [only-cloud? (and remote? (nil? root))
  65. db-based? (config/db-based-graph? url)]]
  66. [:div.flex.justify-between.mb-4.items-center.group {:key (or url GraphUUID)}
  67. [:div
  68. [:span.flex.items-center.gap-1
  69. (normalized-graph-label repo
  70. (fn []
  71. (when-not (state/sub :rtc/downloading-graph-uuid)
  72. (cond
  73. root ; exists locally
  74. (state/pub-event! [:graph/switch url])
  75. (and db-based? remote?)
  76. (state/pub-event! [:rtc/download-remote-graph GraphName GraphUUID GraphSchemaVersion])
  77. :else
  78. (state/pub-event! [:graph/pull-down-remote-graph repo])))))]
  79. (when-let [time (some-> (or last-seen-at created-at) (safe-locale-date))]
  80. [:small.text-gray-400.opacity-50 (str "Last opened at: " time)])]
  81. [:div.controls
  82. [:div.flex.flex-row.items-center
  83. (when (util/electron?)
  84. [:a.text-xs.items-center.text-gray-08.hover:underline.hidden.group-hover:flex
  85. {:on-click #(util/open-url (str "file://" root))}
  86. (shui/tabler-icon "folder-pin") [:span.pl-1 root]])
  87. (let [db-graph? (config/db-based-graph? url)
  88. manager? (and db-graph? (user-handler/manager? url))
  89. title (cond
  90. only-cloud?
  91. "Deletes this remote graph. Note this can't be recovered."
  92. db-based?
  93. "Unsafe delete this DB-based graph. Note this can't be recovered."
  94. :else
  95. "Removes Logseq's access to the local file path of your graph. It won't remove your local files.")]
  96. (when-not (and only-cloud? (not manager?))
  97. [:a.text-gray-400.ml-4.font-medium.text-sm.whitespace-nowrap
  98. {:title title
  99. :on-click (fn []
  100. (let [has-prompt? true
  101. prompt-str (cond only-cloud?
  102. (str "Are you sure to permanently delete the graph \"" GraphName "\" from our server?")
  103. db-based?
  104. (str "Are you sure to permanently delete the graph \"" url "\" from Logseq?")
  105. :else
  106. (str "Are you sure to unlink the graph \"" url "\" from local folder?"))
  107. unlink-or-remote-fn! (fn []
  108. (repo-handler/remove-repo! repo)
  109. (state/pub-event! [:graph/unlinked repo (state/get-current-repo)]))
  110. action-confirm-fn! (if only-cloud?
  111. (fn []
  112. (when (or manager? (not db-graph?))
  113. (let [<delete-graph (if db-graph?
  114. rtc-handler/<rtc-delete-graph!
  115. (fn [graph-uuid _graph-schema-version]
  116. (async-util/c->p (file-sync/<delete-graph graph-uuid))))]
  117. (state/set-state! [:file-sync/remote-graphs :loading] true)
  118. (p/do! (<delete-graph GraphUUID GraphSchemaVersion)
  119. (state/delete-repo! repo)
  120. (state/delete-remote-graph! repo)
  121. (state/set-state! [:file-sync/remote-graphs :loading] false)))))
  122. unlink-or-remote-fn!)
  123. confirm-fn!
  124. (fn []
  125. (-> (shui/dialog-confirm!
  126. [:p.font-medium.-my-4 prompt-str
  127. [:span.mt-1.flex.font-normal.opacity-70
  128. (if (or db-based? only-cloud?)
  129. [:small.text-red-rx-11 "⚠️ Notice that we can't recover this graph after being deleted. Make sure you have backups before deleting it."]
  130. [:small.opacity-70 "⚠️ It won't remove your local files!"])]])
  131. (p/then #(action-confirm-fn!))))]
  132. (if has-prompt?
  133. (confirm-fn!)
  134. (unlink-or-remote-fn!))))}
  135. (if only-cloud? "Remove (server)" "Unlink (local)")]))]]]))
  136. (rum/defc repos-cp < rum/reactive
  137. []
  138. (let [login? (boolean (state/sub :auth/id-token))
  139. repos (state/sub [:me :repos])
  140. repos (util/distinct-by :url repos)
  141. remotes (concat
  142. (state/sub :rtc/graphs)
  143. (state/sub [:file-sync/remote-graphs :graphs]))
  144. remotes-loading? (state/sub [:file-sync/remote-graphs :loading])
  145. repos (if (and login? (seq remotes))
  146. (repo-handler/combine-local-&-remote-graphs repos remotes) repos)
  147. repos (remove #(= (:url %) config/demo-repo) repos)
  148. {remote-graphs true local-graphs false} (group-by (comp boolean :remote?) repos)]
  149. [:div#graphs
  150. [:h1.title (t :graph/all-graphs)]
  151. [:div.pl-1.content.mt-3
  152. [:div
  153. [:h2.text-lg.font-medium.my-4 (t :graph/local-graphs)]
  154. (when (seq local-graphs)
  155. (repos-inner local-graphs))
  156. [:div.flex.flex-row.my-4
  157. (if util/web-platform?
  158. [:div.mr-8
  159. (ui/button
  160. "Create a new graph"
  161. :on-click #(state/pub-event! [:graph/new-db-graph]))]
  162. (when (or (nfs-handler/supported?)
  163. (mobile-util/native-platform?))
  164. [:div.mr-8
  165. (ui/button
  166. (t :open-a-directory)
  167. :on-click #(state/pub-event! [:graph/setup-a-repo]))]))]]
  168. (when (and (or (file-sync/enable-sync?)
  169. (state/enable-rtc?))
  170. login?)
  171. [:div
  172. [:hr]
  173. [:div.flex.align-items.justify-between
  174. [:h2.text-lg.font-medium.my-4 (t :graph/remote-graphs)]
  175. [:div
  176. (ui/button
  177. [:span.flex.items-center "Refresh"
  178. (when remotes-loading? [:small.pl-2 (ui/loading nil)])]
  179. :background "gray"
  180. :disabled remotes-loading?
  181. :on-click (fn []
  182. (file-sync/load-session-graphs)
  183. (p/do!
  184. (rtc-handler/<get-remote-graphs)
  185. (repo-handler/refresh-repos!))))]]
  186. (repos-inner remote-graphs)])]]))
  187. (defn- repos-dropdown-links [repos current-repo downloading-graph-id & {:as opts}]
  188. (let [switch-repos (if-not (nil? current-repo)
  189. (remove (fn [repo] (= current-repo (:url repo))) repos) repos) ; exclude current repo
  190. repo-links (mapv
  191. (fn [{:keys [url remote? rtc-graph? GraphName GraphSchemaVersion GraphUUID] :as graph}]
  192. (let [local? (config/local-file-based-graph? url)
  193. db-only? (config/db-based-graph? url)
  194. repo-url (cond
  195. local? (db/get-repo-name url)
  196. db-only? url
  197. :else GraphName)
  198. short-repo-name (if (or local? db-only?)
  199. (text-util/get-graph-name-from-path repo-url)
  200. GraphName)
  201. downloading? (and downloading-graph-id (= GraphUUID downloading-graph-id))]
  202. (when short-repo-name
  203. {:title [:span.flex.items-center.title-wrap short-repo-name
  204. (when remote? [:span.pl-1.flex.items-center
  205. {:title (str "<" GraphName "> #" GraphUUID)}
  206. (ui/icon "cloud" {:size 18})
  207. (when downloading?
  208. [:span.opacity.text-sm.pl-1 "downloading"])])]
  209. :hover-detail repo-url ;; show full path on hover
  210. :options {:on-click
  211. (fn [e]
  212. (when-not downloading?
  213. (when-let [on-click (:on-click opts)]
  214. (on-click e))
  215. (if (and (gobj/get e "shiftKey")
  216. (not (and rtc-graph? remote?)))
  217. (state/pub-event! [:graph/open-new-window url])
  218. (cond
  219. ;; exists locally?
  220. (or (:root graph)
  221. (and db-only? (not rtc-graph?)))
  222. (state/pub-event! [:graph/switch url])
  223. (and rtc-graph? remote?)
  224. (state/pub-event!
  225. [:rtc/download-remote-graph GraphName GraphUUID GraphSchemaVersion])
  226. :else
  227. (state/pub-event! [:graph/pull-down-remote-graph graph])))))}})))
  228. switch-repos)]
  229. (->> repo-links (remove nil?))))
  230. (defn- repos-footer [multiple-windows? db-based?]
  231. [:div.cp__repos-quick-actions
  232. {:on-click #(shui/popup-hide!)}
  233. (when (and (not db-based?)
  234. (not (config/demo-graph?)))
  235. [:<>
  236. (shui/button {:size :sm :variant :ghost
  237. :title (t :sync-from-local-files-detail)
  238. :on-click (fn []
  239. (state/pub-event! [:graph/ask-for-re-fresh]))}
  240. (shui/tabler-icon "file-report") [:span (t :sync-from-local-files)])
  241. (shui/button {:size :sm :variant :ghost
  242. :title (t :re-index-detail)
  243. :on-click (fn []
  244. (state/pub-event! [:graph/ask-for-re-index multiple-windows? nil]))}
  245. (shui/tabler-icon "folder-bolt") [:span (t :re-index)])])
  246. (when (util/electron?)
  247. (shui/button {:size :sm :variant :ghost
  248. :on-click (fn []
  249. (if (or (nfs-handler/supported?) (mobile-util/native-platform?))
  250. (state/pub-event! [:graph/setup-a-repo])
  251. (route-handler/redirect-to-all-graphs)))}
  252. (shui/tabler-icon "folder-plus")
  253. [:span (t :new-graph)]))
  254. (when-not config/publishing?
  255. (shui/button
  256. {:size :sm :variant :ghost
  257. :on-click #(state/pub-event! [:graph/new-db-graph])}
  258. (shui/tabler-icon "database-plus")
  259. [:span (if util/electron? "Create db graph" "Create new graph")]))
  260. (when-not config/publishing?
  261. (shui/button
  262. {:size :sm :variant :ghost
  263. :on-click (fn [] (route-handler/redirect! {:to :import}))}
  264. (shui/tabler-icon "database-import")
  265. [:span (t :import-notes)]))
  266. (when-not config/publishing?
  267. (shui/button {:size :sm :variant :ghost
  268. :on-click #(route-handler/redirect-to-all-graphs)}
  269. (shui/tabler-icon "layout-2") [:span (t :all-graphs)]))])
  270. (rum/defcs repos-dropdown-content < rum/reactive
  271. [_state & {:keys [contentid] :as opts}]
  272. (let [multiple-windows? false
  273. current-repo (state/sub :git/current-repo)
  274. login? (boolean (state/sub :auth/id-token))
  275. repos (state/sub [:me :repos])
  276. remotes (state/sub [:file-sync/remote-graphs :graphs])
  277. rtc-graphs (state/sub :rtc/graphs)
  278. downloading-graph-id (state/sub :rtc/downloading-graph-uuid)
  279. remotes-loading? (state/sub [:file-sync/remote-graphs :loading])
  280. db-based? (config/db-based-graph? current-repo)
  281. repos (sort-repos-with-metadata-local repos)
  282. repos (distinct
  283. (if (and (or (seq remotes) (seq rtc-graphs)) login?)
  284. (repo-handler/combine-local-&-remote-graphs repos (concat remotes rtc-graphs)) repos))
  285. items-fn #(repos-dropdown-links repos current-repo downloading-graph-id opts)
  286. header-fn #(when (> (count repos) 1) ; show switch to if there are multiple repos
  287. [:div.font-medium.text-sm.opacity-50.px-1.py-1.flex.flex-row.justify-between.items-center
  288. [:h4.pb-1 (t :left-side-bar/switch)]
  289. (when (and (file-sync/enable-sync?) login?)
  290. (if remotes-loading?
  291. (ui/loading "")
  292. (shui/button
  293. {:variant :ghost
  294. :size :sm
  295. :title "Refresh remote graphs"
  296. :class "!h-6 !px-1 relative right-[-4px]"
  297. :on-click (fn []
  298. (file-sync/load-session-graphs)
  299. (rtc-handler/<get-remote-graphs))}
  300. (ui/icon "refresh" {:size 15}))))])
  301. _remote? (and current-repo (:remote? (first (filter #(= current-repo (:url %)) repos))))
  302. _repo-name (when current-repo (db/get-repo-name current-repo))]
  303. [:div
  304. {:class (when (<= (count repos) 1) "no-repos")}
  305. (header-fn)
  306. [:div.cp__repos-list-wrap
  307. (for [{:keys [hr item hover-detail title options icon]} (items-fn)]
  308. (let [on-click' (:on-click options)
  309. href' (:href options)]
  310. (if hr
  311. (shui/dropdown-menu-separator)
  312. (shui/dropdown-menu-item
  313. (assoc options
  314. :title hover-detail
  315. :on-click (fn [^js e]
  316. (when on-click'
  317. (when-not (false? (on-click' e))
  318. (shui/popup-hide! contentid)))))
  319. (or item
  320. (if href'
  321. [:a.flex.items-center.w-full
  322. {:href href' :on-click #(shui/popup-hide! contentid)
  323. :style {:color "inherit"}} title]
  324. [:span.flex.items-center.gap-1.w-full
  325. icon [:div title]]))))))]
  326. (repos-footer multiple-windows? db-based?)]))
  327. (rum/defcs graphs-selector < rum/reactive
  328. [_state]
  329. (let [current-repo (state/get-current-repo)
  330. user-repos (state/get-repos)
  331. current-repo' (some->> user-repos (medley/find-first #(= current-repo (:url %))))
  332. repo-name (when current-repo (db/get-repo-name current-repo))
  333. db-based? (config/db-based-graph? current-repo)
  334. remote? (:remote? current-repo')
  335. short-repo-name (if current-repo
  336. (db/get-short-repo-name repo-name)
  337. "Select a Graph")]
  338. [:div.cp__graphs-selector.flex.items-center.justify-between
  339. [:a.item.flex.items-center.gap-1.select-none
  340. {:title current-repo
  341. :on-click (fn [^js e]
  342. (shui/popup-show! (.closest (.-target e) "a")
  343. (fn [{:keys [id]}] (repos-dropdown-content {:contentid id}))
  344. {:as-dropdown? true
  345. :content-props {:class "repos-list"}
  346. :align :start}))}
  347. [:span.thumb (shui/tabler-icon (if remote? "cloud" (if db-based? "database" "folder")) {:size 16})]
  348. [:strong short-repo-name]
  349. (shui/tabler-icon "selector" {:size 18})]]))
  350. (defn invalid-graph-name-warning
  351. []
  352. (notification/show!
  353. [:div
  354. [:p "Graph name can't contain following reserved characters:"]
  355. [:ul
  356. [:li "< (less than)"]
  357. [:li "> (greater than)"]
  358. [:li ": (colon)"]
  359. [:li "\" (double quote)"]
  360. [:li "/ (forward slash)"]
  361. [:li "\\ (backslash)"]
  362. [:li "| (vertical bar or pipe)"]
  363. [:li "? (question mark)"]
  364. [:li "* (asterisk)"]
  365. [:li "# (hash)"]
  366. ;; `+` is used to encode path that includes `:` or `/`
  367. [:li "+ (plus)"]]]
  368. :warning false))
  369. (defn invalid-graph-name?
  370. "Returns boolean indicating if DB graph name is invalid. Must be kept in sync with invalid-graph-name-warning"
  371. [graph-name]
  372. (or (fs-util/include-reserved-chars? graph-name)
  373. (string/includes? graph-name "+")
  374. (string/includes? graph-name "/")))
  375. (rum/defcs new-db-graph < rum/reactive
  376. (rum/local "" ::graph-name)
  377. (rum/local false ::cloud?)
  378. (rum/local false ::creating-db?)
  379. (rum/local (rum/create-ref) ::input-ref)
  380. {:did-mount (fn [s]
  381. (when-let [^js input (some-> @(::input-ref s)
  382. (rum/deref))]
  383. (js/setTimeout #(.focus input) 32))
  384. s)}
  385. [state]
  386. (let [*creating-db? (::creating-db? state)
  387. *graph-name (::graph-name state)
  388. *cloud? (::cloud? state)
  389. input-ref @(::input-ref state)
  390. new-db-f (fn []
  391. (when-not (or (string/blank? @*graph-name)
  392. @*creating-db?)
  393. (if (invalid-graph-name? @*graph-name)
  394. (invalid-graph-name-warning)
  395. (do
  396. (reset! *creating-db? true)
  397. (p/let [repo (repo-handler/new-db! @*graph-name)]
  398. (when @*cloud?
  399. (->
  400. (p/do
  401. (state/set-state! :rtc/uploading? true)
  402. (rtc-handler/<rtc-create-graph! repo)
  403. (state/set-state! :rtc/uploading? false)
  404. (rtc-flows/trigger-rtc-start repo))
  405. (p/catch (fn [error]
  406. (reset! *creating-db? false)
  407. (state/set-state! :rtc/uploading? false)
  408. (log/error :create-db-failed error)))))
  409. (reset! *creating-db? false)
  410. (shui/dialog-close!))))))
  411. submit! (fn [^js e click?]
  412. (when-let [value (and (or click? (= (gobj/get e "key") "Enter"))
  413. (util/trim-safe (.-value (rum/deref input-ref))))]
  414. (reset! *graph-name value)
  415. (new-db-f)))]
  416. [:div.new-graph.flex.flex-col.gap-4.p-1.pt-2
  417. (shui/input
  418. {:default-value @*graph-name
  419. :disabled @*creating-db?
  420. :ref input-ref
  421. :placeholder "your graph name"
  422. :on-key-down submit!})
  423. (when (user-handler/team-member?)
  424. [:div.flex.flex-row.items-center.gap-1
  425. (shui/checkbox
  426. {:id "rtc-sync"
  427. :value @*cloud?
  428. :on-checked-change #(swap! *cloud? not)})
  429. [:label.opacity-70.text-sm
  430. {:for "rtc-sync"}
  431. "Use Logseq Sync?"]])
  432. (shui/button
  433. {:on-click #(submit! % true)
  434. :on-key-down submit!}
  435. (if @*creating-db?
  436. (ui/loading "Creating graph")
  437. "Submit"))]))