page.cljs 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
  1. (ns frontend.components.page
  2. (:require [clojure.string :as string]
  3. [frontend.components.block :as block]
  4. [frontend.components.content :as content]
  5. [frontend.components.editor :as editor]
  6. [frontend.components.hierarchy :as hierarchy]
  7. [frontend.components.plugins :as plugins]
  8. [frontend.components.reference :as reference]
  9. [frontend.components.svg :as svg]
  10. [frontend.config :as config]
  11. [frontend.context.i18n :refer [t]]
  12. [frontend.date :as date]
  13. [frontend.db :as db]
  14. [frontend.db-mixins :as db-mixins]
  15. [frontend.db.model :as model]
  16. [frontend.extensions.graph :as graph]
  17. [frontend.extensions.pdf.assets :as pdf-assets]
  18. [frontend.format.block :as format-block]
  19. [frontend.handler.common :as common-handler]
  20. [frontend.handler.config :as config-handler]
  21. [frontend.handler.editor :as editor-handler]
  22. [frontend.handler.graph :as graph-handler]
  23. [frontend.handler.notification :as notification]
  24. [frontend.handler.page :as page-handler]
  25. [frontend.handler.plugin :as plugin-handler]
  26. [frontend.handler.route :as route-handler]
  27. [frontend.mixins :as mixins]
  28. [frontend.state :as state]
  29. [frontend.text :as text]
  30. [frontend.search :as search]
  31. [frontend.ui :as ui]
  32. [frontend.util :as util]
  33. [goog.object :as gobj]
  34. [reitit.frontend.easy :as rfe]
  35. [medley.core :as medley]
  36. [rum.core :as rum]
  37. [frontend.mobile.util :as mobile-util]))
  38. (defn- get-page-name
  39. [state]
  40. (let [route-match (first (:rum/args state))]
  41. (get-in route-match [:parameters :path :name])))
  42. (defn- get-blocks
  43. [repo page-name block-id]
  44. (when page-name
  45. (let [root (if block-id
  46. (db/pull [:block/uuid block-id])
  47. (model/get-page page-name))
  48. opts (if block-id
  49. {:scoped-block-id (:db/id root)}
  50. {})]
  51. (db/get-paginated-blocks repo (:db/id root) opts))))
  52. (defn- open-first-block!
  53. [state]
  54. (let [[_ blocks _ sidebar? preview?] (:rum/args state)]
  55. (when (and
  56. (or preview?
  57. (not (contains? #{:home :all-journals} (state/get-current-route))))
  58. (not sidebar?))
  59. (let [block (first blocks)]
  60. (when (and (= (count blocks) 1)
  61. (string/blank? (:block/content block))
  62. (not preview?))
  63. (editor-handler/edit-block! block :max (:block/uuid block))))))
  64. state)
  65. (rum/defc page-blocks-inner <
  66. {:did-mount open-first-block!
  67. :did-update open-first-block!}
  68. [page-name _blocks hiccup sidebar? _block-uuid]
  69. [:div.page-blocks-inner {:style {:margin-left (if sidebar? 0 -20)}}
  70. (rum/with-key
  71. (content/content page-name
  72. {:hiccup hiccup
  73. :sidebar? sidebar?})
  74. (str page-name "-hiccup"))])
  75. (declare page)
  76. (rum/defc dummy-block
  77. [page-name]
  78. (let [handler-fn (fn []
  79. (let [block (editor-handler/insert-first-page-block-if-not-exists! page-name :check-empty-page? false)]
  80. (js/setTimeout #(editor-handler/edit-block! block :max (:block/uuid block)) 0)))]
  81. [:div.ls-block.flex-1.flex-col.rounded-sm {:style {:width "100%"}}
  82. [:div.flex.flex-row
  83. [:div.flex.flex-row.items-center.mr-2.ml-1 {:style {:height 24}}
  84. [:span.bullet-container.cursor
  85. [:span.bullet]]]
  86. [:div.flex.flex-1 {:tabindex 0
  87. :on-key-press (fn [e]
  88. (when (= "Enter" (util/ekey e))
  89. (handler-fn)))
  90. :on-click handler-fn}
  91. [:span.opacity-50
  92. "Click here to edit..."]]]]))
  93. (rum/defc add-button
  94. [args]
  95. [:div.flex-1.flex-col.rounded-sm.add-button-link-wrap
  96. {:on-click (fn [] (editor-handler/api-insert-new-block! "" args))}
  97. [:div.flex.flex-row
  98. [:div.block {:style {:height 20
  99. :width 20
  100. :margin-left 2}}
  101. [:a.add-button-link.block
  102. (ui/icon "circle-plus")]]]])
  103. (rum/defc page-blocks-cp < rum/reactive
  104. db-mixins/query
  105. [repo page-e {:keys [sidebar?] :as config}]
  106. (when page-e
  107. (let [page-name (or (:block/name page-e)
  108. (str (:block/uuid page-e)))
  109. block? (util/uuid-string? page-name)
  110. block-id (and block? (uuid page-name))
  111. page-blocks (get-blocks repo page-name block-id)]
  112. (if (empty? page-blocks)
  113. (dummy-block page-name)
  114. (let [document-mode? (state/sub :document/mode?)
  115. block-entity (db/entity (if block-id
  116. [:block/uuid block-id]
  117. [:block/name page-name]))
  118. hiccup-config (merge
  119. {:id (if block? (str block-id) page-name)
  120. :db/id (:db/id block-entity)
  121. :block? block?
  122. :editor-box editor/box
  123. :document/mode? document-mode?}
  124. config)
  125. hiccup-config (common-handler/config-with-document-mode hiccup-config)
  126. hiccup (block/->hiccup page-blocks hiccup-config {})]
  127. [:div
  128. (page-blocks-inner page-name page-blocks hiccup sidebar? block-id)
  129. (when-not config/publishing?
  130. (let [args (if block-id
  131. {:block-uuid block-id}
  132. {:page page-name})]
  133. (add-button args)))])))))
  134. (defn contents-page
  135. [page]
  136. (when-let [repo (state/get-current-repo)]
  137. (page-blocks-cp repo page {:sidebar? true})))
  138. (rum/defc today-queries < rum/reactive
  139. [repo today? sidebar?]
  140. (when (and today? (not sidebar?))
  141. (let [queries (state/sub [:config repo :default-queries :journals])]
  142. (when (seq queries)
  143. [:div#today-queries.mt-10
  144. (for [query queries]
  145. (rum/with-key
  146. (ui/catch-error
  147. (ui/component-error "Failed default query:" {:content (pr-str query)})
  148. (block/custom-query {:attr {:class "mt-10"}
  149. :editor-box editor/box
  150. :page page} query))
  151. (str repo "-custom-query-" (:query query))))]))))
  152. (defn tagged-pages
  153. [repo tag]
  154. (let [pages (db/get-tag-pages repo tag)]
  155. (when (seq pages)
  156. [:div.references.mt-6.flex-1.flex-row
  157. [:div.content
  158. (ui/foldable
  159. [:h2.font-bold.opacity-50 (util/format "Pages tagged with \"%s\"" tag)]
  160. [:ul.mt-2
  161. (for [[original-name name] (sort pages)]
  162. [:li {:key (str "tagged-page-" name)}
  163. [:a {:href (rfe/href :page {:name name})}
  164. original-name]])]
  165. {:default-collapsed? false})]])))
  166. (rum/defcs page-title <
  167. (rum/local false ::edit?)
  168. {:init (fn [state]
  169. (assoc state ::title-value (atom (nth (:rum/args state) 2))))}
  170. [state page-name icon title _format fmt-journal?]
  171. (when title
  172. (let [*title-value (get state ::title-value)
  173. *edit? (get state ::edit?)
  174. input-ref (rum/create-ref)
  175. repo (state/get-current-repo)
  176. hls-file? (pdf-assets/hls-file? title)
  177. title (if hls-file?
  178. (pdf-assets/human-hls-filename-display title)
  179. (if fmt-journal? (date/journal-title->custom-format title) title))
  180. old-name (or title page-name)
  181. confirm-fn (fn []
  182. (let [new-page-name (string/trim @*title-value)
  183. merge? (and (not= (util/page-name-sanity-lc page-name)
  184. (util/page-name-sanity-lc @*title-value))
  185. (page-handler/page-exists? page-name)
  186. (page-handler/page-exists? @*title-value))]
  187. (ui/make-confirm-modal
  188. {:title (if merge?
  189. (str "Page “" @*title-value "” already exists, merge to it?")
  190. (str "Do you really want to change the page name to “" new-page-name "”?"))
  191. :on-confirm (fn [_e {:keys [close-fn]}]
  192. (close-fn)
  193. (page-handler/rename! (or title page-name) @*title-value)
  194. (reset! *edit? false))
  195. :on-cancel (fn []
  196. (reset! *title-value old-name)
  197. (gobj/set (rum/deref input-ref) "value" old-name)
  198. (reset! *edit? true))})))
  199. rollback-fn #(do
  200. (reset! *title-value old-name)
  201. (gobj/set (rum/deref input-ref) "value" old-name)
  202. (reset! *edit? false)
  203. (notification/show! "Illegal page name, can not rename!" :warning))
  204. blur-fn (fn [e]
  205. (when (util/wrapped-by-quotes? @*title-value)
  206. (swap! *title-value util/unquote-string)
  207. (gobj/set (rum/deref input-ref) "value" @*title-value))
  208. (state/set-state! :editor/editing-page-title? false)
  209. (cond
  210. (= old-name @*title-value)
  211. (reset! *edit? false)
  212. (string/blank? @*title-value)
  213. (rollback-fn)
  214. :else
  215. (state/set-modal! (confirm-fn)))
  216. (util/stop e))]
  217. (if @*edit?
  218. [:h1.title.ls-page-title
  219. {:class (util/classnames [{:editing @*edit?}])}
  220. [:input.edit-input
  221. {:type "text"
  222. :ref input-ref
  223. :auto-focus true
  224. :style {:outline "none"
  225. :font-weight 600}
  226. :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
  227. :default-value old-name
  228. :on-change (fn [^js e]
  229. (let [value (util/evalue e)]
  230. (reset! *title-value (string/trim value))))
  231. :on-blur blur-fn
  232. :on-key-down (fn [^js e]
  233. (when (= (gobj/get e "key") "Enter")
  234. (blur-fn e)))
  235. :on-key-up (fn [^js e]
  236. ;; Esc
  237. (when (= 27 (.-keyCode e))
  238. (reset! *title-value old-name)
  239. (reset! *edit? false)))}]]
  240. [:a.page-title {:on-mouse-down (fn [e]
  241. (state/set-state! :editor/editing-page-title? true)
  242. (when (util/right-click? e)
  243. (state/set-state! :page-title/context {:page page-name})))
  244. :on-click (fn [e]
  245. (.preventDefault e)
  246. (if (gobj/get e "shiftKey")
  247. (when-let [page (db/pull repo '[*] [:block/name page-name])]
  248. (state/sidebar-add-block!
  249. repo
  250. (:db/id page)
  251. :page
  252. {:page page}))
  253. (when (and (not hls-file?) (not fmt-journal?))
  254. (reset! *edit? true))))}
  255. [:h1.title.ls-page-title {:data-ref page-name}
  256. (when (not= icon "") [:span.page-icon icon])
  257. title]]))))
  258. (defn- page-mouse-over
  259. [e *control-show? *all-collapsed?]
  260. (util/stop e)
  261. (reset! *control-show? true)
  262. (let [all-collapsed?
  263. (->> (editor-handler/all-blocks-with-level {:collapse? true})
  264. (filter (fn [b] (editor-handler/collapsable? (:block/uuid b))))
  265. (empty?))]
  266. (reset! *all-collapsed? all-collapsed?)))
  267. (defn- page-mouse-leave
  268. [e *control-show?]
  269. (util/stop e)
  270. (reset! *control-show? false))
  271. (rum/defcs page-blocks-collapse-control <
  272. [state title *control-show? *all-collapsed?]
  273. [:a.page-blocks-collapse-control
  274. {:id (str "control-" title)
  275. :on-click (fn [event]
  276. (util/stop event)
  277. (if @*all-collapsed?
  278. (editor-handler/expand-all!)
  279. (editor-handler/collapse-all!))
  280. (swap! *all-collapsed? not))}
  281. [:span.mt-6 {:class (if @*control-show?
  282. "control-show cursor-pointer" "control-hide")}
  283. (ui/rotating-arrow @*all-collapsed?)]])
  284. ;; A page is just a logical block
  285. (rum/defcs page < rum/reactive
  286. (rum/local false ::all-collapsed?)
  287. (rum/local false ::control-show?)
  288. [state {:keys [repo page-name] :as option}]
  289. (when-let [path-page-name (or page-name
  290. (get-page-name state)
  291. (state/get-current-page))]
  292. (let [current-repo (state/sub :git/current-repo)
  293. repo (or repo current-repo)
  294. page-name (util/page-name-sanity-lc path-page-name)
  295. block? (util/uuid-string? page-name)
  296. block-id (and block? (uuid page-name))
  297. format (let [page (if block-id
  298. (:block/name (:block/page (db/entity [:block/uuid block-id])))
  299. page-name)]
  300. (db/get-page-format page))
  301. journal? (db/journal-page? page-name)
  302. fmt-journal? (boolean (date/journal-title->int page-name))
  303. sidebar? (:sidebar? option)
  304. route-page-name path-page-name
  305. page (if block?
  306. (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
  307. (db/entity repo))
  308. (do
  309. (when-not (db/entity repo [:block/name page-name])
  310. (let [m (format-block/page-name->map path-page-name true)]
  311. (db/transact! repo [m])))
  312. (db/pull [:block/name page-name])))
  313. {:keys [icon]} (:block/properties page)
  314. page-name (:block/name page)
  315. page-original-name (:block/original-name page)
  316. title (or page-original-name page-name)
  317. icon (or icon "")
  318. today? (and
  319. journal?
  320. (= page-name (util/page-name-sanity-lc (date/journal-name))))
  321. *control-show? (::control-show? state)
  322. *all-collapsed? (::all-collapsed? state)]
  323. [:div.flex-1.page.relative
  324. (merge (if (seq (:block/tags page))
  325. (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
  326. {:data-page-tags (text/build-data-value page-names)})
  327. {})
  328. {:key path-page-name
  329. :class (util/classnames [{:is-journals (or journal? fmt-journal?)}])})
  330. [:div.relative
  331. (when (and (not sidebar?) (not block?))
  332. [:div.flex.flex-row.space-between
  333. (when (or (mobile-util/is-native-platform?) (util/mobile?))
  334. [:div.flex.flex-row.pr-2
  335. {:style {:margin-left -15}
  336. :on-mouse-over (fn [e]
  337. (page-mouse-over e *control-show? *all-collapsed?))
  338. :on-mouse-leave (fn [e]
  339. (page-mouse-leave e *control-show?))}
  340. (page-blocks-collapse-control title *control-show? *all-collapsed?)])
  341. [:div.flex-1.flex-row
  342. (page-title page-name icon title format fmt-journal?)]
  343. (when (not config/publishing?)
  344. [:div.flex.flex-row
  345. (when plugin-handler/lsp-enabled?
  346. (plugins/hook-ui-slot :page-head-actions-slotted nil)
  347. (plugins/hook-ui-items :pagebar))])])
  348. [:div
  349. (when (and block? (not sidebar?))
  350. (let [config {:id "block-parent"
  351. :block? true}]
  352. [:div.mb-4
  353. (block/block-parents config repo block-id {:level-limit 3})]))
  354. ;; blocks
  355. (let [page (if block?
  356. (db/entity repo [:block/uuid block-id])
  357. page)]
  358. (page-blocks-cp repo page {:sidebar? sidebar?}))]]
  359. (when-not block?
  360. (today-queries repo today? sidebar?))
  361. (when-not block?
  362. (tagged-pages repo page-name))
  363. ;; referenced blocks
  364. [:div {:key "page-references"}
  365. (rum/with-key
  366. (reference/references route-page-name)
  367. (str route-page-name "-refs"))]
  368. (when-not block?
  369. [:div
  370. (when (not journal?)
  371. (hierarchy/structures route-page-name))
  372. ;; TODO: or we can lazy load them
  373. (when-not sidebar?
  374. [:div {:key "page-unlinked-references"}
  375. (reference/unlinked-references route-page-name)])])])))
  376. (defonce layout (atom [js/window.innerWidth js/window.innerHeight]))
  377. ;; scrollHeight
  378. (rum/defcs graph-filter-section < (rum/local false ::open?)
  379. [state title content {:keys [search-filters]}]
  380. (let [open? (get state ::open?)]
  381. (when (and (seq search-filters) (not @open?))
  382. (reset! open? true))
  383. [:li.relative
  384. [:div
  385. [:button.w-full.px-4.py-2.text-left.focus:outline-none {:on-click #(swap! open? not)}
  386. [:div.flex.items-center.justify-between
  387. title
  388. (if @open? (svg/caret-down) (svg/caret-right))]]
  389. (content open?)]]))
  390. (rum/defc filter-expand-area
  391. [open? content]
  392. [:div.relative.overflow-hidden.transition-all.max-h-0.duration-700
  393. {:style {:max-height (if @open? 400 0)}}
  394. content])
  395. (defonce *n-hops (atom nil))
  396. (defonce *focus-nodes (atom []))
  397. (defonce *graph-reset? (atom false))
  398. (defonce *journal? (atom nil))
  399. (defonce *orphan-pages? (atom true))
  400. (defonce *builtin-pages? (atom nil))
  401. (rum/defc ^:large-vars/cleanup-todo graph-filters < rum/reactive
  402. [graph settings n-hops]
  403. (let [{:keys [journal? orphan-pages? builtin-pages?]
  404. :or {orphan-pages? true}} settings
  405. journal?' (rum/react *journal?)
  406. orphan-pages?' (rum/react *orphan-pages?)
  407. builtin-pages?' (rum/react *builtin-pages?)
  408. journal? (if (nil? journal?') journal? journal?')
  409. orphan-pages? (if (nil? orphan-pages?') orphan-pages? orphan-pages?')
  410. builtin-pages? (if (nil? builtin-pages?') builtin-pages? builtin-pages?')
  411. set-setting! (fn [key value]
  412. (let [new-settings (assoc settings key value)]
  413. (config-handler/set-config! :graph/settings new-settings)))
  414. search-graph-filters (state/sub :search/graph-filters)
  415. focus-nodes (rum/react *focus-nodes)]
  416. [:div.absolute.top-4.right-4.graph-filters
  417. [:div.flex.flex-col
  418. [:div.shadow-xl.rounded-sm
  419. [:ul
  420. (graph-filter-section
  421. [:span.font-medium "Nodes"]
  422. (fn [open?]
  423. (filter-expand-area
  424. open?
  425. [:div
  426. [:p.text-sm.opacity-70.px-4
  427. (let [c1 (count (:nodes graph))
  428. s1 (if (> c1 1) "s" "")
  429. ;; c2 (count (:links graph))
  430. ;; s2 (if (> c2 1) "s" "")
  431. ]
  432. ;; (util/format "%d page%s, %d link%s" c1 s1 c2 s2)
  433. (util/format "%d page%s" c1 s1))]
  434. [:div.p-6
  435. ;; [:div.flex.items-center.justify-between.mb-2
  436. ;; [:span "Layout"]
  437. ;; (ui/select
  438. ;; (mapv
  439. ;; (fn [item]
  440. ;; (if (= (:label item) layout)
  441. ;; (assoc item :selected "selected")
  442. ;; item))
  443. ;; [{:label "gForce"}
  444. ;; {:label "dagre"}])
  445. ;; (fn [value]
  446. ;; (set-setting! :layout value))
  447. ;; "graph-layout")]
  448. [:div.flex.items-center.justify-between.mb-2
  449. [:span (t :settings-page/enable-journals)]
  450. ;; FIXME: why it's not aligned well?
  451. [:div.mt-1
  452. (ui/toggle journal?
  453. (fn []
  454. (let [value (not journal?)]
  455. (reset! *journal? value)
  456. (set-setting! :journal? value)))
  457. true)]]
  458. [:div.flex.items-center.justify-between.mb-2
  459. [:span "Orphan pages"]
  460. [:div.mt-1
  461. (ui/toggle orphan-pages?
  462. (fn []
  463. (let [value (not orphan-pages?)]
  464. (reset! *orphan-pages? value)
  465. (set-setting! :orphan-pages? value)))
  466. true)]]
  467. [:div.flex.items-center.justify-between.mb-2
  468. [:span "Built-in pages"]
  469. [:div.mt-1
  470. (ui/toggle builtin-pages?
  471. (fn []
  472. (let [value (not builtin-pages?)]
  473. (reset! *builtin-pages? value)
  474. (set-setting! :builtin-pages? value)))
  475. true)]]
  476. (when (seq focus-nodes)
  477. [:div.flex.flex-col.mb-2
  478. [:p {:title "N hops from selected nodes"}
  479. "N hops from selected nodes"]
  480. (ui/tippy {:html [:div.pr-3 n-hops]}
  481. (ui/slider (or n-hops 10)
  482. {:min 1
  483. :max 10
  484. :on-change #(reset! *n-hops (int %))}))])
  485. [:a.opacity-70.opacity-100 {:on-click (fn []
  486. (swap! *graph-reset? not)
  487. (reset! *focus-nodes [])
  488. (reset! *n-hops nil)
  489. (state/clear-search-filters!))}
  490. "Reset Graph"]]]))
  491. {})
  492. (graph-filter-section
  493. [:span.font-medium "Search"]
  494. (fn [open?]
  495. (filter-expand-area
  496. open?
  497. [:div.p-6
  498. (if (seq search-graph-filters)
  499. [:div
  500. (for [q search-graph-filters]
  501. [:div.flex.flex-row.justify-between.items-center.mb-2
  502. [:span.font-medium q]
  503. [:a.search-filter-close.opacity-70.opacity-100 {:on-click #(state/remove-search-filter! q)}
  504. svg/close]])
  505. [:a.opacity-70.opacity-100 {:on-click state/clear-search-filters!}
  506. "Clear All"]]
  507. [:a.opacity-70.opacity-100 {:on-click #(route-handler/go-to-search! :graph)}
  508. "Click to search"])]))
  509. {:search-filters search-graph-filters})]]]]))
  510. (defonce last-node-position (atom nil))
  511. (defn- graph-register-handlers
  512. [graph focus-nodes n-hops dark?]
  513. (.on graph "nodeClick"
  514. (fn [event node]
  515. (let [x (.-x event)
  516. y (.-y event)
  517. drag? (not= [node x y] @last-node-position)]
  518. (graph/on-click-handler graph node event focus-nodes n-hops drag? dark?))))
  519. (.on graph "nodeMousedown"
  520. (fn [event node]
  521. (reset! last-node-position [node (.-x event) (.-y event)]))))
  522. (rum/defc global-graph-inner < rum/reactive
  523. [graph settings theme]
  524. (let [[width height] (rum/react layout)
  525. dark? (= theme "dark")
  526. n-hops (rum/react *n-hops)
  527. reset? (rum/react *graph-reset?)
  528. focus-nodes (when n-hops (rum/react *focus-nodes))
  529. graph (if (and (integer? n-hops)
  530. (seq focus-nodes)
  531. (not (:orphan-pages? settings)))
  532. (graph-handler/n-hops graph focus-nodes n-hops)
  533. graph)
  534. graph (update graph :links (fn [links]
  535. (let [nodes (set (map :id (:nodes graph)))]
  536. (remove (fn [link]
  537. (and (not (nodes (:source link)))
  538. (not (nodes (:target link)))))
  539. links))))]
  540. [:div.relative#global-graph
  541. (graph/graph-2d {:nodes (:nodes graph)
  542. :links (:links graph)
  543. :width (- width 24)
  544. :height (- height 48)
  545. :dark? dark?
  546. :register-handlers-fn
  547. (fn [graph]
  548. (graph-register-handlers graph *focus-nodes *n-hops dark?))
  549. :reset? reset?})
  550. (graph-filters graph settings n-hops)]))
  551. (defn- filter-graph-nodes
  552. [nodes filters]
  553. (if (seq filters)
  554. (let [filter-patterns (map #(re-pattern (str "(?i)" (util/regex-escape %))) filters)]
  555. (filter (fn [node] (some #(re-find % (:id node)) filter-patterns)) nodes))
  556. nodes))
  557. (rum/defcs global-graph < rum/reactive
  558. (mixins/event-mixin
  559. (fn [state]
  560. (mixins/listen state js/window "resize"
  561. (fn [_e]
  562. (reset! layout [js/window.innerWidth js/window.innerHeight])))))
  563. {:will-mount (fn [state]
  564. (state/set-search-mode! :graph)
  565. state)
  566. :will-unmount (fn [state]
  567. (reset! *n-hops nil)
  568. (reset! *focus-nodes [])
  569. (state/set-search-mode! :global)
  570. state)}
  571. [state]
  572. (let [settings (state/sub-graph-config-settings)
  573. theme (state/sub :ui/theme)
  574. graph (graph-handler/build-global-graph theme settings)
  575. search-graph-filters (state/sub :search/graph-filters)
  576. graph (update graph :nodes #(filter-graph-nodes % search-graph-filters))]
  577. (global-graph-inner graph settings theme)))
  578. (rum/defc page-graph-inner < rum/static
  579. [_page graph dark?]
  580. [:div.sidebar-item.flex-col
  581. (graph/graph-2d {:nodes (:nodes graph)
  582. :links (:links graph)
  583. :width 600
  584. :height 600
  585. :dark? dark?
  586. :register-handlers-fn
  587. (fn [graph]
  588. (graph-register-handlers graph (atom nil) (atom nil) dark?))})])
  589. (rum/defc page-graph < db-mixins/query rum/reactive
  590. []
  591. (let [page (or
  592. (and (= :page (state/sub [:route-match :data :name]))
  593. (state/sub [:route-match :path-params :name]))
  594. (date/today))
  595. theme (:ui/theme @state/state)
  596. dark? (= theme "dark")
  597. graph (if (util/uuid-string? page)
  598. (graph-handler/build-block-graph (uuid page) theme)
  599. (graph-handler/build-page-graph page theme))]
  600. (when (seq (:nodes graph))
  601. (page-graph-inner page graph dark?))))
  602. (defn- sort-pages-by
  603. [by-item desc? pages]
  604. (let [comp (if desc? > <)
  605. by-item (if (= by-item :block/name)
  606. (fn [x] (string/lower-case (:block/name x)))
  607. by-item)]
  608. (sort-by by-item comp pages)))
  609. (rum/defc checkbox-opt
  610. [key checked opts]
  611. (let [*input (rum/create-ref)
  612. indeterminate? (boolean (:indeterminate opts))]
  613. (rum/use-effect!
  614. #(set! (.-indeterminate (rum/deref *input)) indeterminate?)
  615. [indeterminate?])
  616. [:label {:for key}
  617. [:input.form-checkbox
  618. (merge {:type "checkbox"
  619. :checked (boolean checked)
  620. :ref *input
  621. :id key} opts)]]))
  622. (rum/defc sortable-title
  623. [title key by-item desc?]
  624. [:th
  625. {:class [(name key)]}
  626. [:a {:on-click (fn []
  627. (reset! by-item key)
  628. (swap! desc? not))}
  629. [:span.flex.items-center
  630. [:span.mr-1 title]
  631. (when (= @by-item key)
  632. [:span
  633. (if @desc? (svg/caret-down) (svg/caret-up))])]]])
  634. (defn batch-delete-dialog
  635. [pages orphaned-pages? refresh-fn]
  636. (fn [close-fn]
  637. [:div
  638. [:div.sm:flex.items-center
  639. [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-red-100.sm:mx-0.sm:h-10.sm:w-10
  640. [:span.text-red-600.text-xl
  641. (ui/icon "alert-triangle")]]
  642. [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
  643. [:h3#modal-headline.text-lg.leading-6.font-medium
  644. (if orphaned-pages?
  645. (str (t :remove-orphaned-pages) "?")
  646. (t :page/delete-confirmation))]]]
  647. [:table.table-auto.cp__all_pages_table.mt-4
  648. [:thead
  649. [:tr.opacity-70
  650. [:th [:span "#"]]
  651. [:th [:span (t :block/name)]]
  652. [:th [:span (t :page/backlinks)]]
  653. (when-not orphaned-pages? [:th [:span (t :page/created-at)]])
  654. (when-not orphaned-pages? [:th [:span (t :page/updated-at)]])]]
  655. [:tbody
  656. (for [[n {:block/keys [name created-at updated-at backlinks] :as page}] (medley/indexed pages)]
  657. [:tr {:key name}
  658. [:td.n.w-12 [:span.opacity-70 (str (inc n) ".")]]
  659. [:td.name [:a {:href (rfe/href :page {:name (:block/name page)})}
  660. (block/page-cp {} page)]]
  661. [:td.backlinks [:span (or backlinks "0")]]
  662. (when-not orphaned-pages? [:td.created-at [:span (if created-at (date/int->local-time-2 created-at) "Unknown")]])
  663. (when-not orphaned-pages? [:td.updated-at [:span (if updated-at (date/int->local-time-2 updated-at) "Unknown")]])])]]
  664. [:div.pt-6.flex.justify-end
  665. [:span.pr-2
  666. (ui/button
  667. (t :cancel)
  668. :intent "logseq"
  669. :on-click close-fn)]
  670. (ui/button
  671. (t :yes)
  672. :on-click (fn []
  673. (close-fn)
  674. (doseq [page-name (map :block/name pages)]
  675. (page-handler/delete! page-name #()))
  676. (notification/show! (str (t :tips/all-done) "!") :success)
  677. (js/setTimeout #(refresh-fn) 200)))]]))
  678. (rum/defcs ^:large-vars/cleanup-todo all-pages < rum/reactive
  679. (rum/local nil ::pages)
  680. (rum/local nil ::search-key)
  681. (rum/local nil ::results-all)
  682. (rum/local nil ::results)
  683. (rum/local {} ::checks)
  684. (rum/local :block/updated-at ::sort-by-item)
  685. (rum/local true ::desc?)
  686. (rum/local false ::journals)
  687. (rum/local nil ::filter-fn)
  688. (rum/local 1 ::current-page)
  689. ;; {:did-mount (fn [state]
  690. ;; (let [current-repo (state/sub :git/current-repo)]
  691. ;; (js/setTimeout #(db/remove-orphaned-pages! current-repo) 0))
  692. ;; state)}
  693. [state]
  694. (let [current-repo (state/sub :git/current-repo)
  695. per-page-num 40
  696. *sort-by-item (get state ::sort-by-item)
  697. *desc? (::desc? state)
  698. *journal? (::journals state)
  699. *results (::results state)
  700. *results-all (::results-all state)
  701. *checks (::checks state)
  702. *pages (::pages state)
  703. *current-page (::current-page state)
  704. *filter-fn (::filter-fn state)
  705. *search-key (::search-key state)
  706. *search-input (rum/create-ref)
  707. *indeterminate (rum/derived-atom
  708. [*checks] ::indeterminate
  709. (fn [checks]
  710. (when-let [checks (vals checks)]
  711. (if (every? true? checks)
  712. 1 (if (some true? checks) -1 0)))))
  713. mobile? (util/mobile?)
  714. total-pages (if-not @*results-all 0
  715. (js/Math.ceil (/ (count @*results-all) per-page-num)))
  716. to-page (fn [page]
  717. (when (> total-pages 1)
  718. (if (and (> page 0)
  719. (<= page total-pages))
  720. (reset! *current-page page)
  721. (reset! *current-page 1))
  722. (js/setTimeout #(util/scroll-to-top))))
  723. search-key (fn [key]
  724. (when-let [key (and key (string/trim key))]
  725. (if (and (not (string/blank? key))
  726. (seq @*results))
  727. (reset! *search-key key)
  728. (reset! *search-key nil))))
  729. refresh-pages #(do
  730. (reset! *pages nil)
  731. (reset! *current-page 1))]
  732. [:div.flex-1.cp__all_pages
  733. [:h1.title (t :all-pages)]
  734. (when current-repo
  735. ;; all pages
  736. (when (nil? @*pages)
  737. (let [pages (->> (page-handler/get-all-pages current-repo)
  738. (map-indexed (fn [idx page] (assoc page
  739. :block/backlinks (count (:block/_refs (db/entity (:db/id page))))
  740. :block/idx idx))))]
  741. (reset! *filter-fn
  742. (memoize (fn [sort-by-item desc? journal?]
  743. (->> pages
  744. (filter #(or (boolean journal?)
  745. (= false (boolean (:block/journal? %)))))
  746. (sort-pages-by sort-by-item desc?)))))
  747. (reset! *pages pages)))
  748. ;; filter results
  749. (when @*filter-fn
  750. (let [pages (@*filter-fn @*sort-by-item @*desc? @*journal?)
  751. ;; search key
  752. pages (if-not (string/blank? @*search-key)
  753. (search/fuzzy-search pages (util/page-name-sanity-lc @*search-key)
  754. :limit 20
  755. :extract-fn :block/name)
  756. pages)
  757. _ (reset! *results-all pages)
  758. pages (take per-page-num (drop (* per-page-num (dec @*current-page)) pages))]
  759. (reset! *checks (into {} (for [{:block/keys [idx]} pages]
  760. [idx (boolean (get @*checks idx))])))
  761. (reset! *results pages)))
  762. [[:div.actions
  763. {:class (util/classnames [{:has-selected (or (nil? @*indeterminate)
  764. (not= 0 @*indeterminate))}])}
  765. [:div.l.flex.items-center
  766. [:div.actions-wrap
  767. (ui/button
  768. [(ui/icon "trash") (t :delete)]
  769. :on-click (fn []
  770. (let [selected (filter (fn [[_ v]] v) @*checks)
  771. selected (and (seq selected)
  772. (into #{} (for [[k _] selected] k)))]
  773. (when-let [pages (and selected (filter #(contains? selected (:block/idx %)) @*results))]
  774. (state/set-modal! (batch-delete-dialog pages false #(do
  775. (reset! *checks nil)
  776. (refresh-pages)))))))
  777. :small? true)]
  778. [:div.search-wrap.flex.items-center.pl-2
  779. (let [search-fn (fn []
  780. (let [^js input (rum/deref *search-input)]
  781. (search-key (.-value input))
  782. (reset! *current-page 1)))
  783. reset-fn (fn []
  784. (let [^js input (rum/deref *search-input)]
  785. (set! (.-value input) "")
  786. (reset! *search-key nil)))]
  787. [(ui/button (ui/icon "search")
  788. :on-click search-fn
  789. :small? true)
  790. [:input.form-input {:placeholder (t :search/page-names)
  791. :on-key-up (fn [^js e]
  792. (let [^js target (.-target e)]
  793. (if (string/blank? (.-value target))
  794. (reset! *search-key nil)
  795. (cond
  796. (= 13 (.-keyCode e)) (search-fn)
  797. (= 27 (.-keyCode e)) (reset-fn)))))
  798. :ref *search-input
  799. :default-value ""}]
  800. (when (not (string/blank? @*search-key))
  801. [:a.cancel {:on-click reset-fn}
  802. (ui/icon "x")])])]]
  803. [:div.r.flex.items-center.justify-between
  804. (let [orphaned-pages (model/get-orphaned-pages {})
  805. orphaned-pages? (seq orphaned-pages)]
  806. [:a.ml-1.pr-2.opacity-70.hover:opacity-100
  807. {:on-click (fn []
  808. (if orphaned-pages?
  809. (state/set-modal!
  810. (batch-delete-dialog
  811. orphaned-pages true
  812. #(do
  813. (reset! *checks nil)
  814. (refresh-pages))))
  815. (notification/show! "Congratulations, no orphaned pages in your graph!" :success)))}
  816. [:span
  817. (ui/icon "file-x")
  818. [:span.ml-1 (t :remove-orphaned-pages)]]])
  819. [:a.ml-1.pr-2.opacity-70.hover:opacity-100 {:href (rfe/href :all-files)}
  820. [:span
  821. (ui/icon "files")
  822. [:span.ml-1 (t :all-files)]]]
  823. [:div
  824. (ui/tippy
  825. {:html [:small (str (t :page/show-journals) " ?")]
  826. :arrow true}
  827. [:a.button.journal
  828. {:class (util/classnames [{:active (boolean @*journal?)}])
  829. :on-click #(reset! *journal? (not @*journal?))}
  830. (ui/icon "calendar")])]
  831. [:div.paginates
  832. [:span.flex.items-center.opacity-60.text-sm
  833. [:span.pr-1 (t :paginates/pages (count @*results-all))]]
  834. [:span.flex.items-center
  835. {:class (util/classnames [{:is-first (= 1 @*current-page)
  836. :is-last (= @*current-page total-pages)}])}
  837. [:a.py-4.pr-2 {:on-click #(to-page (dec @*current-page))} (ui/icon "caret-left") (str " " (t :paginates/prev))]
  838. [:span.opacity-30 (str @*current-page "/" total-pages)]
  839. [:a.py-4.pl-2 {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ") (ui/icon "caret-right")]]]]]
  840. [:table.table-auto.cp__all_pages_table
  841. [:thead
  842. [:tr
  843. [:th.selector
  844. (checkbox-opt "all-pages-select-all"
  845. (= 1 @*indeterminate)
  846. {:on-change (fn []
  847. (let [indeterminate? (= -1 @*indeterminate)
  848. all? (= 1 @*indeterminate)]
  849. (doseq [{:block/keys [idx]} @*results]
  850. (swap! *checks assoc idx (or indeterminate? (not all?))))))
  851. :indeterminate (= -1 @*indeterminate)})]
  852. (sortable-title (t :block/name) :block/name *sort-by-item *desc?)
  853. (when-not mobile?
  854. [(sortable-title (t :page/backlinks) :block/backlinks *sort-by-item *desc?)
  855. (sortable-title (t :page/created-at) :block/created-at *sort-by-item *desc?)
  856. (sortable-title (t :page/updated-at) :block/updated-at *sort-by-item *desc?)])]]
  857. [:tbody
  858. (for [{:block/keys [idx name created-at updated-at backlinks] :as page} @*results]
  859. (when-not (string/blank? name)
  860. [:tr {:key name}
  861. [:td.selector
  862. (checkbox-opt (str "label-" idx)
  863. (get @*checks idx)
  864. {:on-change (fn []
  865. (swap! *checks update idx not))})]
  866. [:td.name [:a {:on-click (fn [e]
  867. (let [repo (state/get-current-repo)]
  868. (when (gobj/get e "shiftKey")
  869. (state/sidebar-add-block!
  870. repo
  871. (:db/id page)
  872. :page
  873. {:page (:block/name page)}))))
  874. :href (rfe/href :page {:name (:block/name page)})}
  875. (block/page-cp {} page)]]
  876. (when-not mobile?
  877. [:td.backlinks [:span backlinks]])
  878. (when-not mobile?
  879. [:td.created-at [:span (if created-at
  880. (date/int->local-time-2 created-at)
  881. "Unknown")]])
  882. (when-not mobile?
  883. [:td.updated-at [:span (if updated-at
  884. (date/int->local-time-2 updated-at)
  885. "Unknown")]])]))]]
  886. [:div.paginates
  887. [:span]
  888. [:span.flex.items-center
  889. {:class (util/classnames [{:is-first (= 1 @*current-page)
  890. :is-last (= @*current-page total-pages)}])}
  891. [:a.py-4.text-sm {:on-click #(to-page (dec @*current-page))} (ui/icon "caret-left") (str " " (t :paginates/prev))]
  892. [:a.py-4.pl-2.text-sm {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ") (ui/icon "caret-right")]]]])]))