page.cljs 52 KB

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