page.cljs 19 KB

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