1
0

page.cljs 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. (ns frontend.handler.page
  2. "Provides util handler fns for pages"
  3. (:require [cljs.reader :as reader]
  4. [clojure.string :as string]
  5. [clojure.walk :as walk]
  6. [datascript.core :as d]
  7. [frontend.commands :as commands]
  8. [frontend.config :as config]
  9. [frontend.date :as date]
  10. [frontend.db :as db]
  11. [frontend.db.conn :as conn]
  12. [frontend.db.model :as model]
  13. [frontend.db.utils :as db-utils]
  14. [frontend.format.block :as block]
  15. [frontend.fs :as fs]
  16. [frontend.handler.common :as common-handler]
  17. [frontend.handler.config :as config-handler]
  18. [frontend.handler.editor :as editor-handler]
  19. [frontend.handler.plugin :as plugin-handler]
  20. [frontend.handler.notification :as notification]
  21. [frontend.handler.file-based.recent :as recent-handler]
  22. [frontend.handler.route :as route-handler]
  23. [frontend.handler.ui :as ui-handler]
  24. [frontend.handler.web.nfs :as web-nfs]
  25. [frontend.mobile.util :as mobile-util]
  26. [frontend.modules.outliner.core :as outliner-core]
  27. [frontend.modules.outliner.file :as outliner-file]
  28. [frontend.modules.outliner.tree :as outliner-tree]
  29. [frontend.state :as state]
  30. [frontend.util :as util]
  31. [frontend.util.cursor :as cursor]
  32. [frontend.util.fs :as fs-util]
  33. [frontend.util.page-property :as page-property]
  34. [frontend.util.page :as page-util]
  35. [frontend.util.property-edit :as property-edit]
  36. [frontend.util.url :as url-util]
  37. [goog.functions :refer [debounce]]
  38. [goog.object :as gobj]
  39. [lambdaisland.glogi :as log]
  40. [logseq.db.schema :as db-schema]
  41. [logseq.graph-parser.block :as gp-block]
  42. [logseq.graph-parser.config :as gp-config]
  43. [logseq.graph-parser.property :as gp-property]
  44. [logseq.graph-parser.text :as text]
  45. [logseq.graph-parser.util :as gp-util]
  46. [logseq.graph-parser.util.page-ref :as page-ref]
  47. [promesa.core :as p]
  48. [logseq.common.path :as path]))
  49. ;; FIXME: add whiteboard
  50. (defn- get-directory
  51. [journal?]
  52. (if journal?
  53. (config/get-journals-directory)
  54. (config/get-pages-directory)))
  55. (defn- get-file-name
  56. [journal? title]
  57. (when-let [s (if journal?
  58. (date/journal-title->default title)
  59. ;; legacy in org-mode format, don't escape slashes except bug reported
  60. (gp-util/page-name-sanity (string/lower-case title)))]
  61. ;; Win10 file path has a length limit of 260 chars
  62. (gp-util/safe-subs s 0 200)))
  63. (defn- build-title [page]
  64. ;; Don't wrap `\"` anymore, as tiitle property is not effected by `,` now
  65. ;; The previous extract behavior isn't unwrapping the `'"` either. So no need
  66. ;; to maintain the compatibility.
  67. (:block/original-name page))
  68. (defn default-properties-block
  69. ([title format page]
  70. (default-properties-block title format page {}))
  71. ([title format page properties]
  72. (let [p (common-handler/get-page-default-properties title)
  73. ps (merge p properties)
  74. content (page-property/insert-properties format "" ps)
  75. refs (gp-block/get-page-refs-from-properties properties
  76. (db/get-db (state/get-current-repo))
  77. (state/get-date-formatter)
  78. (state/get-config))]
  79. {:block/uuid (db/new-block-id)
  80. :block/pre-block? true
  81. :block/properties ps
  82. :block/properties-order (keys ps)
  83. :block/refs refs
  84. :block/left page
  85. :block/format format
  86. :block/content content
  87. :block/parent page
  88. :block/page page})))
  89. (defn- create-title-property?
  90. [journal? page-name]
  91. (and (not (config/db-based-graph? (state/get-current-repo)))
  92. (not journal?)
  93. (= (state/get-filename-format) :legacy) ;; reduce title computation
  94. (fs-util/create-title-property? page-name)))
  95. (defn- build-page-tx [repo format properties page journal? whiteboard?]
  96. (when (:block/uuid page)
  97. (let [page-entity [:block/uuid (:block/uuid page)]
  98. title (util/get-page-original-name page)
  99. create-title? (create-title-property? journal? title)
  100. page (merge page
  101. (when (seq properties) {:block/properties properties})
  102. (when whiteboard? {:block/type "whiteboard"}))
  103. page-empty? (db/page-empty? (state/get-current-repo) (:block/name page))]
  104. (cond
  105. (not page-empty?)
  106. [page]
  107. (and create-title?
  108. (not whiteboard?))
  109. (let [properties-block (default-properties-block (build-title page) format page-entity properties)]
  110. [page
  111. properties-block])
  112. (and (seq properties)
  113. (not whiteboard?)
  114. (not (config/db-based-graph? (state/get-current-repo))))
  115. [page (editor-handler/properties-block repo properties format page-entity)]
  116. :else
  117. [page]))))
  118. (defn create!
  119. "Create page.
  120. :redirect? - when true, redirect to the created page, otherwise return sanitized page name.
  121. :split-namespace? - when true, split hierarchical namespace into levels.
  122. :create-first-block? - when true, create an empty block if the page is empty.
  123. :uuid - when set, use this uuid instead of generating a new one."
  124. ([title]
  125. (create! title {}))
  126. ([title {:keys [redirect? create-first-block? format properties split-namespace? journal? uuid whiteboard? rename?]
  127. :or {redirect? true
  128. create-first-block? true
  129. rename? false
  130. format nil
  131. properties nil
  132. split-namespace? true
  133. uuid nil}}]
  134. (let [title (-> (string/trim title)
  135. (text/page-ref-un-brackets!)
  136. ;; remove `#` from tags
  137. (string/replace #"^#+" ""))
  138. title (gp-util/remove-boundary-slashes title)
  139. page-name (util/page-name-sanity-lc title)
  140. repo (state/get-current-repo)
  141. with-uuid? (if (uuid? uuid) uuid true)] ;; FIXME: prettier validation
  142. (when (or (db/page-empty? repo page-name) rename?)
  143. (let [pages (if split-namespace?
  144. (gp-util/split-namespace-pages title)
  145. [title])
  146. format (or format (state/get-preferred-format))
  147. pages (map (fn [page]
  148. ;; only apply uuid to the deepest hierarchy of page to create if provided.
  149. (-> (block/page-name->map page (if (= page title) with-uuid? true))
  150. (assoc :block/format format)))
  151. pages)
  152. txs (->> pages
  153. ;; for namespace pages, only last page need properties
  154. drop-last
  155. (mapcat #(build-page-tx repo format nil % journal? false))
  156. (remove nil?))
  157. txs (map-indexed (fn [i page]
  158. (if (zero? i)
  159. page
  160. (assoc page :block/namespace
  161. [:block/uuid (:block/uuid (nth txs (dec i)))])))
  162. txs)
  163. last-txs (build-page-tx repo format properties (last pages) journal? whiteboard?)
  164. last-txs (if (seq txs)
  165. (update last-txs 0
  166. (fn [p]
  167. (assoc p :block/namespace [:block/uuid (:block/uuid (last txs))])))
  168. last-txs)
  169. txs (concat
  170. (when (and rename? uuid)
  171. (when-let [e (db/entity [:block/uuid uuid])]
  172. [[:db/retract (:db/id e) :block/namespace]
  173. [:db/retract (:db/id e) :block/refs]]))
  174. txs
  175. last-txs)]
  176. (when (seq txs)
  177. (db/transact! txs)))
  178. (when create-first-block?
  179. (when (or
  180. (db/page-empty? repo (:db/id (db/entity [:block/name page-name])))
  181. (create-title-property? journal? page-name))
  182. (editor-handler/api-insert-new-block! "" {:page page-name}))))
  183. (when redirect?
  184. (route-handler/redirect-to-page! page-name))
  185. page-name)))
  186. (defn delete-file!
  187. [repo page-name unlink-file?]
  188. (let [file (db/get-page-file page-name)
  189. file-path (:file/path file)]
  190. ;; delete file
  191. (when-not (string/blank? file-path)
  192. (db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
  193. (when unlink-file?
  194. (-> (fs/unlink! repo (config/get-repo-fpath repo file-path) nil)
  195. (p/catch (fn [error] (js/console.error error))))))))
  196. (defn- compute-new-file-path
  197. "Construct the full path given old full path and the file sanitized body.
  198. Ext. included in the `old-path`."
  199. [old-path new-file-name-body]
  200. (let [result (string/split old-path "/")
  201. ext (last (string/split (last result) "."))
  202. new-file (str new-file-name-body "." ext)
  203. parts (concat (butlast result) [new-file])]
  204. (util/string-join-path parts)))
  205. (defn rename-file!
  206. "emit file-rename events to :file/rename-event-chan
  207. force-fs? - when true, rename file event the db transact is failed."
  208. ([file new-file-name-body ok-handler]
  209. (rename-file! file new-file-name-body ok-handler false))
  210. ([file new-file-name-body ok-handler force-fs?]
  211. (let [repo (state/get-current-repo)
  212. file (db/pull (:db/id file))
  213. old-path (:file/path file)
  214. new-path (compute-new-file-path old-path new-file-name-body)
  215. transact #(db/transact! repo [{:db/id (:db/id file)
  216. :file/path new-path}])]
  217. ;; update db
  218. (if force-fs?
  219. (try (transact) ;; capture error and continue FS rename if failed
  220. (catch :default e
  221. (log/error :rename-file e)))
  222. (transact)) ;; interrupted if failed
  223. (->
  224. (p/let [_ (state/offer-file-rename-event-chan! {:repo repo
  225. :old-path old-path
  226. :new-path new-path})
  227. _ (fs/rename! repo old-path new-path)]
  228. (ok-handler))
  229. (p/catch (fn [error]
  230. (println "file rename failed: " error)))))))
  231. (defn- replace-page-ref!
  232. "Unsanitized names"
  233. [content old-name new-name]
  234. (let [[original-old-name original-new-name] (map string/trim [old-name new-name])
  235. [old-ref new-ref] (map page-ref/->page-ref [old-name new-name])
  236. [old-name new-name] (map #(if (string/includes? % "/")
  237. (string/replace % "/" ".")
  238. %)
  239. [original-old-name original-new-name])
  240. old-org-ref (and (= :org (state/get-preferred-format))
  241. (:org-mode/insert-file-link? (state/get-config))
  242. (re-find
  243. (re-pattern
  244. (util/format
  245. "\\[\\[file:\\.*/.*%s\\.org\\]\\[(.*?)\\]\\]" old-name))
  246. content))]
  247. (-> (if old-org-ref
  248. (let [[old-full-ref old-label] old-org-ref
  249. new-label (if (= old-label original-old-name)
  250. original-new-name
  251. old-label)
  252. new-full-ref (-> (string/replace old-full-ref old-name new-name)
  253. (string/replace (str "[" old-label "]")
  254. (str "[" new-label "]")))]
  255. (string/replace content old-full-ref new-full-ref))
  256. content)
  257. (string/replace old-ref new-ref))))
  258. (defn- replace-tag-ref!
  259. [content old-name new-name]
  260. (let [old-tag (util/format "#%s" old-name)
  261. new-tag (if (re-find #"[\s\t]+" new-name)
  262. (util/format "#[[%s]]" new-name)
  263. (str "#" new-name))]
  264. ;; hash tag parsing rules https://github.com/logseq/mldoc/blob/701243eaf9b4157348f235670718f6ad19ebe7f8/test/test_markdown.ml#L631
  265. ;; Safari doesn't support look behind, don't use
  266. ;; TODO: parse via mldoc
  267. (string/replace content
  268. (re-pattern (str "(?i)(^|\\s)(" (util/escape-regex-chars old-tag) ")(?=[,\\.]*($|\\s))"))
  269. ;; case_insense^ ^lhs ^_grp2 look_ahead^ ^_grp3
  270. (fn [[_match lhs _grp2 _grp3]]
  271. (str lhs new-tag)))))
  272. (defn- replace-property-ref!
  273. [content old-name new-name format]
  274. (let [new-name (keyword (string/replace (string/lower-case new-name) #"\s+" "-"))
  275. org-format? (= :org format)
  276. old-property (if org-format? (gp-property/colons-org old-name) (str old-name gp-property/colons))
  277. new-property (if org-format? (gp-property/colons-org (name new-name)) (str (name new-name) gp-property/colons))]
  278. (util/replace-ignore-case content old-property new-property)))
  279. (defn- replace-old-page!
  280. "Unsanitized names"
  281. [content old-name new-name format]
  282. (when (and (string? content) (string? old-name) (string? new-name))
  283. (-> content
  284. (replace-page-ref! old-name new-name)
  285. (replace-tag-ref! old-name new-name)
  286. (replace-property-ref! old-name new-name format))))
  287. (defn- walk-replace-old-page!
  288. "Unsanitized names"
  289. [form old-name new-name format]
  290. (walk/postwalk (fn [f]
  291. (cond
  292. (and (vector? f)
  293. (contains? #{"Search" "Label"} (first f))
  294. (string/starts-with? (second f) (str old-name "/")))
  295. [(first f) (string/replace-first (second f)
  296. (str old-name "/")
  297. (str new-name "/"))]
  298. (string? f)
  299. (if (= f old-name)
  300. new-name
  301. (replace-old-page! f old-name new-name format))
  302. (and (keyword f) (= (name f) old-name))
  303. (keyword (string/replace (string/lower-case new-name) #"\s+" "-"))
  304. :else
  305. f))
  306. form))
  307. (defn favorited?
  308. [page-name]
  309. (let [favorites (->> (:favorites (state/get-config))
  310. (filter string?)
  311. (map string/lower-case)
  312. (set))]
  313. (contains? favorites page-name)))
  314. (defn favorite-page!
  315. [page-name]
  316. (when-not (string/blank? page-name)
  317. (let [favorites (->
  318. (cons
  319. page-name
  320. (or (:favorites (state/get-config)) []))
  321. (distinct)
  322. (vec))]
  323. (config-handler/set-config! :favorites favorites))))
  324. (defn unfavorite-page!
  325. [page-name]
  326. (when-not (string/blank? page-name)
  327. (let [old-favorites (:favorites (state/get-config))
  328. new-favorites (->> old-favorites
  329. (remove #(= (string/lower-case %) (string/lower-case page-name)))
  330. (vec))]
  331. (when-not (= old-favorites new-favorites)
  332. (config-handler/set-config! :favorites new-favorites)))))
  333. (defn toggle-favorite! []
  334. ;; NOTE: in journals or settings, current-page is nil
  335. (when-let [page-name (state/get-current-page)]
  336. (let [favorites (:favorites (state/sub-config))
  337. favorited? (contains? (set (map string/lower-case favorites))
  338. (string/lower-case page-name))]
  339. (if favorited?
  340. (unfavorite-page! page-name)
  341. (favorite-page! page-name)))))
  342. (defn delete!
  343. [page-name ok-handler & {:keys [delete-file?]
  344. :or {delete-file? true}}]
  345. (route-handler/redirect-to-home!)
  346. (when page-name
  347. (when-let [repo (state/get-current-repo)]
  348. (let [page-name (util/page-name-sanity-lc page-name)
  349. blocks (db/get-page-blocks-no-cache page-name)
  350. tx-data (mapv
  351. (fn [block]
  352. [:db.fn/retractEntity [:block/uuid (:block/uuid block)]])
  353. blocks)
  354. page (db/entity [:block/name page-name])]
  355. (db/transact! tx-data)
  356. (delete-file! repo page-name delete-file?)
  357. ;; if other page alias this pagename,
  358. ;; then just remove some attrs of this entity instead of retractEntity
  359. (when-not (:block/_namespace page)
  360. (if (model/get-alias-source-page (state/get-current-repo) page-name)
  361. (when-let [id (:db/id (db/entity [:block/name page-name]))]
  362. (let [txs (mapv (fn [attribute]
  363. [:db/retract id attribute])
  364. db-schema/retract-page-attributes)]
  365. (db/transact! txs)))
  366. (db/transact! [[:db.fn/retractEntity [:block/name page-name]]])))
  367. (unfavorite-page! page-name)
  368. (when (fn? ok-handler) (ok-handler))
  369. (ui-handler/re-render-root!)))))
  370. (defn- rename-update-block-refs!
  371. [refs from-id to-id]
  372. (->> refs
  373. (remove #{{:db/id from-id}})
  374. (cons {:db/id to-id})
  375. (distinct)
  376. (vec)))
  377. (defn- rename-update-refs!
  378. "Unsanitized only"
  379. [page old-original-name new-name]
  380. ;; update all pages which have references to this page
  381. (let [repo (state/get-current-repo)
  382. to-page (db/entity [:block/name (util/page-name-sanity-lc new-name)])
  383. blocks (:block/_refs (db/entity (:db/id page)))
  384. page-ids (->> (map (fn [b]
  385. {:db/id (:db/id (:block/page b))}) blocks)
  386. (set))
  387. tx (->> (map (fn [{:block/keys [uuid content properties format] :as block}]
  388. (let [content (let [content' (replace-old-page! content old-original-name new-name format)]
  389. (when-not (= content' content)
  390. content'))
  391. properties (let [properties' (walk-replace-old-page! properties old-original-name new-name format)]
  392. (when-not (= properties' properties)
  393. properties'))]
  394. (when (or content properties)
  395. (util/remove-nils-non-nested
  396. {:block/uuid uuid
  397. :block/content content
  398. :block/properties properties
  399. :block/properties-order (when (seq properties)
  400. (map first properties))
  401. :block/refs (->> (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page))
  402. (map :db/id)
  403. (set))})))) blocks)
  404. (remove nil?))]
  405. (db/transact! repo tx)
  406. (doseq [page-id page-ids]
  407. (outliner-file/sync-to-file page-id))))
  408. (defn- rename-update-namespace!
  409. "update :block/namespace of the renamed block"
  410. [page old-original-name new-name]
  411. (let [old-namespace? (text/namespace-page? old-original-name)
  412. new-namespace? (text/namespace-page? new-name)
  413. update-namespace! (fn [] (let [namespace (first (gp-util/split-last "/" new-name))]
  414. (when namespace
  415. (create! namespace {:redirect? false}) ;; create parent page if not exist, creation of namespace ref is handled in `create!`
  416. (let [namespace-block (db/pull [:block/name (gp-util/page-name-sanity-lc namespace)])
  417. repo (state/get-current-repo)
  418. page-txs [{:db/id (:db/id page)
  419. :block/namespace (:db/id namespace-block)}]]
  420. (d/transact! (db/get-db repo false) page-txs)))))
  421. remove-namespace! (fn []
  422. (db/transact! [[:db/retract (:db/id page) :block/namespace]]))]
  423. (when old-namespace?
  424. (if new-namespace?
  425. (update-namespace!)
  426. (remove-namespace!)))
  427. (when-not old-namespace?
  428. (when new-namespace?
  429. (update-namespace!)))))
  430. (defn- rename-page-aux
  431. "Only accepts unsanitized page names"
  432. [old-name new-name redirect?]
  433. (let [old-page-name (util/page-name-sanity-lc old-name)
  434. new-file-name-body (fs-util/file-name-sanity new-name) ;; w/o file extension
  435. new-page-name (util/page-name-sanity-lc new-name)
  436. repo (state/get-current-repo)
  437. page (db/pull [:block/name old-page-name])]
  438. (when (and repo page)
  439. (let [old-original-name (:block/original-name page)
  440. file (:block/file page)
  441. journal? (:block/journal? page)
  442. properties-block (:data (outliner-tree/-get-down (outliner-core/block page)))
  443. properties-content (:block/content properties-block)
  444. properties-block-tx (when (and properties-block
  445. properties-content
  446. (string/includes? (util/page-name-sanity-lc properties-content)
  447. old-page-name))
  448. (let [front-matter? (and (property-edit/front-matter?-when-file-based properties-content)
  449. (= :markdown (:block/format properties-block)))]
  450. {:db/id (:db/id properties-block)
  451. :block/content (property-edit/insert-property-when-file-based
  452. repo
  453. (:block/format properties-block)
  454. properties-content
  455. :title
  456. new-name
  457. front-matter?)}))
  458. page-txs [{:db/id (:db/id page)
  459. :block/uuid (:block/uuid page)
  460. :block/name new-page-name
  461. :block/original-name new-name}]
  462. page-txs (if properties-block-tx (conj page-txs properties-block-tx) page-txs)]
  463. (d/transact! (db/get-db repo false) page-txs)
  464. (when (fs-util/create-title-property? new-page-name)
  465. (page-property/add-property! new-page-name :title new-name))
  466. (when (and file (not journal?))
  467. (rename-file! file new-file-name-body (fn [] nil)))
  468. (let [home (get (state/get-config) :default-home {})]
  469. (when (= old-page-name (util/page-name-sanity-lc (get home :page "")))
  470. (config-handler/set-config! :default-home (assoc home :page new-name))))
  471. (rename-update-refs! page old-original-name new-name)
  472. (rename-update-namespace! page old-original-name new-name)
  473. (outliner-file/sync-to-file page))
  474. ;; Redirect to the newly renamed page
  475. (when redirect?
  476. (route-handler/redirect! {:to (if (model/whiteboard-page? page) :whiteboard :page)
  477. :push false
  478. :path-params {:name new-page-name}}))
  479. (when (favorited? old-page-name)
  480. (p/do!
  481. (unfavorite-page! old-page-name)
  482. (favorite-page! new-page-name)))
  483. (recent-handler/update-or-add-renamed-page repo old-page-name new-page-name)
  484. (ui-handler/re-render-root!))))
  485. (defn- rename-nested-pages
  486. "Unsanitized names only"
  487. [old-ns-name new-ns-name]
  488. (let [repo (state/get-current-repo)
  489. nested-page-str (page-ref/->page-ref (util/page-name-sanity-lc old-ns-name))
  490. ns-prefix-format-str (str page-ref/left-brackets "%s/")
  491. ns-prefix (util/format ns-prefix-format-str (util/page-name-sanity-lc old-ns-name))
  492. nested-pages (db/get-pages-by-name-partition repo nested-page-str)
  493. nested-pages-ns (db/get-pages-by-name-partition repo ns-prefix)]
  494. (when nested-pages
  495. ;; rename page "[[obsidian]] is a tool" to "[[logseq]] is a tool"
  496. (doseq [{:block/keys [name original-name]} nested-pages]
  497. (let [old-page-title (or original-name name)
  498. new-page-title (string/replace
  499. old-page-title
  500. (page-ref/->page-ref old-ns-name)
  501. (page-ref/->page-ref new-ns-name))]
  502. (when (and old-page-title new-page-title)
  503. (p/do!
  504. (rename-page-aux old-page-title new-page-title false)
  505. (println "Renamed " old-page-title " to " new-page-title))))))
  506. (when nested-pages-ns
  507. ;; rename page "[[obsidian/page1]] is a tool" to "[[logseq/page1]] is a tool"
  508. (doseq [{:block/keys [name original-name]} nested-pages-ns]
  509. (let [old-page-title (or original-name name)
  510. new-page-title (string/replace
  511. old-page-title
  512. (util/format ns-prefix-format-str old-ns-name)
  513. (util/format ns-prefix-format-str new-ns-name))]
  514. (when (and old-page-title new-page-title)
  515. (p/do!
  516. (rename-page-aux old-page-title new-page-title false)
  517. (println "Renamed " old-page-title " to " new-page-title))))))))
  518. (defn- rename-namespace-pages!
  519. "Original names (unsanitized only)"
  520. [repo old-name new-name]
  521. (let [pages (db/get-namespace-pages repo old-name)
  522. page (db/pull [:block/name (util/page-name-sanity-lc old-name)])
  523. pages (cons page pages)]
  524. (doseq [{:block/keys [name original-name]} pages]
  525. (let [old-page-title (or original-name name)
  526. ;; only replace one time, for the case that the namespace is a sub-string of the sub-namespace page name
  527. ;; Example: has pages [[work]] [[work/worklog]],
  528. ;; we want to rename [[work/worklog]] to [[work1/worklog]] when rename [[work]] to [[work1]],
  529. ;; but don't rename [[work/worklog]] to [[work1/work1log]]
  530. new-page-title (string/replace-first old-page-title old-name new-name)
  531. redirect? (= name (:block/name page))]
  532. (when (and old-page-title new-page-title)
  533. (p/let [_ (rename-page-aux old-page-title new-page-title redirect?)]
  534. (println "Renamed " old-page-title " to " new-page-title)))))))
  535. (defn merge-pages!
  536. "Only accepts sanitized page names"
  537. [from-page-name to-page-name]
  538. (when (and (db/page-exists? from-page-name)
  539. (db/page-exists? to-page-name)
  540. (not= from-page-name to-page-name))
  541. (let [to-page (db/entity [:block/name to-page-name])
  542. to-id (:db/id to-page)
  543. from-page (db/entity [:block/name from-page-name])
  544. from-id (:db/id from-page)
  545. from-first-child (some->> (db/pull from-id)
  546. (outliner-core/block)
  547. (outliner-tree/-get-down)
  548. (outliner-core/get-data))
  549. to-last-direct-child-id (model/get-block-last-direct-child (db/get-db) to-id false)
  550. repo (state/get-current-repo)
  551. conn (conn/get-db repo false)
  552. datoms (d/datoms @conn :avet :block/page from-id)
  553. block-eids (mapv :e datoms)
  554. blocks (db-utils/pull-many repo '[:db/id :block/page :block/refs :block/path-refs :block/left :block/parent] block-eids)
  555. tx-data (map (fn [block]
  556. (let [id (:db/id block)]
  557. (cond->
  558. {:db/id id
  559. :block/page {:db/id to-id}
  560. :block/refs (rename-update-block-refs! (:block/refs block) from-id to-id)}
  561. (and from-first-child (= id (:db/id from-first-child)))
  562. (assoc :block/left {:db/id (or to-last-direct-child-id to-id)})
  563. (= (:block/parent block) {:db/id from-id})
  564. (assoc :block/parent {:db/id to-id})))) blocks)]
  565. (d/transact! conn tx-data)
  566. (outliner-file/sync-to-file {:db/id to-id})
  567. (rename-update-refs! from-page
  568. (util/get-page-original-name from-page)
  569. (util/get-page-original-name to-page))
  570. (rename-update-namespace! from-page
  571. (util/get-page-original-name from-page)
  572. (util/get-page-original-name to-page)))
  573. (delete! from-page-name nil)
  574. (route-handler/redirect! {:to :page
  575. :push false
  576. :path-params {:name to-page-name}})))
  577. (defn db-based-rename!
  578. ([old-name new-name]
  579. (db-based-rename! old-name new-name true))
  580. ([old-name new-name redirect?]
  581. (let [old-name (string/trim old-name)
  582. new-name (string/trim new-name)
  583. old-page-name (util/page-name-sanity-lc old-name)
  584. page-e (db/entity [:block/name old-page-name])
  585. new-page-name (util/page-name-sanity-lc new-name)
  586. name-changed? (not= old-name new-name)]
  587. (if (and old-name
  588. new-name
  589. (not (string/blank? new-name))
  590. name-changed?)
  591. (if (and (not= old-page-name new-page-name)
  592. (db/entity [:block/name new-page-name]))
  593. (when (string/blank? new-name)
  594. (notification/show! "Merging pages is not supported yet." :info))
  595. (create! new-page-name
  596. {:rename? true
  597. :uuid (:block/uuid page-e)
  598. :redirect? redirect?
  599. :create-first-block? false}))
  600. (when (string/blank? new-name)
  601. (notification/show! "Please use a valid name, empty name is not allowed!" :error)))
  602. (ui-handler/re-render-root!))))
  603. (defn file-based-rename!
  604. "Accepts unsanitized page names"
  605. ([old-name new-name] (file-based-rename! old-name new-name true))
  606. ([old-name new-name redirect?]
  607. (let [repo (state/get-current-repo)
  608. old-name (string/trim old-name)
  609. new-name (string/trim new-name)
  610. old-page-name (util/page-name-sanity-lc old-name)
  611. new-page-name (util/page-name-sanity-lc new-name)
  612. name-changed? (not= old-name new-name)]
  613. (if (and old-name
  614. new-name
  615. (not (string/blank? new-name))
  616. name-changed?)
  617. (do
  618. (cond
  619. (= old-page-name new-page-name)
  620. (rename-page-aux old-name new-name redirect?)
  621. (db/pull [:block/name new-page-name])
  622. (merge-pages! old-page-name new-page-name)
  623. :else
  624. (rename-namespace-pages! repo old-name new-name))
  625. (rename-nested-pages old-name new-name))
  626. (when (string/blank? new-name)
  627. (notification/show! "Please use a valid name, empty name is not allowed!" :error)))
  628. (ui-handler/re-render-root!))))
  629. (defn rename!
  630. ([old-name new-name] (rename! old-name new-name true))
  631. ([old-name new-name redirect?]
  632. (prn "db based? " (config/db-based-graph? (state/get-current-repo)))
  633. (let [f (if (config/db-based-graph? (state/get-current-repo))
  634. db-based-rename!
  635. file-based-rename!)]
  636. (f old-name new-name redirect?))))
  637. (defn- split-col-by-element
  638. [col element]
  639. (let [col (vec col)
  640. idx (.indexOf col element)]
  641. [(subvec col 0 (inc idx))
  642. (subvec col (inc idx))]))
  643. (defn reorder-favorites!
  644. [{:keys [to up?]}]
  645. (let [favorites (:favorites (state/get-config))
  646. from (get @state/state :favorites/dragging)]
  647. (when (and from to (not= from to))
  648. (let [[prev next] (split-col-by-element favorites to)
  649. [prev next] (mapv #(remove (fn [e] (= from e)) %) [prev next])
  650. favorites (->>
  651. (if up?
  652. (concat (drop-last prev) [from (last prev)] next)
  653. (concat prev [from] next))
  654. (remove nil?)
  655. distinct
  656. vec)]
  657. (config-handler/set-config! :favorites favorites)))))
  658. (defn has-more-journals?
  659. []
  660. (let [current-length (:journals-length @state/state)]
  661. (< current-length (db/get-journals-length))))
  662. (defn load-more-journals!
  663. []
  664. (when (has-more-journals?)
  665. (state/set-journals-length! (+ (:journals-length @state/state) 7))))
  666. (defn update-public-attribute!
  667. [page-name value]
  668. (page-property/add-property! page-name :public value))
  669. (defn get-page-ref-text
  670. [page]
  671. (let [edit-block-file-path (model/get-block-file-path (state/get-edit-block))
  672. page-name (string/lower-case page)]
  673. (if (and edit-block-file-path
  674. (state/org-mode-file-link? (state/get-current-repo)))
  675. (if-let [ref-file-path (:file/path (db/get-page-file page-name))]
  676. (util/format "[[file:%s][%s]]"
  677. (util/get-relative-path edit-block-file-path ref-file-path)
  678. page)
  679. (let [journal? (date/valid-journal-title? page)
  680. ref-file-path (str
  681. (if (or (util/electron?) (mobile-util/native-platform?))
  682. (-> (config/get-repo-dir (state/get-current-repo))
  683. js/decodeURI
  684. (string/replace #"/+$" "")
  685. (str "/"))
  686. "")
  687. (get-directory journal?)
  688. "/"
  689. (get-file-name journal? page)
  690. ".org")]
  691. (create! page {:redirect? false})
  692. (util/format "[[file:%s][%s]]"
  693. (util/get-relative-path edit-block-file-path ref-file-path)
  694. page)))
  695. (page-ref/->page-ref page))))
  696. (defn init-commands!
  697. []
  698. (commands/init-commands! get-page-ref-text))
  699. (def rebuild-slash-commands-list!
  700. (debounce init-commands! 1500))
  701. (defn template-exists?
  702. [title]
  703. (when title
  704. (let [templates (keys (db/get-all-templates))]
  705. (when (seq templates)
  706. (let [templates (map string/lower-case templates)]
  707. (contains? (set templates) (string/lower-case title)))))))
  708. (defn ls-dir-files!
  709. ([ok-handler] (ls-dir-files! ok-handler nil))
  710. ([ok-handler opts]
  711. (web-nfs/ls-dir-files-with-handler!
  712. (fn [e]
  713. (init-commands!)
  714. (when ok-handler
  715. (ok-handler e)))
  716. opts)))
  717. (defn get-all-pages
  718. [repo]
  719. (->> (db/get-all-pages repo)
  720. (remove (fn [p]
  721. (let [name (:block/name p)]
  722. (or (util/uuid-string? name)
  723. (gp-config/draw? name)
  724. (db/built-in-pages-names (string/upper-case name))))))
  725. (common-handler/fix-pages-timestamps)))
  726. (defn get-filters
  727. [page-name]
  728. (let [properties (db/get-page-properties page-name)
  729. properties-str (get properties :filters "{}")]
  730. (try (reader/read-string properties-str)
  731. (catch :default e
  732. (log/error :syntax/filters e)))))
  733. (defn save-filter!
  734. [page-name filter-state]
  735. (page-property/add-property! page-name :filters filter-state))
  736. ;; Editor
  737. (defn page-not-exists-handler
  738. [input id q current-pos]
  739. (state/clear-editor-action!)
  740. (if (state/org-mode-file-link? (state/get-current-repo))
  741. (let [page-ref-text (get-page-ref-text q)
  742. value (gobj/get input "value")
  743. old-page-ref (page-ref/->page-ref q)
  744. new-value (string/replace value
  745. old-page-ref
  746. page-ref-text)]
  747. (state/set-edit-content! id new-value)
  748. (let [new-pos (+ current-pos
  749. (- (count page-ref-text)
  750. (count old-page-ref))
  751. 2)]
  752. (cursor/move-cursor-to input new-pos)))
  753. (let [current-selected (util/get-selected-text)]
  754. (cursor/move-cursor-forward input (+ 2 (count current-selected))))))
  755. (defn on-chosen-handler
  756. [input id _q pos format]
  757. (let [current-pos (cursor/pos input)
  758. edit-content (state/sub :editor/content :path-in-sub-atom id)
  759. action (state/get-editor-action)
  760. hashtag? (= action :page-search-hashtag)
  761. q (or
  762. (editor-handler/get-selected-text)
  763. (when hashtag?
  764. (gp-util/safe-subs edit-content pos current-pos))
  765. (when (> (count edit-content) current-pos)
  766. (gp-util/safe-subs edit-content pos current-pos)))]
  767. (if hashtag?
  768. (fn [chosen _click?]
  769. (state/clear-editor-action!)
  770. (let [wrapped? (= page-ref/left-brackets (gp-util/safe-subs edit-content (- pos 2) pos))
  771. chosen (if (and (util/safe-re-find #"\s+" chosen) (not wrapped?))
  772. (page-ref/->page-ref chosen)
  773. chosen)
  774. q (if (editor-handler/get-selected-text) "" q)
  775. last-pattern (if wrapped?
  776. q
  777. (if (= \# (first q))
  778. (subs q 1)
  779. q))
  780. last-pattern (str "#" (when wrapped? page-ref/left-brackets) last-pattern)]
  781. (editor-handler/insert-command! id
  782. (str "#" (when wrapped? page-ref/left-brackets) chosen)
  783. format
  784. {:last-pattern last-pattern
  785. :end-pattern (when wrapped? page-ref/right-brackets)
  786. :command :page-ref})))
  787. (fn [chosen _click?]
  788. (state/clear-editor-action!)
  789. (let [page-ref-text (get-page-ref-text chosen)]
  790. (editor-handler/insert-command! id
  791. page-ref-text
  792. format
  793. {:last-pattern (str page-ref/left-brackets (if (editor-handler/get-selected-text) "" q))
  794. :end-pattern page-ref/right-brackets
  795. :postfix-fn (fn [s] (util/replace-first page-ref/right-brackets s ""))
  796. :command :page-ref}))))))
  797. (defn create-today-journal!
  798. []
  799. (when-let [repo (state/get-current-repo)]
  800. (when (and (state/enable-journals? repo)
  801. (not (state/loading-files? repo))
  802. (not (state/whiteboard-route?)))
  803. (state/set-today! (date/today))
  804. (when (or (config/db-based-graph? repo)
  805. (config/local-file-based-graph? repo)
  806. (and (= "local" repo) (not (mobile-util/native-platform?))))
  807. (let [title (date/today)
  808. today-page (util/page-name-sanity-lc title)
  809. format (state/get-preferred-format repo)
  810. template (state/get-default-journal-template)
  811. create-f (fn []
  812. (create! title {:redirect? false
  813. :split-namespace? false
  814. :create-first-block? (not template)
  815. :journal? true})
  816. (state/pub-event! [:journal/insert-template today-page])
  817. (ui-handler/re-render-root!)
  818. (plugin-handler/hook-plugin-app :today-journal-created {:title today-page}))]
  819. (when (db/page-empty? repo today-page)
  820. (if (config/db-based-graph? repo)
  821. (create-f)
  822. (p/let [file-name (date/journal-title->default title)
  823. file-rpath (str (config/get-journals-directory) "/" file-name "."
  824. (config/get-file-extension format))
  825. repo-dir (config/get-repo-dir repo)
  826. file-exists? (fs/file-exists? repo-dir file-rpath)
  827. file-content (when file-exists?
  828. (fs/read-file repo-dir file-rpath))]
  829. (when (or (not file-exists?)
  830. (and file-exists? (string/blank? file-content)))
  831. (create-f))))))))))
  832. (defn open-today-in-sidebar
  833. []
  834. (when-let [page (db/entity [:block/name (util/page-name-sanity-lc (date/today))])]
  835. (state/sidebar-add-block!
  836. (state/get-current-repo)
  837. (:db/id page)
  838. :page)))
  839. (defn open-file-in-default-app []
  840. (if-let [file-rpath (and (util/electron?) (page-util/get-page-file-rpath))]
  841. (let [repo-dir (config/get-repo-dir (state/get-current-repo))
  842. file-fpath (path/path-join repo-dir file-rpath)]
  843. (js/window.apis.openPath file-fpath))
  844. (notification/show! "No file found" :warning)))
  845. (defn copy-current-file
  846. "FIXME: clarify usage, copy file or copy file path"
  847. []
  848. (if-let [file-rpath (and (util/electron?) (page-util/get-page-file-rpath))]
  849. (let [repo-dir (config/get-repo-dir (state/get-current-repo))
  850. file-fpath (path/path-join repo-dir file-rpath)]
  851. (util/copy-to-clipboard! file-fpath))
  852. (notification/show! "No file found" :warning)))
  853. (defn open-file-in-directory []
  854. (if-let [file-rpath (and (util/electron?) (page-util/get-page-file-rpath))]
  855. (let [repo-dir (config/get-repo-dir (state/get-current-repo))
  856. file-fpath (path/path-join repo-dir file-rpath)]
  857. (js/window.apis.showItemInFolder file-fpath))
  858. (notification/show! "No file found" :warning)))
  859. (defn copy-page-url
  860. ([] (copy-page-url (page-util/get-current-page-name)))
  861. ([page-name]
  862. (if page-name
  863. (util/copy-to-clipboard!
  864. (url-util/get-logseq-graph-page-url nil (state/get-current-repo) page-name))
  865. (notification/show! "No page found to copy" :warning))))