page.cljs 47 KB

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