page.cljs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. (ns frontend.handler.page
  2. "Provides util handler fns for pages"
  3. (:require [cljs.reader :as reader]
  4. [clojure.string :as string]
  5. [datascript.core :as d]
  6. [datascript.impl.entity :as de]
  7. [electron.ipc :as ipc]
  8. [frontend.commands :as commands]
  9. [frontend.config :as config]
  10. [frontend.context.i18n :refer [t]]
  11. [frontend.date :as date]
  12. [frontend.db :as db]
  13. [frontend.db.async :as db-async]
  14. [frontend.db.conn :as conn]
  15. [frontend.db.model :as model]
  16. [frontend.fs :as fs]
  17. ;; [frontend.handler.common :as common-handler]
  18. [frontend.handler.common.page :as page-common-handler]
  19. [frontend.handler.db-based.page :as db-page-handler]
  20. [frontend.handler.db-based.property :as db-property-handler]
  21. [frontend.handler.editor :as editor-handler]
  22. [frontend.handler.file-based.nfs :as nfs-handler]
  23. [frontend.handler.file-based.page-property :as file-page-property]
  24. [frontend.handler.graph :as graph-handler]
  25. [frontend.handler.notification :as notification]
  26. [frontend.handler.plugin :as plugin-handler]
  27. [frontend.handler.property :as property-handler]
  28. [frontend.handler.ui :as ui-handler]
  29. [frontend.mobile.util :as mobile-util]
  30. [frontend.modules.outliner.op :as outliner-op]
  31. [frontend.modules.outliner.ui :as ui-outliner-tx]
  32. [frontend.persist-db.browser :as db-browser]
  33. [frontend.state :as state]
  34. [frontend.util :as util]
  35. [frontend.util.cursor :as cursor]
  36. [frontend.util.page :as page-util]
  37. [frontend.util.url :as url-util]
  38. [goog.functions :refer [debounce]]
  39. [goog.object :as gobj]
  40. [lambdaisland.glogi :as log]
  41. [logseq.common.config :as common-config]
  42. [logseq.common.path :as path]
  43. [logseq.common.util :as common-util]
  44. [logseq.common.util.page-ref :as page-ref]
  45. [logseq.db :as ldb]
  46. [logseq.graph-parser.db :as gp-db]
  47. [logseq.graph-parser.text :as text]
  48. [promesa.core :as p]))
  49. (def <create! page-common-handler/<create!)
  50. (def <delete! page-common-handler/<delete!)
  51. (defn <unfavorite-page!
  52. [page-name]
  53. (p/do!
  54. (let [repo (state/get-current-repo)]
  55. (if (config/db-based-graph? repo)
  56. (when-let [page-block-uuid (:block/uuid (db/get-page page-name))]
  57. (page-common-handler/<db-unfavorite-page! page-block-uuid))
  58. (page-common-handler/file-unfavorite-page! page-name)))
  59. (state/update-favorites-updated!)))
  60. (defn <favorite-page!
  61. [page-name]
  62. (p/do!
  63. (let [repo (state/get-current-repo)]
  64. (if (config/db-based-graph? repo)
  65. (when-let [page-block-uuid (:block/uuid (db/get-page page-name))]
  66. (page-common-handler/<db-favorite-page! page-block-uuid))
  67. (page-common-handler/file-favorite-page! page-name)))
  68. (state/update-favorites-updated!)))
  69. (defn favorited?
  70. [page-name]
  71. (let [repo (state/get-current-repo)]
  72. (if (config/db-based-graph? repo)
  73. (boolean
  74. (when-let [page-block-uuid (:block/uuid (db/get-page page-name))]
  75. (page-common-handler/db-favorited? page-block-uuid)))
  76. (page-common-handler/file-favorited? page-name))))
  77. (defn get-favorites
  78. "return page-block entities"
  79. []
  80. (when-let [db (conn/get-db)]
  81. (let [repo (state/get-current-repo)]
  82. (if (config/db-based-graph? repo)
  83. (when-let [page (ldb/get-page db common-config/favorites-page-name)]
  84. (let [blocks (ldb/sort-by-order (:block/_parent page))]
  85. (keep (fn [block]
  86. (when-let [block-db-id (:db/id (:block/link block))]
  87. (d/entity db block-db-id))) blocks)))
  88. (let [page-names (->> (:favorites (state/sub-config))
  89. (remove string/blank?)
  90. (filter string?)
  91. (mapv util/safe-page-name-sanity-lc)
  92. (distinct))]
  93. (keep (fn [page-name] (db/get-page page-name)) page-names))))))
  94. ;; FIXME: add whiteboard
  95. (defn- get-directory
  96. [journal?]
  97. (if journal?
  98. (config/get-journals-directory)
  99. (config/get-pages-directory)))
  100. (defn- get-file-name
  101. [journal? title]
  102. (when-let [s (if journal?
  103. (date/journal-title->default title)
  104. ;; legacy in org-mode format, don't escape slashes except bug reported
  105. (common-util/page-name-sanity (string/lower-case title)))]
  106. ;; Win10 file path has a length limit of 260 chars
  107. (common-util/safe-subs s 0 200)))
  108. (defn toggle-favorite! []
  109. ;; NOTE: in journals or settings, current-page is nil
  110. (when-let [page-name (state/get-current-page)]
  111. (if (favorited? page-name)
  112. (<unfavorite-page! page-name)
  113. (<favorite-page! page-name))))
  114. (defn rename!
  115. [page-uuid-or-old-name new-name & {:as _opts}]
  116. (when @db-browser/*worker
  117. (p/let [page-uuid (cond
  118. (uuid? page-uuid-or-old-name)
  119. page-uuid-or-old-name
  120. (common-util/uuid-string? page-uuid-or-old-name)
  121. page-uuid-or-old-name
  122. :else
  123. (:block/uuid (db/get-page page-uuid-or-old-name)))
  124. result (ui-outliner-tx/transact!
  125. {:outliner-op :rename-page}
  126. (outliner-op/rename-page! page-uuid new-name))
  127. result' (ldb/read-transit-str result)]
  128. (case (if (string? result') (keyword result') result')
  129. :invalid-empty-name
  130. (notification/show! "Please use a valid name, empty name is not allowed!" :warning)
  131. :rename-page-exists
  132. (notification/show! "Another page with the new name exists already" :warning)
  133. nil))))
  134. (defn <reorder-favorites!
  135. [favorites]
  136. (let [conn (conn/get-db false)]
  137. (when-let [favorites-page (db/get-page common-config/favorites-page-name)]
  138. (let [favorite-page-block-db-id-coll
  139. (keep (fn [page-uuid]
  140. (:db/id (db/get-page page-uuid)))
  141. favorites)
  142. current-blocks (ldb/sort-by-order (ldb/get-page-blocks @conn (:db/id favorites-page)))]
  143. (p/do!
  144. (ui-outliner-tx/transact!
  145. {:outliner-op :reorder-favorites}
  146. (doseq [[page-block-db-id block] (zipmap favorite-page-block-db-id-coll current-blocks)]
  147. (when (not= page-block-db-id (:db/id (:block/link block)))
  148. (outliner-op/save-block! (assoc block :block/link page-block-db-id)))))
  149. (state/update-favorites-updated!))))))
  150. (defn has-more-journals?
  151. []
  152. (let [current-length (:journals-length @state/state)]
  153. (< current-length (db/get-journals-length))))
  154. (defn load-more-journals!
  155. []
  156. (when (has-more-journals?)
  157. (state/set-journals-length! (+ (:journals-length @state/state) 7))))
  158. (defn update-public-attribute!
  159. [repo page value]
  160. (if (config/db-based-graph? repo)
  161. (db-property-handler/set-block-property! [:block/uuid (:block/uuid page)] :logseq.property/publishing-public? value)
  162. (file-page-property/add-property! page :public value)))
  163. (defn get-page-ref-text
  164. [page]
  165. (let [edit-block-file-path (model/get-block-file-path (state/get-edit-block))
  166. page-name (string/lower-case page)]
  167. (if (and edit-block-file-path
  168. (state/org-mode-file-link? (state/get-current-repo)))
  169. (if-let [ref-file-path (:file/path (db/get-page-file page-name))]
  170. (util/format "[[file:%s][%s]]"
  171. (util/get-relative-path edit-block-file-path ref-file-path)
  172. page)
  173. (let [journal? (date/valid-journal-title? page)
  174. ref-file-path (str
  175. (if (or (util/electron?) (mobile-util/native-platform?))
  176. (-> (config/get-repo-dir (state/get-current-repo))
  177. js/decodeURI
  178. (string/replace #"/+$" "")
  179. (str "/"))
  180. "")
  181. (get-directory journal?)
  182. "/"
  183. (get-file-name journal? page)
  184. ".org")]
  185. (<create! page {:redirect? false})
  186. (util/format "[[file:%s][%s]]"
  187. (util/get-relative-path edit-block-file-path ref-file-path)
  188. page)))
  189. (page-ref/->page-ref page))))
  190. (defn init-commands!
  191. []
  192. (commands/init-commands! get-page-ref-text))
  193. (def rebuild-slash-commands-list!
  194. (debounce init-commands! 1500))
  195. (defn <template-exists?
  196. [title]
  197. (when title
  198. (p/let [result (db-async/<get-all-templates (state/get-current-repo))
  199. templates (keys result)]
  200. (when (seq templates)
  201. (let [templates (map string/lower-case templates)]
  202. (contains? (set templates) (string/lower-case title)))))))
  203. (defn ls-dir-files!
  204. ([ok-handler] (ls-dir-files! ok-handler nil))
  205. ([ok-handler opts]
  206. (nfs-handler/ls-dir-files-with-handler!
  207. (fn [e]
  208. (init-commands!)
  209. (when ok-handler
  210. (ok-handler e))
  211. (graph-handler/settle-metadata-to-local! {:created-at (js/Date.now)}))
  212. opts)))
  213. (comment
  214. (defn get-all-pages
  215. [repo]
  216. (let [db-based? (config/db-based-graph? repo)
  217. graph-specific-hidden?
  218. (if db-based?
  219. (fn [p]
  220. (and (ldb/property? p) (ldb/built-in? p)))
  221. (fn [p]
  222. (gp-db/built-in-pages-names (string/upper-case (:block/name p)))))]
  223. (cond->>
  224. (->> (db/get-all-pages repo)
  225. (remove graph-specific-hidden?))
  226. (not db-based?)
  227. (common-handler/fix-pages-timestamps)))))
  228. (defn get-filters
  229. [page]
  230. (if (config/db-based-graph? (state/get-current-repo))
  231. (let [included-pages (:logseq.property.linked-references/includes page)
  232. excluded-pages (:logseq.property.linked-references/excludes page)]
  233. {:included included-pages
  234. :excluded excluded-pages})
  235. (let [k :filters
  236. properties (:block/properties page)
  237. properties-str (or (get properties k) "{}")]
  238. (try (let [result (reader/read-string properties-str)]
  239. (when (seq result)
  240. (let [excluded-pages (->> (filter #(false? (second %)) result)
  241. (keep first)
  242. (keep db/get-page))
  243. included-pages (->> (filter #(true? (second %)) result)
  244. (keep first)
  245. (keep db/get-page))]
  246. {:included included-pages
  247. :excluded excluded-pages})))
  248. (catch :default e
  249. (log/error :syntax/filters e))))))
  250. (defn file-based-save-filter!
  251. [page filter-state]
  252. (property-handler/add-page-property! page :filters filter-state))
  253. (defn db-based-save-filter!
  254. [page filter-page-id {:keys [include? add?]}]
  255. (let [repo (state/get-current-repo)
  256. property-id (if include?
  257. :logseq.property.linked-references/includes
  258. :logseq.property.linked-references/excludes)]
  259. (if add?
  260. (property-handler/set-block-property! repo (:db/id page) property-id filter-page-id)
  261. (db-property-handler/delete-property-value! (:db/id page) property-id filter-page-id))))
  262. ;; Editor
  263. (defn page-not-exists-handler
  264. [input id q current-pos]
  265. (state/clear-editor-action!)
  266. (if (state/org-mode-file-link? (state/get-current-repo))
  267. (let [page-ref-text (get-page-ref-text q)
  268. value (gobj/get input "value")
  269. old-page-ref (page-ref/->page-ref q)
  270. new-value (string/replace value
  271. old-page-ref
  272. page-ref-text)]
  273. (state/set-edit-content! id new-value)
  274. (let [new-pos (+ current-pos
  275. (- (count page-ref-text)
  276. (count old-page-ref))
  277. 2)]
  278. (cursor/move-cursor-to input new-pos)))
  279. (let [current-selected (util/get-selected-text)]
  280. (cursor/move-cursor-forward input (+ 2 (count current-selected))))))
  281. (defn- tag-on-chosen-handler
  282. [input id pos format current-pos edit-content q db-based?]
  283. (fn [chosen-result ^js e]
  284. (util/stop e)
  285. (state/clear-editor-action!)
  286. (let [chosen-result (if (:block/uuid chosen-result)
  287. (db/entity [:block/uuid (:block/uuid chosen-result)])
  288. chosen-result)
  289. target (first (:block/_alias chosen-result))
  290. chosen-result (if (and target (not (ldb/class? chosen-result)) (ldb/class? target)) target chosen-result)
  291. chosen (:block/title chosen-result)
  292. class? (and db-based?
  293. (or (string/includes? chosen (str (t :new-tag) " "))
  294. (ldb/class? chosen-result)))
  295. inline-tag? (and class? (= (.-identifier e) "auto-complete/meta-complete"))
  296. chosen (-> chosen
  297. (string/replace-first (str (t :new-tag) " ") "")
  298. (string/replace-first (str (t :new-page) " ") ""))
  299. wrapped? (= page-ref/left-brackets (common-util/safe-subs edit-content (- pos 2) pos))
  300. chosen-last-part (if (text/namespace-page? chosen)
  301. (text/get-namespace-last-part chosen)
  302. chosen)
  303. wrapped-tag (if (and (util/safe-re-find #"\s+" chosen-last-part) (not wrapped?))
  304. (page-ref/->page-ref chosen-last-part)
  305. chosen-last-part)
  306. q (if (editor-handler/get-selected-text) "" q)
  307. last-pattern (if wrapped?
  308. q
  309. (if (= \# (first q))
  310. (subs q 1)
  311. q))
  312. last-pattern (str "#" (when wrapped? page-ref/left-brackets) last-pattern)
  313. tag-in-page-auto-complete? (= page-ref/right-brackets (common-util/safe-subs edit-content current-pos (+ current-pos 2)))]
  314. (p/do!
  315. (editor-handler/insert-command! id
  316. (if (and class? (not inline-tag?)) "" (str "#" wrapped-tag))
  317. format
  318. {:last-pattern last-pattern
  319. :end-pattern (when wrapped? page-ref/right-brackets)
  320. :command :page-ref})
  321. (when (and db-based? (not tag-in-page-auto-complete?))
  322. (db-page-handler/tag-on-chosen-handler chosen chosen-result class? edit-content current-pos last-pattern))
  323. (when input (.focus input))))))
  324. (defn- page-on-chosen-handler
  325. [id format q db-based?]
  326. (fn [chosen-result e]
  327. (util/stop e)
  328. (state/clear-editor-action!)
  329. (p/let [chosen-result (if (:block/uuid chosen-result)
  330. (db/entity [:block/uuid (:block/uuid chosen-result)])
  331. chosen-result)
  332. chosen (:block/title chosen-result)
  333. chosen' (string/replace-first chosen (str (t :new-page) " ") "")
  334. [chosen' chosen-result] (or (when (and (:nlp-date? chosen-result) (not (de/entity? chosen-result)))
  335. (when-let [result (date/nld-parse chosen')]
  336. (let [d (doto (goog.date.DateTime.) (.setTime (.getTime result)))
  337. gd (goog.date.Date. (.getFullYear d) (.getMonth d) (.getDate d))
  338. page (date/js-date->journal-title gd)]
  339. [page (db/get-page page)])))
  340. [chosen' chosen-result])
  341. ref-text (if (and (de/entity? chosen-result) (not (ldb/page? chosen-result)))
  342. (page-ref/->page-ref (:block/uuid chosen-result))
  343. (get-page-ref-text chosen'))
  344. result (when db-based?
  345. (when-not (de/entity? chosen-result)
  346. (<create! chosen'
  347. {:redirect? false
  348. :create-first-block? false
  349. :split-namespace? true})))
  350. ref-text' (if result
  351. (let [title (:block/title result)]
  352. (page-ref/->page-ref title))
  353. ref-text)]
  354. (p/do!
  355. (editor-handler/insert-command! id
  356. ref-text'
  357. format
  358. {:last-pattern (str page-ref/left-brackets (if (editor-handler/get-selected-text) "" q))
  359. :end-pattern page-ref/right-brackets
  360. :postfix-fn (fn [s] (util/replace-first page-ref/right-brackets s ""))
  361. :command :page-ref})
  362. (p/let [chosen-result (or result chosen-result)]
  363. (when (de/entity? chosen-result)
  364. (state/conj-block-ref! chosen-result)))))))
  365. (defn on-chosen-handler
  366. [input id pos format]
  367. (let [current-pos (cursor/pos input)
  368. edit-content (state/get-edit-content)
  369. action (state/get-editor-action)
  370. hashtag? (= action :page-search-hashtag)
  371. q (or
  372. (editor-handler/get-selected-text)
  373. (when hashtag?
  374. (common-util/safe-subs edit-content pos current-pos))
  375. (when (> (count edit-content) current-pos)
  376. (common-util/safe-subs edit-content pos current-pos)))
  377. db-based? (config/db-based-graph? (state/get-current-repo))]
  378. (if hashtag?
  379. (tag-on-chosen-handler input id pos format current-pos edit-content q db-based?)
  380. (page-on-chosen-handler id format q db-based?))))
  381. (defn create-today-journal!
  382. []
  383. (when-let [repo (state/get-current-repo)]
  384. (when (and (state/enable-journals? repo)
  385. ;; FIXME: There are a lot of long-running actions we don't want interrupted by this fn.
  386. ;; We should implement an app-wide check rather than list them all here
  387. (not (:graph/loading? @state/state))
  388. (not (:graph/importing @state/state))
  389. (not (state/loading-files? repo))
  390. (not config/publishing?))
  391. (state/set-today! (date/today))
  392. (when (or (config/db-based-graph? repo)
  393. (config/local-file-based-graph? repo)
  394. (and (= config/demo-repo repo) (not (mobile-util/native-platform?))))
  395. (let [title (date/today)
  396. today-page (util/page-name-sanity-lc title)
  397. format (state/get-preferred-format repo)
  398. template (state/get-default-journal-template)
  399. create-f (fn []
  400. (p/do!
  401. (<create! title {:redirect? false
  402. :split-namespace? false
  403. :create-first-block? (not template)
  404. :today-journal? true})
  405. (state/pub-event! [:journal/insert-template today-page])
  406. (ui-handler/re-render-root!)
  407. (plugin-handler/hook-plugin-app :today-journal-created {:title today-page})))]
  408. (when (db/page-empty? repo today-page)
  409. (if (config/db-based-graph? repo)
  410. (when-not (model/get-journal-page title)
  411. (create-f))
  412. (p/let [file-name (date/journal-title->default title)
  413. file-rpath (str (config/get-journals-directory) "/" file-name "."
  414. (config/get-file-extension format))
  415. repo-dir (config/get-repo-dir repo)
  416. file-exists? (fs/file-exists? repo-dir file-rpath)
  417. file-content (when file-exists?
  418. (fs/read-file repo-dir file-rpath))]
  419. (when (or (not file-exists?)
  420. (and file-exists? (string/blank? file-content)))
  421. (create-f))))))))))
  422. (defn open-today-in-sidebar
  423. []
  424. (when-let [page (db/get-page (date/today))]
  425. (state/sidebar-add-block!
  426. (state/get-current-repo)
  427. (:db/id page)
  428. :page)))
  429. (defn open-file-in-default-app []
  430. (if-let [file-rpath (and (util/electron?) (page-util/get-page-file-rpath))]
  431. (let [repo-dir (config/get-repo-dir (state/get-current-repo))
  432. file-fpath (path/path-join repo-dir file-rpath)]
  433. (js/window.apis.openPath file-fpath))
  434. (notification/show! "No file found" :warning)))
  435. (defn copy-current-file
  436. "FIXME: clarify usage, copy file or copy file path"
  437. []
  438. (if-let [file-rpath (and (util/electron?) (page-util/get-page-file-rpath))]
  439. (let [repo-dir (config/get-repo-dir (state/get-current-repo))
  440. file-fpath (path/path-join repo-dir file-rpath)]
  441. (util/copy-to-clipboard! file-fpath))
  442. (notification/show! "No file found" :warning)))
  443. (defn open-file-in-directory []
  444. (if-let [file-rpath (and (util/electron?) (page-util/get-page-file-rpath))]
  445. (let [repo-dir (config/get-repo-dir (state/get-current-repo))
  446. file-fpath (path/path-join repo-dir file-rpath)]
  447. (ipc/ipc "openFileInFolder" file-fpath))
  448. (notification/show! "No file found" :warning)))
  449. (defn copy-page-url
  450. ([]
  451. (let [id (if (config/db-based-graph? (state/get-current-repo))
  452. (page-util/get-current-page-uuid)
  453. (page-util/get-current-page-name))]
  454. (copy-page-url id)))
  455. ([page-uuid]
  456. (if page-uuid
  457. (util/copy-to-clipboard!
  458. (url-util/get-logseq-graph-page-url nil (state/get-current-repo) (str page-uuid)))
  459. (notification/show! "No page found to copy" :warning))))