page.cljs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. (ns frontend.handler.page
  2. "Provides util handler fns for pages"
  3. (:require [cljs.reader :as reader]
  4. [clojure.string :as string]
  5. [frontend.commands :as commands]
  6. [frontend.config :as config]
  7. [frontend.date :as date]
  8. [frontend.db :as db]
  9. [frontend.db.async :as db-async]
  10. [frontend.db.model :as model]
  11. [frontend.fs :as fs]
  12. [frontend.handler.common :as common-handler]
  13. [frontend.handler.common.page :as page-common-handler]
  14. [frontend.handler.editor :as editor-handler]
  15. [frontend.handler.plugin :as plugin-handler]
  16. [frontend.handler.notification :as notification]
  17. [frontend.handler.property :as property-handler]
  18. [frontend.handler.db-based.property :as db-property-handler]
  19. [frontend.handler.ui :as ui-handler]
  20. [frontend.handler.file-based.nfs :as nfs-handler]
  21. [frontend.handler.graph :as graph-handler]
  22. [frontend.mobile.util :as mobile-util]
  23. [frontend.state :as state]
  24. [frontend.util :as util]
  25. [frontend.util.cursor :as cursor]
  26. [frontend.util.page :as page-util]
  27. [frontend.util.url :as url-util]
  28. [goog.functions :refer [debounce]]
  29. [goog.object :as gobj]
  30. [lambdaisland.glogi :as log]
  31. [logseq.common.config :as common-config]
  32. [logseq.common.util :as common-util]
  33. [logseq.common.util.page-ref :as page-ref]
  34. [logseq.common.util.block-ref :as block-ref]
  35. [promesa.core :as p]
  36. [logseq.common.path :as path]
  37. [electron.ipc :as ipc]
  38. [frontend.context.i18n :refer [t]]
  39. [frontend.persist-db.browser :as db-browser]
  40. [datascript.core :as d]
  41. [frontend.db.conn :as conn]
  42. [logseq.db :as ldb]
  43. [logseq.graph-parser.db :as gp-db]
  44. [frontend.modules.outliner.ui :as ui-outliner-tx]
  45. [frontend.modules.outliner.op :as outliner-op]
  46. [frontend.handler.property.util :as pu]
  47. [datascript.impl.entity :as de]
  48. [frontend.handler.db-based.page :as db-page-handler]))
  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) 1))))
  158. (defn update-public-attribute!
  159. [page value]
  160. (property-handler/add-page-property! page (pu/get-pid :logseq.property/public) value))
  161. (defn get-page-ref-text
  162. [page]
  163. (let [edit-block-file-path (model/get-block-file-path (state/get-edit-block))
  164. page-name (string/lower-case page)]
  165. (if (and edit-block-file-path
  166. (state/org-mode-file-link? (state/get-current-repo)))
  167. (if-let [ref-file-path (:file/path (db/get-page-file page-name))]
  168. (util/format "[[file:%s][%s]]"
  169. (util/get-relative-path edit-block-file-path ref-file-path)
  170. page)
  171. (let [journal? (date/valid-journal-title? page)
  172. ref-file-path (str
  173. (if (or (util/electron?) (mobile-util/native-platform?))
  174. (-> (config/get-repo-dir (state/get-current-repo))
  175. js/decodeURI
  176. (string/replace #"/+$" "")
  177. (str "/"))
  178. "")
  179. (get-directory journal?)
  180. "/"
  181. (get-file-name journal? page)
  182. ".org")]
  183. (<create! page {:redirect? false})
  184. (util/format "[[file:%s][%s]]"
  185. (util/get-relative-path edit-block-file-path ref-file-path)
  186. page)))
  187. (page-ref/->page-ref page))))
  188. (defn init-commands!
  189. []
  190. (commands/init-commands! get-page-ref-text))
  191. (def rebuild-slash-commands-list!
  192. (debounce init-commands! 1500))
  193. (defn <template-exists?
  194. [title]
  195. (when title
  196. (p/let [result (db-async/<get-all-templates (state/get-current-repo))
  197. templates (keys result)]
  198. (when (seq templates)
  199. (let [templates (map string/lower-case templates)]
  200. (contains? (set templates) (string/lower-case title)))))))
  201. (defn ls-dir-files!
  202. ([ok-handler] (ls-dir-files! ok-handler nil))
  203. ([ok-handler opts]
  204. (nfs-handler/ls-dir-files-with-handler!
  205. (fn [e]
  206. (init-commands!)
  207. (when ok-handler
  208. (ok-handler e))
  209. (graph-handler/settle-metadata-to-local! {:created-at (js/Date.now)}))
  210. opts)))
  211. (defn get-all-pages
  212. [repo]
  213. (let [graph-specific-hidden?
  214. (if (config/db-based-graph? repo)
  215. (fn [p]
  216. (and (ldb/property? p) (ldb/built-in? p)))
  217. (fn [p]
  218. (gp-db/built-in-pages-names (string/upper-case (:block/name p)))))]
  219. (->> (db/get-all-pages repo)
  220. (remove (fn [p]
  221. (let [name (:block/name p)]
  222. (or (util/uuid-string? name)
  223. (common-config/draw? name)
  224. (graph-specific-hidden? p)))))
  225. (common-handler/fix-pages-timestamps))))
  226. (defn get-filters
  227. [page]
  228. (if (config/db-based-graph? (state/get-current-repo))
  229. (let [included-pages (:logseq.property.linked-references/includes page)
  230. excluded-pages (:logseq.property.linked-references/excludes page)]
  231. {:included included-pages
  232. :excluded excluded-pages})
  233. (let [k :filters
  234. properties (:block/properties page)
  235. properties-str (or (get properties k) "{}")]
  236. (try (let [result (reader/read-string properties-str)]
  237. (when (seq result)
  238. (let [excluded-pages (->> (filter #(false? (second %)) result)
  239. (keep first)
  240. (keep db/get-page))
  241. included-pages (->> (filter #(true? (second %)) result)
  242. (keep first)
  243. (keep db/get-page))]
  244. {:included included-pages
  245. :excluded excluded-pages})))
  246. (catch :default e
  247. (log/error :syntax/filters e))))))
  248. (defn file-based-save-filter!
  249. [page filter-state]
  250. (property-handler/add-page-property! page :filters filter-state))
  251. (defn db-based-save-filter!
  252. [page filter-page-id {:keys [include? add?]}]
  253. (let [repo (state/get-current-repo)
  254. property-id (if include?
  255. :logseq.property.linked-references/includes
  256. :logseq.property.linked-references/excludes)]
  257. (if add?
  258. (property-handler/set-block-property! repo (:db/id page) property-id filter-page-id)
  259. (db-property-handler/delete-property-value! (:db/id page) property-id filter-page-id))))
  260. ;; Editor
  261. (defn page-not-exists-handler
  262. [input id q current-pos]
  263. (state/clear-editor-action!)
  264. (if (state/org-mode-file-link? (state/get-current-repo))
  265. (let [page-ref-text (get-page-ref-text q)
  266. value (gobj/get input "value")
  267. old-page-ref (page-ref/->page-ref q)
  268. new-value (string/replace value
  269. old-page-ref
  270. page-ref-text)]
  271. (state/set-edit-content! id new-value)
  272. (let [new-pos (+ current-pos
  273. (- (count page-ref-text)
  274. (count old-page-ref))
  275. 2)]
  276. (cursor/move-cursor-to input new-pos)))
  277. (let [current-selected (util/get-selected-text)]
  278. (cursor/move-cursor-forward input (+ 2 (count current-selected))))))
  279. (defn- tag-on-chosen-handler
  280. [input id pos format current-pos edit-content q db-based?]
  281. (fn [chosen-result ^js e]
  282. (util/stop e)
  283. (state/clear-editor-action!)
  284. (let [chosen-result (if (:block/uuid chosen-result)
  285. (db/entity [:block/uuid (:block/uuid chosen-result)])
  286. chosen-result)
  287. target (first (:block/_alias chosen-result))
  288. chosen-result (if (and target (not (ldb/class? chosen-result)) (ldb/class? target)) target chosen-result)
  289. chosen (:block/title chosen-result)
  290. class? (and db-based?
  291. (or (string/includes? chosen (str (t :new-tag) " "))
  292. (ldb/class? chosen-result)))
  293. inline-tag? (and class? (= (.-identifier e) "auto-complete/meta-complete"))
  294. chosen (-> chosen
  295. (string/replace-first (str (t :new-tag) " ") "")
  296. (string/replace-first (str (t :new-page) " ") ""))
  297. wrapped? (= page-ref/left-brackets (common-util/safe-subs edit-content (- pos 2) pos))
  298. wrapped-tag (if (and (util/safe-re-find #"\s+" chosen) (not wrapped?))
  299. (page-ref/->page-ref chosen)
  300. chosen)
  301. q (if (editor-handler/get-selected-text) "" q)
  302. last-pattern (if wrapped?
  303. q
  304. (if (= \# (first q))
  305. (subs q 1)
  306. q))
  307. last-pattern (str "#" (when wrapped? page-ref/left-brackets) last-pattern)
  308. tag-in-page-auto-complete? (= page-ref/right-brackets (common-util/safe-subs edit-content current-pos (+ current-pos 2)))]
  309. (p/do!
  310. (editor-handler/insert-command! id
  311. (if (and class? (not inline-tag?)) "" (str "#" wrapped-tag))
  312. format
  313. {:last-pattern last-pattern
  314. :end-pattern (when wrapped? page-ref/right-brackets)
  315. :command :page-ref})
  316. (when (and db-based? (not tag-in-page-auto-complete?))
  317. (db-page-handler/tag-on-chosen-handler chosen chosen-result class? edit-content current-pos last-pattern))
  318. (when input (.focus input))))))
  319. (defn- page-on-chosen-handler
  320. [id format q db-based?]
  321. (fn [chosen-result e]
  322. (util/stop e)
  323. (state/clear-editor-action!)
  324. (p/let [chosen-result (if (:block/uuid chosen-result)
  325. (db/entity [:block/uuid (:block/uuid chosen-result)])
  326. chosen-result)
  327. chosen (:block/title chosen-result)
  328. chosen' (string/replace-first chosen (str (t :new-page) " ") "")
  329. [chosen' chosen-result] (or (when (and (:nlp-date? chosen-result) (not (de/entity? chosen-result)))
  330. (when-let [result (date/nld-parse chosen')]
  331. (let [d (doto (goog.date.DateTime.) (.setTime (.getTime result)))
  332. gd (goog.date.Date. (.getFullYear d) (.getMonth d) (.getDate d))
  333. page (date/js-date->journal-title gd)]
  334. [page (db/get-page page)])))
  335. [chosen' chosen-result])
  336. ref-text (if (and (de/entity? chosen-result) (not (ldb/page? chosen-result)))
  337. (cond
  338. db-based?
  339. (page-ref/->page-ref (:block/uuid chosen-result))
  340. :else
  341. (block-ref/->block-ref (:block/uuid chosen-result)))
  342. (get-page-ref-text chosen'))
  343. result (when db-based?
  344. (when-not (de/entity? chosen-result)
  345. (<create! chosen'
  346. {:redirect? false
  347. :create-first-block? false
  348. :split-namespace? true})))
  349. ref-text' (if result
  350. (let [title (if-let [parent (:logseq.property/parent result)]
  351. (str (:block/title parent) "/" (:block/title result))
  352. (:block/title result))]
  353. (page-ref/->page-ref title))
  354. ref-text)]
  355. (p/do!
  356. (editor-handler/insert-command! id
  357. ref-text'
  358. format
  359. {:last-pattern (str page-ref/left-brackets (if (editor-handler/get-selected-text) "" q))
  360. :end-pattern page-ref/right-brackets
  361. :postfix-fn (fn [s] (util/replace-first page-ref/right-brackets s ""))
  362. :command :page-ref})
  363. (p/let [chosen-result (or result chosen-result)]
  364. (when (de/entity? chosen-result)
  365. (state/conj-block-ref! chosen-result)))))))
  366. (defn on-chosen-handler
  367. [input id pos format]
  368. (let [current-pos (cursor/pos input)
  369. edit-content (state/get-edit-content)
  370. action (state/get-editor-action)
  371. hashtag? (= action :page-search-hashtag)
  372. q (or
  373. (editor-handler/get-selected-text)
  374. (when hashtag?
  375. (common-util/safe-subs edit-content pos current-pos))
  376. (when (> (count edit-content) current-pos)
  377. (common-util/safe-subs edit-content pos current-pos)))
  378. db-based? (config/db-based-graph? (state/get-current-repo))]
  379. (if hashtag?
  380. (tag-on-chosen-handler input id pos format current-pos edit-content q db-based?)
  381. (page-on-chosen-handler id format q db-based?))))
  382. (defn create-today-journal!
  383. []
  384. (when-let [repo (state/get-current-repo)]
  385. (when (and (state/enable-journals? repo)
  386. ;; FIXME: There are a lot of long-running actions we don't want interrupted by this fn.
  387. ;; We should implement an app-wide check rather than list them all here
  388. (not (:graph/loading? @state/state))
  389. (not (:graph/importing @state/state))
  390. (not (state/loading-files? repo))
  391. (not config/publishing?))
  392. (state/set-today! (date/today))
  393. (when (or (config/db-based-graph? repo)
  394. (config/local-file-based-graph? repo)
  395. (and (= config/demo-repo repo) (not (mobile-util/native-platform?))))
  396. (let [title (date/today)
  397. today-page (util/page-name-sanity-lc title)
  398. format (state/get-preferred-format repo)
  399. template (state/get-default-journal-template)
  400. create-f (fn []
  401. (p/do!
  402. (<create! title {:redirect? false
  403. :split-namespace? false
  404. :create-first-block? (not template)
  405. :journal? true
  406. :today-journal? true})
  407. (state/pub-event! [:journal/insert-template today-page])
  408. (ui-handler/re-render-root!)
  409. (plugin-handler/hook-plugin-app :today-journal-created {:title today-page})))]
  410. (when (db/page-empty? repo today-page)
  411. (if (config/db-based-graph? repo)
  412. (when-not (model/get-journal-page title)
  413. (create-f))
  414. (p/let [file-name (date/journal-title->default title)
  415. file-rpath (str (config/get-journals-directory) "/" file-name "."
  416. (config/get-file-extension format))
  417. repo-dir (config/get-repo-dir repo)
  418. file-exists? (fs/file-exists? repo-dir file-rpath)
  419. file-content (when file-exists?
  420. (fs/read-file repo-dir file-rpath))]
  421. (when (or (not file-exists?)
  422. (and file-exists? (string/blank? file-content)))
  423. (create-f))))))))))
  424. (defn open-today-in-sidebar
  425. []
  426. (when-let [page (db/get-page (date/today))]
  427. (state/sidebar-add-block!
  428. (state/get-current-repo)
  429. (:db/id page)
  430. :page)))
  431. (defn open-file-in-default-app []
  432. (if-let [file-rpath (and (util/electron?) (page-util/get-page-file-rpath))]
  433. (let [repo-dir (config/get-repo-dir (state/get-current-repo))
  434. file-fpath (path/path-join repo-dir file-rpath)]
  435. (js/window.apis.openPath file-fpath))
  436. (notification/show! "No file found" :warning)))
  437. (defn copy-current-file
  438. "FIXME: clarify usage, copy file or copy file path"
  439. []
  440. (if-let [file-rpath (and (util/electron?) (page-util/get-page-file-rpath))]
  441. (let [repo-dir (config/get-repo-dir (state/get-current-repo))
  442. file-fpath (path/path-join repo-dir file-rpath)]
  443. (util/copy-to-clipboard! file-fpath))
  444. (notification/show! "No file found" :warning)))
  445. (defn open-file-in-directory []
  446. (if-let [file-rpath (and (util/electron?) (page-util/get-page-file-rpath))]
  447. (let [repo-dir (config/get-repo-dir (state/get-current-repo))
  448. file-fpath (path/path-join repo-dir file-rpath)]
  449. (ipc/ipc "openFileInFolder" file-fpath))
  450. (notification/show! "No file found" :warning)))
  451. (defn copy-page-url
  452. ([]
  453. (let [id (if (config/db-based-graph? (state/get-current-repo))
  454. (page-util/get-current-page-uuid)
  455. (page-util/get-current-page-name))]
  456. (copy-page-url id)))
  457. ([page-uuid]
  458. (if page-uuid
  459. (util/copy-to-clipboard!
  460. (url-util/get-logseq-graph-page-url nil (state/get-current-repo) (str page-uuid)))
  461. (notification/show! "No page found to copy" :warning))))