repo.cljs 25 KB

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