page.cljs 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367
  1. (ns frontend.components.page
  2. (:require ["/frontend/utils" :as utils]
  3. [clojure.string :as string]
  4. [frontend.components.block :as component-block]
  5. [frontend.components.content :as content]
  6. [frontend.components.editor :as editor]
  7. [frontend.components.plugins :as plugins]
  8. [frontend.components.query :as query]
  9. [frontend.components.reference :as reference]
  10. [frontend.components.scheduled-deadlines :as scheduled]
  11. [frontend.components.icon :as icon-component]
  12. [frontend.components.db-based.page :as db-page]
  13. [frontend.components.class :as class-component]
  14. [frontend.handler.db-based.property :as db-property-handler]
  15. [frontend.components.svg :as svg]
  16. [frontend.config :as config]
  17. [frontend.context.i18n :refer [t]]
  18. [frontend.date :as date]
  19. [frontend.db :as db]
  20. [frontend.db-mixins :as db-mixins]
  21. [frontend.db.model :as model]
  22. [frontend.extensions.graph :as graph]
  23. [frontend.extensions.pdf.utils :as pdf-utils]
  24. [frontend.format.mldoc :as mldoc]
  25. [frontend.handler.common :as common-handler]
  26. [frontend.handler.config :as config-handler]
  27. [frontend.handler.dnd :as dnd]
  28. [frontend.handler.editor :as editor-handler]
  29. [frontend.handler.graph :as graph-handler]
  30. [frontend.handler.notification :as notification]
  31. [frontend.handler.page :as page-handler]
  32. [frontend.handler.route :as route-handler]
  33. [frontend.mixins :as mixins]
  34. [frontend.mobile.util :as mobile-util]
  35. [frontend.search :as search]
  36. [frontend.state :as state]
  37. [frontend.ui :as ui]
  38. [logseq.shui.ui :as shui]
  39. [frontend.util :as util]
  40. [frontend.util.text :as text-util]
  41. [goog.object :as gobj]
  42. [logseq.graph-parser.mldoc :as gp-mldoc]
  43. [logseq.common.util :as common-util]
  44. [logseq.common.util.page-ref :as page-ref]
  45. [medley.core :as medley]
  46. [promesa.core :as p]
  47. [reitit.frontend.easy :as rfe]
  48. [rum.core :as rum]
  49. [frontend.extensions.graph.pixi :as pixi]
  50. [frontend.db.async :as db-async]
  51. [logseq.db :as ldb]
  52. [frontend.handler.property.util :as pu]
  53. [frontend.components.hierarchy :as hierarchy]))
  54. (defn- get-page-name
  55. [state]
  56. (let [route-match (first (:rum/args state))]
  57. (get-in route-match [:parameters :path :name])))
  58. ;; Named block links only works on web (and publishing)
  59. (if util/web-platform?
  60. (defn- get-block-uuid-by-block-route-name
  61. "Return string block uuid for matching :name and :block-route-name params or
  62. nil if not found"
  63. [state]
  64. ;; Only query if block name is in the route
  65. (when-let [route-name (get-in (first (:rum/args state))
  66. [:parameters :path :block-route-name])]
  67. (->> (model/get-block-by-page-name-and-block-route-name
  68. (state/get-current-repo)
  69. (get-page-name state)
  70. route-name)
  71. :block/uuid
  72. str)))
  73. (def get-block-uuid-by-block-route-name (constantly nil)))
  74. (defn- get-block
  75. [page-name-or-uuid]
  76. (when page-name-or-uuid
  77. (when-let [block (model/get-page page-name-or-uuid)]
  78. (model/sub-block (:db/id block)))))
  79. (defn- open-root-block!
  80. [state]
  81. (let [[_ block _ sidebar? preview?] (:rum/args state)]
  82. (when (and
  83. (or preview?
  84. (not (contains? #{:home :all-journals} (state/get-current-route))))
  85. (not sidebar?))
  86. (when (and (string/blank? (:block/content block))
  87. (not preview?))
  88. (editor-handler/edit-block! block :max))))
  89. state)
  90. (rum/defc page-blocks-inner <
  91. {:did-mount open-root-block!}
  92. [page-e blocks config sidebar? whiteboard? _block-uuid]
  93. (let [hiccup (component-block/->hiccup blocks config {})]
  94. [:div.page-blocks-inner {:style {:margin-left (if (or sidebar? whiteboard?) 0 -20)}}
  95. (rum/with-key
  96. (content/content (str (:block/uuid page-e))
  97. {:hiccup hiccup
  98. :sidebar? sidebar?})
  99. (str (:block/uuid page-e) "-hiccup"))]))
  100. (declare page)
  101. (if config/publishing?
  102. (rum/defc dummy-block
  103. [_page-name]
  104. [:div])
  105. (rum/defc dummy-block
  106. [page-name]
  107. (let [[hover set-hover!] (rum/use-state false)
  108. click-handler-fn (fn []
  109. (let [block (editor-handler/insert-first-page-block-if-not-exists! page-name {:redirect? false})]
  110. (js/setTimeout #(editor-handler/edit-block! block :max) 0)))
  111. drop-handler-fn (fn [^js event]
  112. (util/stop event)
  113. (p/let [block-uuids (state/get-selection-block-ids)
  114. lookup-refs (map (fn [id] [:block/uuid id]) block-uuids)
  115. selected (db/pull-many (state/get-current-repo) '[*] lookup-refs)
  116. blocks (if (seq selected) selected [@component-block/*dragging-block])
  117. _ (editor-handler/insert-first-page-block-if-not-exists! page-name {:redirect? false})]
  118. (js/setTimeout #(let [target-block (db/pull (:db/id (db/get-page page-name)))]
  119. (dnd/move-blocks event blocks target-block nil :sibling))
  120. 0)))]
  121. [:div.ls-block.flex-1.flex-col.rounded-sm
  122. {:style {:width "100%"
  123. ;; The same as .dnd-separator
  124. :border-top (if hover
  125. "3px solid #ccc"
  126. nil)}}
  127. [:div.flex.flex-row
  128. [:div.flex.flex-row.items-center.mr-2.ml-1 {:style {:height 24}}
  129. [:span.bullet-container.cursor
  130. [:span.bullet]]]
  131. (shui/trigger-as :div.flex.flex-1
  132. {:tabIndex 0
  133. :on-click click-handler-fn
  134. :on-drag-enter #(set-hover! true)
  135. :on-drag-over #(util/stop %)
  136. :on-drop drop-handler-fn
  137. :on-drag-leave #(set-hover! false)}
  138. [:span.opacity-70
  139. "Click here to edit..."])]])))
  140. (rum/defc add-button
  141. [args]
  142. [:div.flex-1.flex-col.rounded-sm.add-button-link-wrap
  143. {:on-click (fn [] (editor-handler/api-insert-new-block! "" args))}
  144. [:div.flex.flex-row
  145. [:div.block {:style {:height 20
  146. :width 20
  147. :margin-left 2}}
  148. [:a.add-button-link.block
  149. (ui/icon "circle-plus")]]]])
  150. (rum/defcs page-blocks-cp < rum/reactive db-mixins/query
  151. {:will-mount (fn [state]
  152. (let [page-e (second (:rum/args state))
  153. page-name (:block/name page-e)]
  154. (when (and page-name
  155. (db/journal-page? page-name)
  156. (>= (date/journal-title->int page-name)
  157. (date/journal-title->int (date/today))))
  158. (state/pub-event! [:journal/insert-template page-name])))
  159. state)}
  160. [state _repo page-e {:keys [sidebar? whiteboard?] :as config}]
  161. (when page-e
  162. (let [page-name (or (:block/name page-e)
  163. (str (:block/uuid page-e)))
  164. block-id (parse-uuid page-name)
  165. block? (boolean block-id)
  166. block (get-block (or (:block/uuid page-e) (:block/name page-e)))
  167. children (:block/_parent block)]
  168. (cond
  169. (and
  170. (not block?)
  171. (empty? children))
  172. (dummy-block page-name)
  173. :else
  174. (let [document-mode? (state/sub :document/mode?)
  175. short-page? (when-not block?
  176. (<= (count (:block/_page block)) 200))
  177. hiccup-config (merge
  178. {:id (if block? (str block-id) page-name)
  179. :db/id (:db/id block)
  180. :block? block?
  181. :editor-box editor/box
  182. :document/mode? document-mode?
  183. :disable-lazy-load? short-page?}
  184. config)
  185. config (common-handler/config-with-document-mode hiccup-config)
  186. blocks (if block? [block] (db/sort-by-left children block))]
  187. [:div
  188. (page-blocks-inner page-e blocks config sidebar? whiteboard? block-id)
  189. (when-not config/publishing?
  190. (let [args (if block-id
  191. {:block-uuid block-id}
  192. {:page page-name})]
  193. (add-button args)))])))))
  194. (rum/defc today-queries < rum/reactive
  195. [repo today? sidebar?]
  196. (when (and today? (not sidebar?))
  197. (let [queries (get-in (state/sub-config repo) [:default-queries :journals])]
  198. (when (seq queries)
  199. [:div#today-queries.mt-10
  200. (for [query queries]
  201. (rum/with-key
  202. (ui/catch-error
  203. (ui/component-error "Failed default query:" {:content (pr-str query)})
  204. (query/custom-query (component-block/wrap-query-components
  205. {:attr {:class "mt-10"}
  206. :editor-box editor/box
  207. :page page})
  208. query))
  209. (str repo "-custom-query-" (:query query))))]))))
  210. (defn tagged-pages
  211. [repo tag tag-original-name]
  212. (let [pages (db/get-tag-pages repo tag)]
  213. (when (seq pages)
  214. [:div.references.page-tags.mt-6.flex-1.flex-row
  215. [:div.content
  216. (ui/foldable
  217. [:h2.font-bold.opacity-50 (util/format "Pages tagged with \"%s\"" tag-original-name)]
  218. [:ul.mt-2
  219. (for [[original-name name] (sort-by last pages)]
  220. [:li {:key (str "tagged-page-" name)}
  221. (component-block/page-cp {} {:block/name name :block/original-name original-name})])]
  222. {:default-collapsed? false})]])))
  223. (rum/defc page-title-editor < rum/reactive
  224. [page {:keys [*input-value *title-value *edit? untitled? page-name old-name whiteboard-page?]}]
  225. (let [input-ref (rum/create-ref)
  226. collide? #(and (not= (util/page-name-sanity-lc page-name)
  227. (util/page-name-sanity-lc @*title-value))
  228. (db/page-exists? page-name)
  229. (db/page-exists? @*title-value))
  230. rollback-fn #(let [old-name (if untitled? "" old-name)]
  231. (reset! *title-value old-name)
  232. (gobj/set (rum/deref input-ref) "value" old-name)
  233. (reset! *edit? true)
  234. (.focus (rum/deref input-ref)))
  235. blur-fn (fn [e]
  236. (when (common-util/wrapped-by-quotes? @*title-value)
  237. (swap! *title-value common-util/unquote-string)
  238. (gobj/set (rum/deref input-ref) "value" @*title-value))
  239. (cond
  240. (or (= old-name @*title-value) (and whiteboard-page? (string/blank? @*title-value)))
  241. (reset! *edit? false)
  242. (string/blank? @*title-value)
  243. (do (when-not untitled? (notification/show! (t :page/illegal-page-name) :warning))
  244. (rollback-fn))
  245. (collide?)
  246. (do (notification/show! (t :page/page-already-exists @*title-value) :error)
  247. (rollback-fn))
  248. (and (date/valid-journal-title? @*title-value) whiteboard-page?)
  249. (do (notification/show! (t :page/whiteboard-to-journal-error) :error)
  250. (rollback-fn))
  251. :else
  252. (p/do!
  253. (page-handler/rename! (:block/uuid page) @*title-value)
  254. (js/setTimeout #(reset! *edit? false) 100)))
  255. (util/stop e))]
  256. [:input.edit-input.p-0.focus:outline-none.ring-none
  257. {:type "text"
  258. :ref input-ref
  259. :auto-focus true
  260. :style {:outline "none"
  261. :width "100%"
  262. :font-weight "inherit"}
  263. :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
  264. :value (rum/react *input-value)
  265. :on-change (fn [^js e]
  266. (let [value (util/evalue e)]
  267. (reset! *title-value (string/trim value))
  268. (reset! *input-value value)))
  269. :on-blur blur-fn
  270. :on-key-down (fn [^js e]
  271. (when (= (gobj/get e "key") "Enter")
  272. (blur-fn e)))
  273. :placeholder (when untitled? (t :untitled))
  274. :on-key-up (fn [^js e]
  275. ;; Esc
  276. (when (= 27 (.-keyCode e))
  277. (reset! *title-value old-name)
  278. (reset! *edit? false)))
  279. :on-focus (fn []
  280. (when untitled? (reset! *title-value "")))}]))
  281. (rum/defc page-title-configure
  282. [*show-page-info?]
  283. [:div.absolute.-top-4.left-2.fade-in
  284. (shui/button
  285. {:variant :outline
  286. :size :xs
  287. :on-click #(swap! *show-page-info? not)}
  288. "Configure")])
  289. (rum/defcs ^:large-vars/cleanup-todo page-title < rum/reactive
  290. (rum/local false ::edit?)
  291. (rum/local "" ::input-value)
  292. {:init (fn [state]
  293. (let [page (first (:rum/args state))
  294. original-name (:block/original-name page)
  295. *title-value (atom original-name)]
  296. (assoc state ::title-value *title-value)))}
  297. [state page {:keys [fmt-journal? preview? *hover? *show-page-info?]}]
  298. (when page
  299. (let [page (db/sub-block (:db/id page))
  300. title (:block/original-name page)]
  301. (when title
  302. (let [journal? (:block/journal? page)
  303. icon (get page (pu/get-pid :logseq.property/icon))
  304. *title-value (get state ::title-value)
  305. *edit? (get state ::edit?)
  306. *input-value (get state ::input-value)
  307. repo (state/get-current-repo)
  308. hls-page? (pdf-utils/hls-file? title)
  309. whiteboard-page? (model/whiteboard-page? page)
  310. untitled? (and whiteboard-page? (parse-uuid title)) ;; normal page cannot be untitled right?
  311. title (if hls-page?
  312. [:a.asset-ref (pdf-utils/fix-local-asset-pagename title)]
  313. (if fmt-journal?
  314. (date/journal-title->custom-format title)
  315. title))
  316. old-name title
  317. db-based? (config/db-based-graph? repo)]
  318. [:div.ls-page-title.flex.flex-1.flex-row.flex-wrap.w-full.relative.items-center.gap-2
  319. {:on-mouse-over #(when-not @*edit? (reset! *hover? true))
  320. :on-mouse-out #(reset! *hover? false)}
  321. (when icon
  322. [:div.page-icon
  323. {:on-pointer-down util/stop-propagation}
  324. (if (and (map? icon) db-based?)
  325. (icon-component/icon-picker icon
  326. {:on-chosen (fn [_e icon]
  327. (db-property-handler/set-block-property!
  328. repo
  329. (:db/id page)
  330. (pu/get-pid :logseq.property/icon)
  331. icon
  332. {}))
  333. :icon-props {:size 38}})
  334. icon)])
  335. [:h1.page-title.flex-1.cursor-pointer.gap-1
  336. {:class (when-not whiteboard-page? "title")
  337. :on-pointer-down (fn [e]
  338. (when (util/right-click? e)
  339. (state/set-state! :page-title/context {:page (:block/original-name page)
  340. :page-entity page})))
  341. :on-click (fn [e]
  342. (when-not (= (.-nodeName (.-target e)) "INPUT")
  343. (.preventDefault e)
  344. (if (gobj/get e "shiftKey")
  345. (state/sidebar-add-block!
  346. repo
  347. (:db/id page)
  348. :page)
  349. (when (and (not hls-page?)
  350. (not journal?)
  351. (not config/publishing?)
  352. (not (ldb/built-in? page)))
  353. (reset! *input-value (if untitled? "" old-name))
  354. (reset! *hover? false)
  355. (reset! *edit? true)))))}
  356. (if @*edit?
  357. (page-title-editor page {:*title-value *title-value
  358. :*edit? *edit?
  359. :*input-value *input-value
  360. :page-name (:block/original-name page)
  361. :old-name old-name
  362. :untitled? untitled?
  363. :whiteboard-page? whiteboard-page?
  364. :preview? preview?})
  365. [:span.title.block
  366. {:on-click (fn []
  367. (when (and (state/home?) (not preview?))
  368. (route-handler/redirect-to-page! (:block/uuid page))))
  369. :data-value @*input-value
  370. :data-ref (:block/original-name page)
  371. :style {:opacity (when @*edit? 0)}}
  372. (let [nested? (and (string/includes? title page-ref/left-brackets)
  373. (string/includes? title page-ref/right-brackets))]
  374. (cond untitled? [:span.opacity-50 (t :untitled)]
  375. nested? (component-block/map-inline {} (gp-mldoc/inline->edn title (mldoc/get-default-config
  376. (:block/format page))))
  377. :else title))])]
  378. (when (and db-based? @*hover?)
  379. (page-title-configure *show-page-info?))])))))
  380. (defn- page-mouse-over
  381. [e *control-show? *all-collapsed?]
  382. (util/stop e)
  383. (reset! *control-show? true)
  384. (let [all-collapsed?
  385. (->> (editor-handler/all-blocks-with-level {:collapse? true})
  386. (filter (fn [b] (editor-handler/collapsable? (:block/uuid b))))
  387. (empty?))]
  388. (reset! *all-collapsed? all-collapsed?)))
  389. (defn- page-mouse-leave
  390. [e *control-show?]
  391. (util/stop e)
  392. (reset! *control-show? false))
  393. (rum/defcs page-blocks-collapse-control <
  394. [state title *control-show? *all-collapsed?]
  395. [:a.page-blocks-collapse-control
  396. {:id (str "control-" title)
  397. :on-click (fn [event]
  398. (util/stop event)
  399. (if @*all-collapsed?
  400. (editor-handler/expand-all!)
  401. (editor-handler/collapse-all!))
  402. (swap! *all-collapsed? not))}
  403. [:span.mt-6 {:class (if @*control-show?
  404. "control-show cursor-pointer" "control-hide")}
  405. (ui/rotating-arrow @*all-collapsed?)]])
  406. (defn- get-path-page-name
  407. [state page-name]
  408. (or page-name
  409. (get-block-uuid-by-block-route-name state)
  410. ;; is page name or uuid
  411. (get-page-name state)
  412. (state/get-current-page)))
  413. (defn get-page-entity
  414. [page-name]
  415. (cond
  416. (uuid? page-name)
  417. (db/entity [:block/uuid page-name])
  418. (common-util/uuid-string? page-name)
  419. (db/entity [:block/uuid (uuid page-name)])
  420. :else
  421. (db/get-page page-name)))
  422. (defn- get-sanity-page-name
  423. [state page-name]
  424. (when-let [path-page-name (get-path-page-name state page-name)]
  425. (util/page-name-sanity-lc path-page-name)))
  426. ;; A page is just a logical block
  427. (rum/defcs ^:large-vars/cleanup-todo page-inner < rum/reactive db-mixins/query
  428. (rum/local false ::all-collapsed?)
  429. (rum/local false ::control-show?)
  430. (rum/local nil ::current-page)
  431. (rum/local false ::hover-title?)
  432. (rum/local false ::show-page-info?)
  433. {:init (fn [state]
  434. (let [page-name (:page-name (first (:rum/args state)))
  435. page-name' (get-sanity-page-name state page-name)]
  436. (p/do!
  437. (db-async/<get-block (state/get-current-repo) page-name')
  438. (route-handler/update-page-title-and-label! (state/get-route-match)))
  439. (assoc state ::page-name page-name')))}
  440. [state {:keys [repo page-name preview? sidebar? linked-refs? unlinked-refs?] :as option}]
  441. (let [loading? (when (::page-name state) (state/sub-async-query-loading (::page-name state)))]
  442. (when-let [path-page-name (get-path-page-name state page-name)]
  443. (let [current-repo (state/sub :git/current-repo)
  444. repo (or repo current-repo)
  445. page-name (util/page-name-sanity-lc path-page-name)
  446. page (get-page-entity page-name)]
  447. (when-not (and loading? (nil? page))
  448. (let [block-id (:block/uuid page)
  449. block? (some? (:block/page page))
  450. journal? (db/journal-page? page-name)
  451. db-based? (config/db-based-graph? repo)
  452. fmt-journal? (boolean (date/journal-title->int page-name))
  453. whiteboard? (:whiteboard? option) ;; in a whiteboard portal shape?
  454. whiteboard-page? (model/whiteboard-page? page) ;; is this page a whiteboard?
  455. route-page-name path-page-name
  456. page-name (:block/name page)
  457. page-original-name (:block/original-name page)
  458. title (or page-original-name page-name)
  459. today? (and
  460. journal?
  461. (= page-name (util/page-name-sanity-lc (date/journal-name))))
  462. *control-show? (::control-show? state)
  463. *all-collapsed? (::all-collapsed? state)
  464. block-or-whiteboard? (or block? whiteboard?)
  465. home? (= :home (state/get-current-route))]
  466. (when (or page-name block-or-whiteboard?)
  467. [:div.flex-1.page.relative
  468. (merge (if (seq (:block/tags page))
  469. (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
  470. {:data-page-tags (text-util/build-data-value page-names)})
  471. {})
  472. {:key path-page-name
  473. :class (util/classnames [{:is-journals (or journal? fmt-journal?)}])})
  474. (if (and whiteboard-page? (not sidebar?))
  475. [:div ((state/get-component :whiteboard/tldraw-preview) (:block/uuid page))] ;; FIXME: this is not reactive
  476. [:div.relative.grid.gap-4
  477. (when (and (not sidebar?) (not block?))
  478. [:div.flex.flex-row.space-between
  479. (when (or (mobile-util/native-platform?) (util/mobile?))
  480. [:div.flex.flex-row.pr-2
  481. {:style {:margin-left -15}
  482. :on-mouse-over (fn [e]
  483. (page-mouse-over e *control-show? *all-collapsed?))
  484. :on-mouse-leave (fn [e]
  485. (page-mouse-leave e *control-show?))}
  486. (page-blocks-collapse-control title *control-show? *all-collapsed?)])
  487. (when (and (not whiteboard?) (ldb/page? page))
  488. (page-title page {:journal? journal?
  489. :fmt-journal? fmt-journal?
  490. :preview? preview?
  491. :*hover? (::hover-title? state)
  492. :*show-page-info? (::show-page-info? state)}))
  493. (when (not config/publishing?)
  494. (when config/lsp-enabled?
  495. [:div.flex.flex-row
  496. (plugins/hook-ui-slot :page-head-actions-slotted nil)
  497. (plugins/hook-ui-items :pagebar)]))])
  498. (cond
  499. (and db-based? (not block?))
  500. (db-page/page-info page (::show-page-info? state))
  501. (and (not db-based?) (not block?))
  502. [:div.pb-2])
  503. [:div
  504. (when (and block? (not sidebar?) (not whiteboard?))
  505. (let [config (merge config {:id "block-parent"
  506. :block? true})]
  507. [:div.mb-4
  508. (component-block/breadcrumb config repo block-id {:level-limit 3})]))
  509. (when (and db-based? (not block?) (not preview?))
  510. (db-page/page-properties-react page {:configure? false}))
  511. ;; blocks
  512. (if loading?
  513. [:div.space-y-2
  514. (shui/skeleton {:class "h-6 w-full"})
  515. (shui/skeleton {:class "h-6 w-full"})]
  516. (page-blocks-cp repo page {:sidebar? sidebar? :whiteboard? whiteboard?}))]])
  517. (when today?
  518. (today-queries repo today? sidebar?))
  519. (when today?
  520. (scheduled/scheduled-and-deadlines page-name))
  521. (when-not block?
  522. (tagged-pages repo page-name page-original-name))
  523. ;; referenced blocks
  524. (when-not block-or-whiteboard?
  525. (when (and page (not (false? linked-refs?)))
  526. [:div {:key "page-references"}
  527. (rum/with-key
  528. (reference/references page)
  529. (str route-page-name "-refs"))]))
  530. (when (contains? (:block/type page) "class")
  531. (class-component/class-children page))
  532. (when-not block-or-whiteboard?
  533. (when (and (not journal?) (not db-based?))
  534. (hierarchy/structures route-page-name)))
  535. (when (and (not (false? unlinked-refs?))
  536. (not (or block-or-whiteboard? sidebar? home?)))
  537. [:div {:key "page-unlinked-references"}
  538. (reference/unlinked-references page)])])))))))
  539. (rum/defcs page < rum/static
  540. [state option]
  541. (rum/with-key
  542. (page-inner option)
  543. (or (:page-name option)
  544. (get-page-name state))))
  545. (rum/defc contents-page < rum/reactive
  546. {:init (fn [state]
  547. (db-async/<get-block (state/get-current-repo) "contents")
  548. state)}
  549. [page]
  550. (when-let [repo (state/get-current-repo)]
  551. (when-not (state/sub-async-query-loading "contents")
  552. (page-blocks-cp repo page {:sidebar? true}))))
  553. (defonce layout (atom [js/window.innerWidth js/window.innerHeight]))
  554. ;; scrollHeight
  555. (rum/defcs graph-filter-section < (rum/local false ::open?)
  556. [state title content {:keys [search-filters]}]
  557. (let [open? (get state ::open?)]
  558. (when (and (seq search-filters) (not @open?))
  559. (reset! open? true))
  560. [:li.relative
  561. [:div
  562. [:button.w-full.px-4.py-2.text-left.focus:outline-none {:on-click #(swap! open? not)}
  563. [:div.flex.items-center.justify-between
  564. title
  565. (if @open? (svg/caret-down) (svg/caret-right))]]
  566. (content open?)]]))
  567. (rum/defc filter-expand-area
  568. [open? content]
  569. [:div.relative.overflow-hidden.transition-all.max-h-0.duration-700
  570. {:style {:max-height (if @open? 400 0)}}
  571. content])
  572. (defonce *n-hops (atom nil))
  573. (defonce *focus-nodes (atom []))
  574. (defonce *graph-reset? (atom false))
  575. (defonce *graph-forcereset? (atom false))
  576. (defonce *journal? (atom nil))
  577. (defonce *orphan-pages? (atom true))
  578. (defonce *builtin-pages? (atom nil))
  579. (defonce *excluded-pages? (atom true))
  580. (defonce *show-journals-in-page-graph? (atom nil))
  581. (defonce *created-at-filter (atom nil))
  582. (defonce *link-dist (atom 70))
  583. (defonce *charge-strength (atom -600))
  584. (defonce *charge-range (atom 600))
  585. (rum/defcs simulation-switch < rum/reactive
  586. [state]
  587. (let [*simulation-paused? pixi/*simulation-paused?]
  588. [:div.flex.flex-col.mb-2
  589. [:p {:title "Pause simulation"}
  590. "Pause simulation"]
  591. (ui/toggle
  592. (rum/react *simulation-paused?)
  593. (fn []
  594. (let [paused? @*simulation-paused?]
  595. (if paused?
  596. (pixi/resume-simulation!)
  597. (pixi/stop-simulation!))))
  598. true)]))
  599. (rum/defc ^:large-vars/cleanup-todo graph-filters < rum/reactive
  600. [graph settings forcesettings n-hops]
  601. (let [{:keys [journal? orphan-pages? builtin-pages? excluded-pages?]
  602. :or {orphan-pages? true}} settings
  603. {:keys [link-dist charge-strength charge-range]} forcesettings
  604. journal?' (rum/react *journal?)
  605. orphan-pages?' (rum/react *orphan-pages?)
  606. builtin-pages?' (rum/react *builtin-pages?)
  607. excluded-pages?' (rum/react *excluded-pages?)
  608. link-dist' (rum/react *link-dist)
  609. charge-strength' (rum/react *charge-strength)
  610. charge-range' (rum/react *charge-range)
  611. journal? (if (nil? journal?') journal? journal?')
  612. orphan-pages? (if (nil? orphan-pages?') orphan-pages? orphan-pages?')
  613. builtin-pages? (if (nil? builtin-pages?') builtin-pages? builtin-pages?')
  614. excluded-pages? (if (nil? excluded-pages?') excluded-pages? excluded-pages?')
  615. created-at-filter (or (rum/react *created-at-filter) (:created-at-filter settings))
  616. link-dist (if (nil? link-dist') link-dist link-dist')
  617. charge-strength (if (nil? charge-strength') charge-strength charge-strength')
  618. charge-range (if (nil? charge-range') charge-range charge-range')
  619. set-setting! (fn [key value]
  620. (let [new-settings (assoc settings key value)]
  621. (config-handler/set-config! :graph/settings new-settings)))
  622. set-forcesetting! (fn [key value]
  623. (let [new-forcesettings (assoc forcesettings key value)]
  624. (config-handler/set-config! :graph/forcesettings new-forcesettings)))
  625. search-graph-filters (state/sub :search/graph-filters)
  626. focus-nodes (rum/react *focus-nodes)]
  627. [:div.absolute.top-4.right-4.graph-filters
  628. [:div.flex.flex-col
  629. [:div.shadow-xl.rounded-sm
  630. [:ul
  631. (graph-filter-section
  632. [:span.font-medium "Nodes"]
  633. (fn [open?]
  634. (filter-expand-area
  635. open?
  636. [:div
  637. [:p.text-sm.opacity-70.px-4
  638. (let [c1 (count (:nodes graph))
  639. s1 (if (> c1 1) "s" "")
  640. ;; c2 (count (:links graph))
  641. ;; s2 (if (> c2 1) "s" "")
  642. ]
  643. ;; (util/format "%d page%s, %d link%s" c1 s1 c2 s2)
  644. (util/format "%d page%s" c1 s1))]
  645. [:div.p-6
  646. ;; [:div.flex.items-center.justify-between.mb-2
  647. ;; [:span "Layout"]
  648. ;; (ui/select
  649. ;; (mapv
  650. ;; (fn [item]
  651. ;; (if (= (:label item) layout)
  652. ;; (assoc item :selected "selected")
  653. ;; item))
  654. ;; [{:label "gForce"}
  655. ;; {:label "dagre"}])
  656. ;; (fn [_e value]
  657. ;; (set-setting! :layout value))
  658. ;; {:class "graph-layout"})]
  659. [:div.flex.items-center.justify-between.mb-2
  660. [:span (t :settings-page/enable-journals)]
  661. ;; FIXME: why it's not aligned well?
  662. [:div.mt-1
  663. (ui/toggle journal?
  664. (fn []
  665. (let [value (not journal?)]
  666. (reset! *journal? value)
  667. (set-setting! :journal? value)))
  668. true)]]
  669. [:div.flex.items-center.justify-between.mb-2
  670. [:span "Orphan pages"]
  671. [:div.mt-1
  672. (ui/toggle orphan-pages?
  673. (fn []
  674. (let [value (not orphan-pages?)]
  675. (reset! *orphan-pages? value)
  676. (set-setting! :orphan-pages? value)))
  677. true)]]
  678. [:div.flex.items-center.justify-between.mb-2
  679. [:span "Built-in pages"]
  680. [:div.mt-1
  681. (ui/toggle builtin-pages?
  682. (fn []
  683. (let [value (not builtin-pages?)]
  684. (reset! *builtin-pages? value)
  685. (set-setting! :builtin-pages? value)))
  686. true)]]
  687. [:div.flex.items-center.justify-between.mb-2
  688. [:span "Excluded pages"]
  689. [:div.mt-1
  690. (ui/toggle excluded-pages?
  691. (fn []
  692. (let [value (not excluded-pages?)]
  693. (reset! *excluded-pages? value)
  694. (set-setting! :excluded-pages? value)))
  695. true)]]
  696. (when (config/db-based-graph? (state/get-current-repo))
  697. [:div.flex.flex-col.mb-2
  698. [:p "Created before"]
  699. (when created-at-filter
  700. [:div (.toDateString (js/Date. (+ created-at-filter (get-in graph [:all-pages :created-at-min]))))])
  701. (ui/tippy {:html [:div.pr-3 (str (js/Date. (+ created-at-filter (get-in graph [:all-pages :created-at-min]))))]}
  702. ;; Slider keeps track off the range from min created-at to max created-at
  703. ;; because there were bugs with setting min and max directly
  704. (ui/slider created-at-filter
  705. {:min 0
  706. :max (- (get-in graph [:all-pages :created-at-max])
  707. (get-in graph [:all-pages :created-at-min]))
  708. :on-change #(do
  709. (reset! *created-at-filter (int %))
  710. (set-setting! :created-at-filter (int %)))}))])
  711. (when (seq focus-nodes)
  712. [:div.flex.flex-col.mb-2
  713. [:p {:title "N hops from selected nodes"}
  714. "N hops from selected nodes"]
  715. (ui/tippy {:html [:div.pr-3 n-hops]}
  716. (ui/slider (or n-hops 10)
  717. {:min 1
  718. :max 10
  719. :on-change #(reset! *n-hops (int %))}))])
  720. [:a.opacity-70.opacity-100 {:on-click (fn []
  721. (swap! *graph-reset? not)
  722. (reset! *focus-nodes [])
  723. (reset! *n-hops nil)
  724. (reset! *created-at-filter nil)
  725. (set-setting! :created-at-filter nil)
  726. (state/clear-search-filters!))}
  727. "Reset Graph"]]]))
  728. {})
  729. (graph-filter-section
  730. [:span.font-medium "Search"]
  731. (fn [open?]
  732. (filter-expand-area
  733. open?
  734. [:div.p-6
  735. (if (seq search-graph-filters)
  736. [:div
  737. (for [q search-graph-filters]
  738. [:div.flex.flex-row.justify-between.items-center.mb-2
  739. [:span.font-medium q]
  740. [:a.search-filter-close.opacity-70.opacity-100 {:on-click #(state/remove-search-filter! q)}
  741. svg/close]])
  742. [:a.opacity-70.opacity-100 {:on-click state/clear-search-filters!}
  743. "Clear All"]]
  744. [:a.opacity-70.opacity-100 {:on-click #(route-handler/go-to-search! :graph)}
  745. "Click to search"])]))
  746. {:search-filters search-graph-filters})
  747. (graph-filter-section
  748. [:span.font-medium "Forces"]
  749. (fn [open?]
  750. (filter-expand-area
  751. open?
  752. [:div
  753. [:p.text-sm.opacity-70.px-4
  754. (let [c2 (count (:links graph))
  755. s2 (if (> c2 1) "s" "")]
  756. (util/format "%d link%s" c2 s2))]
  757. [:div.p-6
  758. (simulation-switch)
  759. [:div.flex.flex-col.mb-2
  760. [:p {:title "Link Distance"}
  761. "Link Distance"]
  762. (ui/tippy {:html [:div.pr-3 link-dist]}
  763. (ui/slider (/ link-dist 10)
  764. {:min 1 ;; 10
  765. :max 18 ;; 180
  766. :on-change #(let [value (int %)]
  767. (reset! *link-dist (* value 10))
  768. (set-forcesetting! :link-dist (* value 10)))}))]
  769. [:div.flex.flex-col.mb-2
  770. [:p {:title "Charge Strength"}
  771. "Charge Strength"]
  772. (ui/tippy {:html [:div.pr-3 charge-strength]}
  773. (ui/slider (/ charge-strength 100)
  774. {:min -10 ;;-1000
  775. :max 10 ;;1000
  776. :on-change #(let [value (int %)]
  777. (reset! *charge-strength (* value 100))
  778. (set-forcesetting! :charge-strength (* value 100)))}))]
  779. [:div.flex.flex-col.mb-2
  780. [:p {:title "Charge Range"}
  781. "Charge Range"]
  782. (ui/tippy {:html [:div.pr-3 charge-range]}
  783. (ui/slider (/ charge-range 100)
  784. {:min 5 ;;500
  785. :max 40 ;;4000
  786. :on-change #(let [value (int %)]
  787. (reset! *charge-range (* value 100))
  788. (set-forcesetting! :charge-range (* value 100)))}))]
  789. [:a.opacity-70.opacity-100 {:on-click (fn []
  790. (swap! *graph-forcereset? not)
  791. (reset! *link-dist 70)
  792. (reset! *charge-strength -600)
  793. (reset! *charge-range 600))}
  794. "Reset Forces"]]]))
  795. {})
  796. (graph-filter-section
  797. [:span.font-medium "Export"]
  798. (fn [open?]
  799. (filter-expand-area
  800. open?
  801. (when-let [canvas (js/document.querySelector "#global-graph canvas")]
  802. [:div.p-6
  803. ;; We'll get an empty image if we don't wrap this in a requestAnimationFrame
  804. [:div [:a {:on-click #(.requestAnimationFrame js/window (fn [] (utils/canvasToImage canvas "graph" "png")))} "as PNG"]]])))
  805. {:search-filters search-graph-filters})]]]]))
  806. (defonce last-node-position (atom nil))
  807. (defn- graph-register-handlers
  808. [graph focus-nodes n-hops dark?]
  809. (.on graph "nodeClick"
  810. (fn [event node]
  811. (let [x (.-x event)
  812. y (.-y event)
  813. drag? (not (let [[last-node last-x last-y] @last-node-position
  814. threshold 5]
  815. (and (= node last-node)
  816. (<= (abs (- x last-x)) threshold)
  817. (<= (abs (- y last-y)) threshold))))]
  818. (graph/on-click-handler graph node event focus-nodes n-hops drag? dark?))))
  819. (.on graph "nodeMousedown"
  820. (fn [event node]
  821. (reset! last-node-position [node (.-x event) (.-y event)]))))
  822. (rum/defc global-graph-inner < rum/reactive
  823. [graph settings forcesettings theme]
  824. (let [[width height] (rum/react layout)
  825. dark? (= theme "dark")
  826. n-hops (rum/react *n-hops)
  827. link-dist (rum/react *link-dist)
  828. charge-strength (rum/react *charge-strength)
  829. charge-range (rum/react *charge-range)
  830. reset? (rum/react *graph-reset?)
  831. forcereset? (rum/react *graph-forcereset?)
  832. focus-nodes (when n-hops (rum/react *focus-nodes))
  833. graph (if (and (integer? n-hops)
  834. (seq focus-nodes)
  835. (not (:orphan-pages? settings)))
  836. (graph-handler/n-hops graph focus-nodes n-hops)
  837. graph)
  838. graph (update graph :links (fn [links]
  839. (let [nodes (set (map :id (:nodes graph)))]
  840. (remove (fn [link]
  841. (and (not (nodes (:source link)))
  842. (not (nodes (:target link)))))
  843. links))))]
  844. [:div.relative#global-graph
  845. (graph/graph-2d {:nodes (:nodes graph)
  846. :links (:links graph)
  847. :width (- width 24)
  848. :height (- height 48)
  849. :dark? dark?
  850. :link-dist link-dist
  851. :charge-strength charge-strength
  852. :charge-range charge-range
  853. :register-handlers-fn
  854. (fn [graph]
  855. (graph-register-handlers graph *focus-nodes *n-hops dark?))
  856. :reset? reset?
  857. :forcereset? forcereset?})
  858. (graph-filters graph settings forcesettings n-hops)]))
  859. (defn- filter-graph-nodes
  860. [nodes filters]
  861. (if (seq filters)
  862. (let [filter-patterns (map #(re-pattern (str "(?i)" (util/regex-escape %))) filters)]
  863. (filter (fn [node] (some #(re-find % (:id node)) filter-patterns)) nodes))
  864. nodes))
  865. (rum/defcs global-graph < rum/reactive
  866. (mixins/event-mixin
  867. (fn [state]
  868. (mixins/listen state js/window "resize"
  869. (fn [_e]
  870. (reset! layout [js/window.innerWidth js/window.innerHeight])))))
  871. {:will-unmount (fn [state]
  872. (reset! *n-hops nil)
  873. (reset! *focus-nodes [])
  874. (state/set-search-mode! :global)
  875. state)}
  876. [state]
  877. (let [settings (state/graph-settings)
  878. forcesettings (state/graph-forcesettings)
  879. theme (state/sub :ui/theme)
  880. ;; Needed for query to retrigger after reset
  881. _reset? (rum/react *graph-reset?)
  882. graph (graph-handler/build-global-graph theme settings)
  883. search-graph-filters (state/sub :search/graph-filters)
  884. graph (update graph :nodes #(filter-graph-nodes % search-graph-filters))]
  885. (global-graph-inner graph settings forcesettings theme)))
  886. (rum/defc page-graph-inner < rum/reactive
  887. [_page graph dark?]
  888. (let [show-journals-in-page-graph? (rum/react *show-journals-in-page-graph?)]
  889. [:div.sidebar-item.flex-col
  890. [:div.flex.items-center.justify-between.mb-0
  891. [:span (t :right-side-bar/show-journals)]
  892. [:div.mt-1
  893. (ui/toggle show-journals-in-page-graph? ;my-val;
  894. (fn []
  895. (let [value (not show-journals-in-page-graph?)]
  896. (reset! *show-journals-in-page-graph? value)))
  897. true)]]
  898. (graph/graph-2d {:nodes (:nodes graph)
  899. :links (:links graph)
  900. :width 600
  901. :height 600
  902. :dark? dark?
  903. :register-handlers-fn
  904. (fn [graph]
  905. (graph-register-handlers graph (atom nil) (atom nil) dark?))})]))
  906. (rum/defc page-graph < db-mixins/query rum/reactive
  907. []
  908. (let [page (or
  909. (and (= :page (state/sub [:route-match :data :name]))
  910. (state/sub [:route-match :path-params :name]))
  911. (date/today))
  912. theme (:ui/theme @state/state)
  913. dark? (= theme "dark")
  914. show-journals-in-page-graph (rum/react *show-journals-in-page-graph?)
  915. graph (if (util/uuid-string? page)
  916. (graph-handler/build-block-graph (uuid page) theme)
  917. (graph-handler/build-page-graph page theme show-journals-in-page-graph))]
  918. (when (seq (:nodes graph))
  919. (page-graph-inner page graph dark?))))
  920. (defn- sort-pages-by
  921. [by-item desc? pages]
  922. (let [comp (if desc? > <)
  923. by-item (if (= by-item :block/name)
  924. (fn [x] (string/lower-case (:block/name x)))
  925. by-item)]
  926. (sort-by by-item comp pages)))
  927. (rum/defc checkbox-opt
  928. [key checked opts]
  929. (let [*input (rum/create-ref)
  930. indeterminate? (boolean (:indeterminate opts))]
  931. (rum/use-effect!
  932. #(set! (.-indeterminate (rum/deref *input)) indeterminate?)
  933. [indeterminate?])
  934. [:label {:for key}
  935. (ui/checkbox
  936. (merge
  937. {:checked (boolean checked)
  938. :ref *input
  939. :id key}
  940. opts))]))
  941. (rum/defc sortable-title
  942. [title key by-item desc?]
  943. [:th
  944. {:class [(name key)]}
  945. [:a.fade-link {:on-click (fn []
  946. (reset! by-item key)
  947. (swap! desc? not))}
  948. [:span.flex.items-center
  949. [:span.mr-1 title]
  950. (when (= @by-item key)
  951. [:span
  952. (if @desc? (svg/caret-down) (svg/caret-up))])]]])
  953. (defn batch-delete-dialog
  954. [pages orphaned-pages? refresh-fn]
  955. (fn [close-fn]
  956. [:div
  957. [:div.sm:flex.items-center
  958. [: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
  959. [:span.text-error.text-xl
  960. (ui/icon "alert-triangle")]]
  961. [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
  962. [:h3#modal-headline.text-lg.leading-6.font-medium
  963. (if orphaned-pages?
  964. (t :remove-orphaned-pages)
  965. (t :page/delete-confirmation))]]]
  966. [:table.table-auto.cp__all_pages_table.mt-4
  967. [:thead
  968. [:tr.opacity-70
  969. [:th [:span "#"]]
  970. [:th [:span (t :block/name)]]
  971. [:th [:span (t :page/backlinks)]]
  972. (when-not orphaned-pages? [:th [:span (t :page/created-at)]])
  973. (when-not orphaned-pages? [:th [:span (t :page/updated-at)]])]]
  974. [:tbody
  975. (for [[n {:block/keys [name created-at updated-at backlinks] :as page}] (medley/indexed pages)]
  976. [:tr {:key name}
  977. [:td.n.w-12 [:span.opacity-70 (str (inc n) ".")]]
  978. [:td.name [:a {:href (rfe/href :page {:name (:block/uuid page)})}
  979. (component-block/page-cp {} page)]]
  980. [:td.backlinks [:span (or backlinks "0")]]
  981. (when-not orphaned-pages? [:td.created-at [:span (if created-at (date/int->local-time-2 created-at) "Unknown")]])
  982. (when-not orphaned-pages? [:td.updated-at [:span (if updated-at (date/int->local-time-2 updated-at) "Unknown")]])])]]
  983. [:div.pt-6.flex.justify-end.gap-4
  984. (ui/button
  985. (t :cancel)
  986. :theme :gray
  987. :on-click close-fn)
  988. (ui/button
  989. (t :yes)
  990. :on-click (fn []
  991. (close-fn)
  992. (let [failed-pages (atom [])]
  993. (p/let [_ (p/all (map (fn [page]
  994. (page-handler/<delete! (:block/uuid page) nil
  995. {:error-handler
  996. (fn []
  997. (swap! failed-pages conj (:block/name page)))}))
  998. pages))]
  999. (if (seq @failed-pages)
  1000. (notification/show! (t :all-pages/failed-to-delete-pages (string/join ", " (map pr-str @failed-pages)))
  1001. :warning false)
  1002. (notification/show! (t :tips/all-done) :success))))
  1003. (js/setTimeout #(refresh-fn) 200)))]]))
  1004. (rum/defc pagination
  1005. "Pagination component, like `<< <Prev 1/10 Next> >>`.
  1006. - current: current page number
  1007. - total: total number of items
  1008. - per-page: number of items per page
  1009. - on-change: callback function when page number changes"
  1010. [& {:keys [current total per-page on-change]
  1011. :or {current 1 per-page 40}}]
  1012. (let [total-pages (int (Math/ceil (/ total per-page)))
  1013. has-prev? (> current 1)
  1014. has-next? (< current total-pages)
  1015. prev-page (if (= 1 current) 1 (dec current))
  1016. next-page (if (= total-pages current) total-pages (inc current))]
  1017. [:div.flex.items-center.select-none
  1018. (when has-prev?
  1019. [[:a.fade-link.flex
  1020. {:on-click #(on-change 1)}
  1021. (ui/icon "chevrons-left")]
  1022. [:a.fade-link.flex.items-center {:on-click #(on-change prev-page)}
  1023. (ui/icon "caret-left") (t :paginates/prev)]])
  1024. [:div.px-2
  1025. [:span (str current "/" total-pages)]]
  1026. (when has-next?
  1027. [[:a.fade-link.flex.items-center {:on-click #(on-change next-page)}
  1028. (t :paginates/next) (ui/icon "caret-right")]
  1029. [:a.fade-link.flex
  1030. {:on-click #(on-change total-pages)}
  1031. (ui/icon "chevrons-right")]])]))
  1032. (rum/defcs ^:large-vars/cleanup-todo all-pages < rum/reactive
  1033. (rum/local nil ::pages)
  1034. (rum/local nil ::search-key)
  1035. (rum/local nil ::results-all)
  1036. (rum/local nil ::results)
  1037. (rum/local {} ::checks)
  1038. (rum/local :block/updated-at ::sort-by-item)
  1039. (rum/local true ::desc?)
  1040. (rum/local {:journals? false :page-type ""} ::filters)
  1041. (rum/local nil ::filter-fn)
  1042. (rum/local 1 ::current-page)
  1043. [state]
  1044. (let [current-repo (state/sub :git/current-repo)
  1045. per-page-num 40
  1046. *sort-by-item (get state ::sort-by-item)
  1047. *desc? (::desc? state)
  1048. *filters (::filters state)
  1049. *results (::results state)
  1050. *results-all (::results-all state)
  1051. *checks (::checks state)
  1052. *pages (::pages state)
  1053. *current-page (::current-page state)
  1054. *filter-fn (::filter-fn state)
  1055. *search-key (::search-key state)
  1056. *search-input (rum/create-ref)
  1057. ;; TODO: remove this
  1058. *indeterminate (rum/derived-atom
  1059. [*checks] ::indeterminate
  1060. (fn [checks]
  1061. (when-let [checks (vals checks)]
  1062. (if (every? true? checks)
  1063. 1 (if (some true? checks) -1 0)))))
  1064. mobile? (util/mobile?)
  1065. total-items (count @*results-all)
  1066. ;; FIXME: "pages" is ambiguous here, it can be either "Logseq pages" or "result pages"
  1067. total-pages (if-not @*results-all 0
  1068. (js/Math.ceil (/ total-items per-page-num)))
  1069. to-page (fn [page]
  1070. (when (> total-pages 1)
  1071. (if (and (> page 0)
  1072. (<= page total-pages))
  1073. (reset! *current-page page)
  1074. (reset! *current-page 1))
  1075. (js/setTimeout #(util/scroll-to-top))))
  1076. search-key (fn [key]
  1077. (when-let [key (and key (string/trim key))]
  1078. (if (and (not (string/blank? key))
  1079. (seq @*results))
  1080. (reset! *search-key key)
  1081. (reset! *search-key nil))))
  1082. refresh-pages #(do
  1083. (reset! *pages nil)
  1084. (reset! *current-page 1))]
  1085. [:div.flex-1.cp__all_pages
  1086. [:h1.title (t :all-pages)]
  1087. [:div.text-sm.ml-1.opacity-70.mb-4 (t :paginates/pages total-items)]
  1088. (when current-repo
  1089. ;; all pages
  1090. (when (nil? @*pages)
  1091. (let [pages (->> (page-handler/get-all-pages current-repo)
  1092. (map-indexed (fn [idx page] (assoc page
  1093. :block/backlinks (count (:block/_refs (db/entity (:db/id page))))
  1094. :block/idx idx))))]
  1095. (reset! *filter-fn
  1096. (memoize (fn [sort-by-item desc? {:keys [journal? page-type]}]
  1097. (->> pages
  1098. (filter #(and
  1099. (or (boolean journal?)
  1100. (= false (boolean (:block/journal? %))))
  1101. (or (empty? page-type)
  1102. (contains? (set (:block/type %)) page-type))))
  1103. (sort-pages-by sort-by-item desc?)))))
  1104. (reset! *pages pages)))
  1105. ;; filter results
  1106. (when @*filter-fn
  1107. (let [pages (@*filter-fn @*sort-by-item @*desc? @*filters)
  1108. ;; search key
  1109. pages (if-not (string/blank? @*search-key)
  1110. (search/fuzzy-search pages (util/page-name-sanity-lc @*search-key)
  1111. :limit 20
  1112. :extract-fn :block/name)
  1113. pages)
  1114. _ (reset! *results-all pages)
  1115. pages (take per-page-num (drop (* per-page-num (dec @*current-page)) pages))]
  1116. (reset! *checks (into {} (for [{:block/keys [idx]} pages]
  1117. [idx (boolean (get @*checks idx))])))
  1118. (reset! *results pages)))
  1119. [:div.cp__all_pages-content
  1120. [:div.actions.pt-4
  1121. {:class (util/classnames [{:has-selected (or (nil? @*indeterminate)
  1122. (not= 0 @*indeterminate))}])}
  1123. [:div.l.flex.items-center
  1124. [:div.actions-wrap
  1125. (ui/button
  1126. (t :delete)
  1127. {:on-click
  1128. (fn []
  1129. (let [selected (filter (fn [[_ v]] v) @*checks)
  1130. selected (and (seq selected)
  1131. (into #{} (for [[k _] selected] k)))]
  1132. (when-let [pages (and selected (filter #(contains? selected (:block/idx %)) @*results))]
  1133. (state/set-modal! (batch-delete-dialog pages false #(do
  1134. (reset! *checks nil)
  1135. (refresh-pages)))))))
  1136. :icon "trash"
  1137. :variant :destructive
  1138. :icon-props {:size 14}
  1139. :size :sm})]
  1140. [:div.search-wrap.flex.items-center.pl-2
  1141. (let [search-fn (fn []
  1142. (let [^js input (rum/deref *search-input)]
  1143. (search-key (.-value input))
  1144. (reset! *current-page 1)))
  1145. reset-fn (fn []
  1146. (let [^js input (rum/deref *search-input)]
  1147. (set! (.-value input) "")
  1148. (reset! *search-key nil)))]
  1149. [(ui/button (ui/icon "search")
  1150. :on-click search-fn
  1151. :variant :link
  1152. :size :xs)
  1153. [:input.form-input {:placeholder (t :search/page-names)
  1154. :on-key-up (fn [^js e]
  1155. (let [^js target (.-target e)]
  1156. (if (string/blank? (.-value target))
  1157. (reset! *search-key nil)
  1158. (cond
  1159. (= 13 (.-keyCode e)) (search-fn)
  1160. (= 27 (.-keyCode e)) (reset-fn)))))
  1161. :ref *search-input
  1162. :default-value ""}]
  1163. (when (not (string/blank? @*search-key))
  1164. [:a.cancel {:on-click reset-fn}
  1165. (ui/icon "x")])])]]
  1166. [:div.r.flex.items-center.justify-between
  1167. [:div
  1168. (ui/select (->> (if (config/db-based-graph? current-repo)
  1169. ["" "class" "property" "whiteboard"]
  1170. ["" "whiteboard"])
  1171. (map (fn [block-type]
  1172. {:label (if (seq block-type) (string/capitalize block-type) "Filter by page type")
  1173. :selected (= block-type type)
  1174. :disabled config/publishing?
  1175. :value block-type})))
  1176. (fn [_e value]
  1177. (swap! *filters assoc :page-type value)))]
  1178. [:div
  1179. (ui/tippy
  1180. {:html [:small (t :page/show-journals)]
  1181. :arrow true}
  1182. [:a.button.journal
  1183. {:class (util/classnames [{:active (boolean (:journal? @*filters))}])
  1184. :on-click #(swap! *filters update :journal? not)}
  1185. (ui/icon "calendar" {:size ui/icon-size})])]
  1186. [:div.paginates
  1187. (pagination :current @*current-page
  1188. :total total-items
  1189. :per-page per-page-num
  1190. :on-change #(to-page %))]
  1191. (ui/dropdown-with-links
  1192. (fn [{:keys [toggle-fn]}]
  1193. [:a.button.fade-link
  1194. {:on-click toggle-fn}
  1195. (ui/icon "dots" {:size ui/icon-size})])
  1196. [{:title (t :remove-orphaned-pages)
  1197. :options {:on-click (fn []
  1198. (let [orphaned-pages (model/get-orphaned-pages {})
  1199. orphaned-pages? (seq orphaned-pages)]
  1200. (if orphaned-pages?
  1201. (state/set-modal!
  1202. (batch-delete-dialog
  1203. orphaned-pages true
  1204. #(do
  1205. (reset! *checks nil)
  1206. (refresh-pages))))
  1207. (notification/show! "Congratulations, no orphaned pages in your graph!" :success))))}
  1208. :icon (ui/icon "file-x")}
  1209. {:title (t :all-files)
  1210. :options {:href (rfe/href :all-files)}
  1211. :icon (ui/icon "files")}]
  1212. {})]]
  1213. [:table.table-auto.cp__all_pages_table
  1214. [:thead
  1215. [:tr
  1216. [:th.selector
  1217. (checkbox-opt "all-pages-select-all"
  1218. (= 1 @*indeterminate)
  1219. {:on-change (fn []
  1220. (let [indeterminate? (= -1 @*indeterminate)
  1221. all? (= 1 @*indeterminate)]
  1222. (doseq [{:block/keys [idx]} @*results]
  1223. (swap! *checks assoc idx (or indeterminate? (not all?))))))
  1224. :indeterminate (when (= -1 @*indeterminate) "indeterminate")})]
  1225. [:th.icon ""]
  1226. (sortable-title (t :block/name) :block/name *sort-by-item *desc?)
  1227. (when-not mobile?
  1228. [(sortable-title (t :page/backlinks) :block/backlinks *sort-by-item *desc?)
  1229. (sortable-title (t :page/created-at) :block/created-at *sort-by-item *desc?)
  1230. (sortable-title (t :page/updated-at) :block/updated-at *sort-by-item *desc?)])]]
  1231. [:tbody
  1232. (for [{:block/keys [idx name created-at updated-at backlinks] :as page} @*results]
  1233. (when-not (string/blank? name)
  1234. [:tr {:key name}
  1235. [:td.selector
  1236. (checkbox-opt (str "label-" idx)
  1237. (get @*checks idx)
  1238. {:on-change (fn []
  1239. (swap! *checks update idx not))})]
  1240. [:td.icon.w-4.p-0.overflow-hidden
  1241. (when-let [icon (get page (pu/get-pid :logseq.property/icon))]
  1242. icon)]
  1243. [:td.name [:a {:on-click (fn [e]
  1244. (.preventDefault e)
  1245. (let [repo (state/get-current-repo)]
  1246. (when (gobj/get e "shiftKey")
  1247. (state/sidebar-add-block!
  1248. repo
  1249. (:db/id page)
  1250. :page))))
  1251. :href (rfe/href :page {:name (:block/uuid page)})}
  1252. (component-block/page-cp {} page)]]
  1253. (when-not mobile?
  1254. [[:td.backlinks [:span backlinks]]
  1255. [:td.created-at [:span (if created-at
  1256. (date/int->local-time-2 created-at)
  1257. "Unknown")]]
  1258. [:td.updated-at [:span (if updated-at
  1259. (date/int->local-time-2 updated-at)
  1260. "Unknown")]]])]))]]
  1261. [:div.flex.justify-end.py-4
  1262. (pagination :current @*current-page
  1263. :total total-items
  1264. :per-page per-page-num
  1265. :on-change #(to-page %))]])]))