whiteboard.cljs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. (ns frontend.handler.whiteboard
  2. "Whiteboard related handlers"
  3. (:require [datascript.core :as d]
  4. [dommy.core :as dom]
  5. [frontend.db :as db]
  6. [frontend.db.model :as model]
  7. [frontend.db.utils :as db-utils]
  8. [frontend.handler.editor :as editor-handler]
  9. [frontend.handler.route :as route-handler]
  10. [frontend.modules.editor.undo-redo :as history]
  11. [frontend.modules.outliner.core :as outliner]
  12. [frontend.modules.outliner.file :as outliner-file]
  13. [frontend.state :as state]
  14. [frontend.util :as util]
  15. [logseq.graph-parser.util :as gp-util]
  16. [logseq.graph-parser.whiteboard :as gp-whiteboard]
  17. [promesa.core :as p]
  18. [goog.object :as gobj]
  19. [clojure.set :as set]
  20. [clojure.string :as string]
  21. [cljs-bean.core :as bean]))
  22. (defn js->clj-keywordize
  23. [obj]
  24. (js->clj obj :keywordize-keys true))
  25. (defn shape->block [shape page-name]
  26. (let [properties {:ls-type :whiteboard-shape
  27. :logseq.tldraw.shape shape}
  28. block {:block/page {:block/name (util/page-name-sanity-lc page-name)}
  29. :block/parent {:block/name page-name}
  30. :block/properties properties}
  31. additional-props (gp-whiteboard/with-whiteboard-block-props block page-name)]
  32. (merge block additional-props)))
  33. (defn- get-whiteboard-clj [page-name]
  34. (when (model/page-exists? page-name)
  35. (let [page-block (model/get-page page-name)
  36. ;; fixme: can we use cache?
  37. blocks (model/get-page-blocks-no-cache page-name)]
  38. [page-block blocks])))
  39. (defn- build-shapes
  40. [page-block blocks]
  41. (let [shapes-index (get-in page-block [:block/properties :logseq.tldraw.page :shapes-index])
  42. shape-id->index (zipmap shapes-index (range 0 (count shapes-index)))]
  43. (->> blocks
  44. (map (fn [block]
  45. (assoc block :index (get shape-id->index (str (:block/uuid block)) 0))))
  46. (sort-by :index)
  47. (filter gp-whiteboard/shape-block?)
  48. (map gp-whiteboard/block->shape))))
  49. (defn- whiteboard-clj->tldr [page-block blocks]
  50. (let [id (str (:block/uuid page-block))
  51. shapes (build-shapes page-block blocks)
  52. tldr-page (gp-whiteboard/page-block->tldr-page page-block)
  53. assets (:assets tldr-page)
  54. tldr-page (dissoc tldr-page :assets)]
  55. (clj->js {:currentPageId id
  56. :assets (or assets #js[])
  57. :selectedIds #js[]
  58. :pages [(merge tldr-page
  59. {:id id
  60. :name (:block/name page-block)
  61. :shapes shapes})]})))
  62. (defn build-page-block
  63. [page-name tldraw-page assets shapes-index]
  64. (let [page-entity (model/get-page page-name)
  65. get-k #(gobj/get tldraw-page %)]
  66. {:block/name page-name
  67. :block/type "whiteboard"
  68. :block/properties {:ls-type :whiteboard-page
  69. :logseq.tldraw.page {:id (get-k "id")
  70. :name (get-k "name")
  71. :bindings (js->clj-keywordize (get-k "bindings"))
  72. :nonce (get-k "nonce")
  73. :assets (js->clj-keywordize assets)
  74. :shapes-index shapes-index}}
  75. :block/updated-at (util/time-ms)
  76. :block/created-at (or (:block/created-at page-entity)
  77. (util/time-ms))}))
  78. (defn- compute-tx
  79. [^js app ^js tl-page new-id-nonces db-id-nonces page-name replace?]
  80. (let [assets (js->clj-keywordize (.getCleanUpAssets app))
  81. new-shapes (.-shapes tl-page)
  82. shapes-index (map #(gobj/get % "id") new-shapes)
  83. upsert-shapes (->> (set/difference new-id-nonces db-id-nonces)
  84. (map (fn [{:keys [id]}]
  85. (-> (.-serialized ^js (.getShapeById tl-page id))
  86. js->clj-keywordize)))
  87. (set))
  88. old-ids (set (map :id db-id-nonces))
  89. new-ids (set (map :id new-id-nonces))
  90. created-ids (->> (set/difference new-ids old-ids)
  91. (remove string/blank?)
  92. (set))
  93. created-shapes (set (filter #(created-ids (:id %)) upsert-shapes))
  94. deleted-ids (->> (set/difference old-ids new-ids)
  95. (remove string/blank?))
  96. repo (state/get-current-repo)
  97. deleted-shapes (when (seq deleted-ids)
  98. (->> (db/pull-many repo '[*] (mapv (fn [id] [:block/uuid (uuid id)]) deleted-ids))
  99. (map (fn [b]
  100. (get-in b [:block/properties :logseq.tldraw.shape])))))
  101. deleted-shapes-tx (mapv (fn [id] [:db/retractEntity [:block/uuid (uuid id)]]) deleted-ids)
  102. with-timestamps (fn [block]
  103. (if (contains? created-ids (str (:block/uuid block)))
  104. (assoc block :block/updated-at (util/time-ms))
  105. (outliner/block-with-timestamps block)))
  106. changed-shapes (set/difference upsert-shapes created-shapes)
  107. prev-changed-blocks (when (seq changed-shapes)
  108. (db/pull-many repo '[*] (mapv (fn [shape]
  109. [:block/uuid (uuid (:id shape))]) changed-shapes)))]
  110. {:page-block (build-page-block page-name tl-page assets shapes-index)
  111. :upserted-blocks (->> upsert-shapes
  112. (map #(shape->block % page-name))
  113. (map with-timestamps))
  114. :delete-blocks deleted-shapes-tx
  115. :metadata {:whiteboard/transact? true
  116. :replace? replace?
  117. :data {:page-name page-name
  118. :deleted-shapes deleted-shapes
  119. :new-shapes created-shapes
  120. :changed-shapes changed-shapes
  121. :prev-changed-blocks prev-changed-blocks}}}))
  122. (defonce *last-shapes-nonce (atom {}))
  123. (defn transact-tldr-delta! [page-name ^js app replace?]
  124. (let [tl-page ^js (second (first (.-pages app)))
  125. shapes (.-shapes ^js tl-page)
  126. new-id-nonces (set (map (fn [shape]
  127. {:id (.-id shape)
  128. :nonce (.-nonce shape)}) shapes))
  129. repo (state/get-current-repo)
  130. db-id-nonces (or
  131. (get-in @*last-shapes-nonce [repo page-name])
  132. (set (->> (model/get-whiteboard-id-nonces repo page-name)
  133. (map #(update % :id str)))))
  134. {:keys [page-block upserted-blocks delete-blocks metadata]}
  135. (compute-tx app tl-page new-id-nonces db-id-nonces page-name replace?)
  136. tx-data (concat delete-blocks [page-block] upserted-blocks)
  137. new-shapes (get-in metadata [:data :new-shapes])
  138. metadata' (cond
  139. ;; group
  140. (some #(= "group" (:type %)) new-shapes)
  141. (assoc metadata :whiteboard/op :group)
  142. ;; ungroup
  143. (some #(= "group" (:type %)) (get-in metadata [:data :deleted-shapes]))
  144. (assoc metadata :whiteboard/op :un-group)
  145. ;; arrow
  146. (some #(and (= "line" (:type %))
  147. (= "arrow "(:end (:decorations %)))) new-shapes)
  148. (assoc metadata :whiteboard/op :new-arrow)
  149. :else
  150. metadata)
  151. metadata' (if (seq (concat upserted-blocks delete-blocks))
  152. metadata'
  153. (assoc metadata :undo? true))]
  154. (swap! *last-shapes-nonce assoc-in [repo page-name] new-id-nonces)
  155. (if (contains? #{:new-arrow} (:whiteboard/op metadata'))
  156. (state/set-state! :whiteboard/pending-tx-data
  157. {:tx-data tx-data
  158. :metadata metadata'})
  159. (let [pending-tx-data (:whiteboard/pending-tx-data @state/state)
  160. tx-data' (concat (:tx-data pending-tx-data) tx-data)
  161. metadata'' (merge metadata' (:metadata pending-tx-data))]
  162. (state/set-state! :whiteboard/pending-tx-data {})
  163. (db-utils/transact! repo tx-data' metadata'')))))
  164. (defn get-default-new-whiteboard-tx
  165. [page-name id]
  166. [#:block{:name page-name,
  167. :type "whiteboard",
  168. :properties
  169. {:ls-type :whiteboard-page,
  170. :logseq.tldraw.page
  171. {:id id,
  172. :name page-name,
  173. :ls-type :whiteboard-page,
  174. :bindings {},
  175. :nonce 1,
  176. :assets []}},
  177. :updated-at (util/time-ms),
  178. :created-at (util/time-ms)}])
  179. (defn get-whiteboard-entity [page-name]
  180. (db-utils/entity [:block/name (util/page-name-sanity-lc page-name)]))
  181. (defn create-new-whiteboard-page!
  182. ([]
  183. (create-new-whiteboard-page! nil))
  184. ([name]
  185. (let [uuid (or (and name (parse-uuid name)) (d/squuid))
  186. name (or name (str uuid))]
  187. (db/transact! (get-default-new-whiteboard-tx name (str uuid)))
  188. (let [entity (get-whiteboard-entity name)
  189. tx (assoc (select-keys entity [:db/id])
  190. :block/uuid uuid)]
  191. (db-utils/transact! [tx])
  192. (let [page-entity (get-whiteboard-entity name)]
  193. (when (and page-entity (nil? (:block/file page-entity)))
  194. (outliner-file/sync-to-file page-entity)))))))
  195. (defn create-new-whiteboard-and-redirect!
  196. ([]
  197. (create-new-whiteboard-and-redirect! (str (d/squuid))))
  198. ([name]
  199. (create-new-whiteboard-page! name)
  200. (route-handler/redirect-to-whiteboard! name)))
  201. (defn ->logseq-portal-shape
  202. [block-id point]
  203. {:blockType (if (parse-uuid (str block-id)) "B" "P")
  204. :id (str (d/squuid))
  205. :compact false
  206. :pageId (str block-id)
  207. :point point
  208. :size [400, 0]
  209. :type "logseq-portal"})
  210. (defn add-new-block-portal-shape!
  211. "Given the block uuid, add a new shape to the referenced block.
  212. By default it will be placed next to the given shape id"
  213. [block-uuid source-shape & {:keys [link? bottom?]}]
  214. (when-let [app (state/active-tldraw-app)]
  215. (let [^js api (.-api app)
  216. point (-> (.getShapeById app source-shape)
  217. (.-bounds)
  218. ((fn [bounds] (if bottom?
  219. [(.-minX bounds) (+ 64 (.-maxY bounds))]
  220. [(+ 64 (.-maxX bounds)) (.-minY bounds)]))))
  221. shape (->logseq-portal-shape block-uuid point)]
  222. (when (uuid? block-uuid) (editor-handler/set-blocks-id! [block-uuid]))
  223. (.createShapes api (clj->js shape))
  224. (when link?
  225. (.createNewLineBinding api source-shape (:id shape))))))
  226. (defn page-name->tldr!
  227. ([page-name]
  228. (clj->js
  229. (if page-name
  230. (if-let [[page-block blocks] (get-whiteboard-clj page-name)]
  231. (whiteboard-clj->tldr page-block blocks)
  232. (create-new-whiteboard-page! page-name))
  233. (create-new-whiteboard-page! nil)))))
  234. (defn- get-whiteboard-blocks
  235. "Given a page, return all the logseq blocks (exclude all shapes)"
  236. [page-name]
  237. (let [blocks (model/get-page-blocks-no-cache page-name)]
  238. (remove gp-whiteboard/shape-block? blocks)))
  239. (defn- get-last-root-block
  240. "Get the last root Logseq block in the page. Main purpose is to calculate the new :block/left id"
  241. [page-name]
  242. (let [page-id (:db/id (model/get-page page-name))
  243. blocks (get-whiteboard-blocks page-name)
  244. root-blocks (filter (fn [block] (= page-id (:db/id (:block/parent block)))) blocks)
  245. root-block-left-ids (->> root-blocks
  246. (map (fn [block] (get-in block [:block/left :db/id] nil)))
  247. (remove nil?)
  248. (set))
  249. blocks-with-no-next (remove #(root-block-left-ids (:db/id %)) root-blocks)]
  250. (when (seq blocks-with-no-next) (first blocks-with-no-next))))
  251. (defn add-new-block!
  252. [page-name content]
  253. (let [uuid (d/squuid)
  254. page-entity (model/get-page page-name)
  255. last-root-block (or (get-last-root-block page-name) page-entity)
  256. tx {:block/left (select-keys last-root-block [:db/id])
  257. :block/uuid uuid
  258. :block/content (or content "")
  259. :block/format :markdown ;; fixme to support org?
  260. :block/page {:block/name (util/page-name-sanity-lc page-name)}
  261. :block/parent {:block/name page-name}}]
  262. (db-utils/transact! [tx])
  263. uuid))
  264. (defn inside-portal?
  265. [target]
  266. (some? (dom/closest target ".tl-logseq-cp-container")))
  267. (defn closest-shape
  268. [target]
  269. (when-let [shape-el (dom/closest target "[data-shape-id]")]
  270. (.getAttribute shape-el "data-shape-id")))
  271. (defn get-onboard-whiteboard-edn
  272. []
  273. (p/let [^js res (js/fetch "./whiteboard/onboarding.edn") ;; do we need to cache it?
  274. text (.text res)
  275. edn (gp-util/safe-read-string text)]
  276. edn))
  277. (defn clone-whiteboard-from-edn
  278. "Given a tldr, clone the whiteboard page into current active whiteboard"
  279. ([edn]
  280. (when-let [app (state/active-tldraw-app)]
  281. (clone-whiteboard-from-edn edn (.-api app))))
  282. ([{:keys [pages blocks]} api]
  283. (let [page-block (first pages)
  284. ;; FIXME: should also clone normal blocks
  285. shapes (build-shapes page-block blocks)
  286. tldr-page (gp-whiteboard/page-block->tldr-page page-block)
  287. assets (:assets tldr-page)
  288. bindings (:bindings tldr-page)]
  289. (.cloneShapesIntoCurrentPage ^js api (clj->js {:shapes shapes
  290. :assets assets
  291. :bindings bindings})))))
  292. (defn should-populate-onboarding-whiteboard?
  293. "When there is not whiteboard, or there is only whiteboard that is the given page name, we should populate the onboarding whiteboard"
  294. [page-name]
  295. (let [whiteboards (model/get-all-whiteboards (state/get-current-repo))]
  296. (and (or (empty? whiteboards)
  297. (and
  298. (= 1 (count whiteboards))
  299. (= page-name (:block/name (first whiteboards)))))
  300. (not (state/get-onboarding-whiteboard?)))))
  301. (defn populate-onboarding-whiteboard
  302. [api]
  303. (when (some? api)
  304. (-> (p/let [edn (get-onboard-whiteboard-edn)]
  305. (clone-whiteboard-from-edn edn api)
  306. (state/set-onboarding-whiteboard! true))
  307. (p/catch
  308. (fn [e] (js/console.warn "Failed to populate onboarding whiteboard" e))))))
  309. (defn- delete-shapes!
  310. [^js api shapes]
  311. (apply (.-deleteShapes api) (map :id shapes)))
  312. (defn- create-shapes!
  313. [^js api shapes]
  314. (apply (.-createShapes api) (bean/->js shapes)))
  315. (defn- update-shapes!
  316. [^js api shapes]
  317. (apply (.-updateShapes api) (bean/->js shapes)))
  318. (defn- select-shapes
  319. [^js api ids]
  320. (apply (.-selectShapes api) ids))
  321. (defn update-bindings!
  322. [^js tl-page page-name]
  323. (when-let [page (db/entity [:block/name page-name])]
  324. (let [bindings (get-in page [:block/properties :logseq.tldraw.page :bindings])]
  325. (when (seq bindings)
  326. (.updateBindings tl-page (bean/->js bindings))))))
  327. (defn undo!
  328. [{:keys [tx-meta]}]
  329. (history/pause-listener!)
  330. (try
  331. (when-let [app (state/active-tldraw-app)]
  332. (let [{:keys [deleted-shapes new-shapes changed-shapes prev-changed-blocks]} (:data tx-meta)
  333. whiteboard-op (:whiteboard/op tx-meta)
  334. ^js api (.-api app)]
  335. (when api
  336. (case whiteboard-op
  337. :group
  338. (do
  339. (select-shapes api (map :id new-shapes))
  340. (.unGroup api))
  341. :un-group
  342. (do
  343. (select-shapes api (mapcat :children deleted-shapes))
  344. (.doGroup api))
  345. (do
  346. (when (seq deleted-shapes)
  347. (create-shapes! api deleted-shapes))
  348. (when (seq new-shapes)
  349. (delete-shapes! api new-shapes))
  350. (when (seq changed-shapes)
  351. (let [prev-shapes (map (fn [b] (get-in b [:block/properties :logseq.tldraw.shape]))
  352. prev-changed-blocks)]
  353. (update-shapes! api prev-shapes))))))))
  354. (catch :default e
  355. (js/console.error e)))
  356. (history/resume-listener!))
  357. (defn redo!
  358. [{:keys [tx-meta]}]
  359. (history/pause-listener!)
  360. (try
  361. (when-let [app (state/active-tldraw-app)]
  362. (let [{:keys [page-name deleted-shapes new-shapes changed-shapes]} (:data tx-meta)
  363. whiteboard-op (:whiteboard/op tx-meta)
  364. ^js api (.-api app)
  365. tl-page ^js (second (first (.-pages app)))]
  366. (when api
  367. (update-bindings! tl-page page-name)
  368. (case whiteboard-op
  369. :group
  370. (do
  371. (select-shapes api (mapcat :children new-shapes))
  372. (.doGroup api))
  373. :un-group
  374. (do
  375. (select-shapes api (map :id deleted-shapes))
  376. (.unGroup api))
  377. (do
  378. (when (seq deleted-shapes)
  379. (delete-shapes! api deleted-shapes))
  380. (when (seq new-shapes)
  381. (create-shapes! api new-shapes))
  382. (when (seq changed-shapes)
  383. (update-shapes! api changed-shapes)))))))
  384. (catch :default e
  385. (js/console.error e)))
  386. (history/resume-listener!))