page.cljs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. (ns frontend.worker.handler.page
  2. "Page operations"
  3. (:require [logseq.db :as ldb]
  4. [logseq.graph-parser.block :as gp-block]
  5. [logseq.graph-parser.property :as gp-property]
  6. [logseq.db.sqlite.util :as sqlite-util]
  7. [datascript.core :as d]
  8. [clojure.string :as string]
  9. [frontend.worker.date :as date]
  10. [logseq.graph-parser.text :as text]
  11. [logseq.common.util :as common-util]
  12. [logseq.common.config :as common-config]
  13. [logseq.db.frontend.content :as db-content]
  14. [medley.core :as medley]
  15. [logseq.db.frontend.schema :as db-schema]))
  16. (defn properties-block
  17. [repo conn config date-formatter properties format page]
  18. (let [content (gp-property/insert-properties repo format "" properties)
  19. refs (gp-block/get-page-refs-from-properties properties @conn date-formatter config)]
  20. {:block/pre-block? true
  21. :block/uuid (ldb/new-block-id)
  22. :block/properties properties
  23. :block/properties-order (keys properties)
  24. :block/refs refs
  25. :block/left page
  26. :block/format format
  27. :block/content content
  28. :block/parent page
  29. :block/page page}))
  30. (defn- build-page-tx [repo conn config date-formatter format properties page {:keys [whiteboard? class? tags]}]
  31. (when (:block/uuid page)
  32. (let [page-entity [:block/uuid (:block/uuid page)]
  33. page (merge page
  34. (when (seq properties) {:block/properties properties})
  35. (when whiteboard? {:block/type "whiteboard"})
  36. (when class? {:block/type "class"})
  37. (when tags {:block/tags (mapv #(hash-map :db/id
  38. (:db/id (d/entity @conn [:block/uuid %])))
  39. tags)}))
  40. page-empty? (ldb/page-empty? @conn (:block/name page))
  41. db-based? (sqlite-util/db-based-graph? repo)]
  42. (if (and (seq properties)
  43. (not whiteboard?)
  44. (not db-based?)
  45. page-empty?)
  46. [page (properties-block repo conn config date-formatter properties format page-entity)]
  47. [page]))))
  48. (defn get-title-and-pagename
  49. [title]
  50. (let [title (-> (string/trim title)
  51. (text/page-ref-un-brackets!)
  52. ;; remove `#` from tags
  53. (string/replace #"^#+" ""))
  54. title (common-util/remove-boundary-slashes title)
  55. page-name (common-util/page-name-sanity-lc title)]
  56. [title page-name]))
  57. (defn create!
  58. "Create page. Has the following options:
  59. * :create-first-block? - when true, create an empty block if the page is empty.
  60. * :uuid - when set, use this uuid instead of generating a new one.
  61. * :class? - when true, adds a :block/type 'class'
  62. * :whiteboard? - when true, adds a :block/type 'whiteboard'
  63. * :tags - tag uuids that are added to :block/tags
  64. * :persist-op? - when true, add an update-page op
  65. TODO: Add other options"
  66. [repo conn config title
  67. & {:keys [create-first-block? format properties uuid persist-op? whiteboard? class? today-journal?]
  68. :or {create-first-block? true
  69. format nil
  70. properties nil
  71. uuid nil
  72. persist-op? true}
  73. :as options}]
  74. (let [date-formatter (common-config/get-date-formatter config)
  75. split-namespace? (not (or (string/starts-with? title "hls__")
  76. (date/valid-journal-title? date-formatter title)))
  77. [title page-name] (get-title-and-pagename title)
  78. with-uuid? (if (uuid? uuid) uuid true)
  79. result (when (ldb/page-empty? @conn page-name)
  80. (let [pages (if split-namespace?
  81. (common-util/split-namespace-pages title)
  82. [title])
  83. format (or format (common-config/get-preferred-format config))
  84. pages (map (fn [page]
  85. ;; only apply uuid to the deepest hierarchy of page to create if provided.
  86. (-> (gp-block/page-name->map page (if (= page title) with-uuid? true) @conn true date-formatter)
  87. (assoc :block/format format)))
  88. pages)
  89. txs (->> pages
  90. ;; for namespace pages, only last page need properties
  91. drop-last
  92. (mapcat #(build-page-tx repo conn config date-formatter format nil % {}))
  93. (remove nil?))
  94. txs (map-indexed (fn [i page]
  95. (if (zero? i)
  96. page
  97. (assoc page :block/namespace
  98. [:block/uuid (:block/uuid (nth txs (dec i)))])))
  99. txs)
  100. page-txs (build-page-tx repo conn config date-formatter format properties (last pages) (select-keys options [:whiteboard? :class? :tags]))
  101. page-txs (if (seq txs)
  102. (update page-txs 0
  103. (fn [p]
  104. (assoc p :block/namespace [:block/uuid (:block/uuid (last txs))])))
  105. page-txs)
  106. first-block-tx (when (and
  107. create-first-block?
  108. (not (or whiteboard? class?))
  109. (ldb/page-empty? @conn (:db/id (d/entity @conn [:block/name page-name])))
  110. page-txs)
  111. (let [page-id [:block/uuid (:block/uuid (first page-txs))]]
  112. [(sqlite-util/block-with-timestamps
  113. {:block/uuid (ldb/new-block-id)
  114. :block/page page-id
  115. :block/parent page-id
  116. :block/left page-id
  117. :block/content ""
  118. :block/format format})]))
  119. txs (concat
  120. txs
  121. page-txs
  122. first-block-tx)]
  123. (when (seq txs)
  124. (ldb/transact! conn txs (cond-> {:persist-op? persist-op?}
  125. today-journal?
  126. (assoc :create-today-journal? true
  127. :today-journal-name page-name))))))] ;; FIXME: prettier validation
  128. [result page-name]))
  129. (defn db-refs->page
  130. "Replace [[page name]] with page name"
  131. [repo page-entity]
  132. (when (sqlite-util/db-based-graph? repo)
  133. (let [refs (:block/_refs page-entity)
  134. id-ref->page #(db-content/special-id-ref->page % [page-entity])]
  135. (when (seq refs)
  136. (let [tx-data (mapcat (fn [{:block/keys [raw-content properties] :as ref}]
  137. ;; block content or properties
  138. (let [content' (id-ref->page raw-content)
  139. content-tx (when (not= raw-content content')
  140. {:db/id (:db/id ref)
  141. :block/content content'})
  142. page-uuid (:block/uuid page-entity)
  143. properties' (-> (medley/map-vals (fn [v]
  144. (cond
  145. (and (coll? v) (uuid? (first v)))
  146. (vec (remove #{page-uuid} v))
  147. (and (uuid? v) (= v page-uuid))
  148. nil
  149. (and (coll? v) (string? (first v)))
  150. (mapv id-ref->page v)
  151. (string? v)
  152. (id-ref->page v)
  153. :else
  154. v)) properties)
  155. (common-util/remove-nils-non-nested))
  156. tx (merge
  157. content-tx
  158. (when (not= (seq properties) (seq properties'))
  159. {:db/id (:db/id ref)
  160. :block/properties properties'}))]
  161. (concat
  162. [[:db/retract (:db/id ref) :block/refs (:db/id page-entity)]]
  163. (when tx [tx])))) refs)]
  164. tx-data)))))
  165. (defn- page-unable-to-delete
  166. "If a page is unable to delete, returns a map with more information. Otherwise returns nil"
  167. [conn page]
  168. (try
  169. (cond
  170. (and (contains? (:block/type page) "class")
  171. (seq (ldb/get-tag-blocks @conn (:block/name page))))
  172. {:msg "Page content deleted but unable to delete this page because blocks are tagged with this page"}
  173. (contains? (:block/type page) "property")
  174. (cond (seq (ldb/get-classes-with-property @conn (:block/uuid page)))
  175. {:msg "Page content deleted but unable to delete this page because classes use this property"}
  176. (seq (ldb/get-block-property-values @conn (:block/uuid page)))
  177. {:msg "Page content deleted but unable to delete this page because blocks use this property"})
  178. (or (seq (:block/_refs page)) (contains? (:block/type page) "hidden"))
  179. {:msg "Page content deleted but unable to delete this page because there are still references to it"})
  180. (catch :default e
  181. (js/console.error e)
  182. {:msg (str "An unexpected failure while deleting: " e)})))
  183. (defn delete!
  184. "Deletes a page. Returns true if able to delete page. If unable to delete,
  185. calls error-handler fn and returns false"
  186. [repo conn page-name & {:keys [persist-op? rename? error-handler]
  187. :or {persist-op? true
  188. error-handler (fn [{:keys [msg]}] (js/console.error msg))}}]
  189. (when (and repo page-name)
  190. (let [page-name (common-util/page-name-sanity-lc page-name)
  191. page (d/entity @conn [:block/name page-name])
  192. blocks (:block/_page page)
  193. truncate-blocks-tx-data (mapv
  194. (fn [block]
  195. [:db.fn/retractEntity [:block/uuid (:block/uuid block)]])
  196. blocks)
  197. db-based? (sqlite-util/db-based-graph? repo)]
  198. (if (ldb/built-in? page)
  199. (do
  200. (error-handler {:msg "Built-in page cannot be deleted"})
  201. false)
  202. (if-let [msg (and db-based? (page-unable-to-delete conn page))]
  203. (do
  204. (ldb/transact! conn truncate-blocks-tx-data
  205. {:outliner-op :truncate-page-blocks :persist-op? persist-op?})
  206. (error-handler msg)
  207. false)
  208. (let [file (ldb/get-page-file @conn page-name)
  209. file-path (:file/path file)
  210. delete-file-tx (when file
  211. [[:db.fn/retractEntity [:file/path file-path]]])
  212. ;; if other page alias this pagename,
  213. ;; then just remove some attrs of this entity instead of retractEntity
  214. delete-page-tx (cond
  215. (or (and db-based? (not (:block/_namespace page)))
  216. (not db-based?))
  217. (if (and db-based? (ldb/get-alias-source-page @conn page-name))
  218. (when-let [id (:db/id (d/entity @conn [:block/name page-name]))]
  219. (mapv (fn [attribute]
  220. [:db/retract id attribute])
  221. db-schema/retract-page-attributes))
  222. (concat (db-refs->page repo page)
  223. [[:db.fn/retractEntity [:block/name page-name]]]))
  224. :else
  225. nil)
  226. tx-data (concat truncate-blocks-tx-data delete-page-tx delete-file-tx)]
  227. (ldb/transact! conn tx-data
  228. (cond-> {:outliner-op :delete-page
  229. :deleted-page page-name
  230. :persist-op? persist-op?}
  231. rename?
  232. (assoc :real-outliner-op :rename-page)
  233. file-path
  234. (assoc :file-path file-path)))
  235. true))))))